module GraphQL::Define::InstanceDefinable

This module provides the `.define { … }` API for {GraphQL::BaseType}, {GraphQL::Field} and others.

Calling `.accepts_definitions(…)` creates:

The `.define { … }` block will be called lazily. To be sure it has been called, use the private method `#ensure_defined`. That will call the definition block if it hasn't been called already.

The goals are:

@example Make a class definable

class Car
  include GraphQL::Define::InstanceDefinable
  attr_accessor :make, :model, :doors
  accepts_definitions(
    # These attrs will be defined with plain setters, `{attr}=`
    :make, :model,
    # This attr has a custom definition which applies the config to the target
    doors: ->(car, doors_count) { doors_count.times { car.doors << Door.new } }
  )
  ensure_defined(:make, :model, :doors)

  def initialize
    @doors = []
  end
end

class Door; end;

# Create an instance with `.define`:
subaru_baja = Car.define do
  make "Subaru"
  model "Baja"
  doors 4
end

# The custom proc was applied:
subaru_baja.doors #=> [<Door>, <Door>, <Door>, <Door>]

@example Extending the definition of a class

# Add some definitions:
Car.accepts_definitions(all_wheel_drive: GraphQL::Define.assign_metadata_key(:all_wheel_drive))

# Use it in a definition
subaru_baja = Car.define do
  # ...
  all_wheel_drive true
end

# Access it from metadata
subaru_baja.metadata[:all_wheel_drive] # => true

@example Extending the definition of a class via a plugin

# A plugin is any object that responds to `.use(definition)`
module SubaruCar
  extend self

  def use(defn)
    # `defn` has the same methods as within `.define { ... }` block
    defn.make "Subaru"
    defn.doors 4
  end
end

# Use the plugin within a `.define { ... }` block
subaru_baja = Car.define do
  use SubaruCar
  model 'Baja'
end

subaru_baja.make # => "Subaru"
subaru_baja.doors # => [<Door>, <Door>, <Door>, <Door>]

@example Making a copy with an extended definition

# Create an instance with `.define`:
subaru_baja = Car.define do
  make "Subaru"
  model "Baja"
  doors 4
end

# Then extend it with `#redefine`
two_door_baja = subaru_baja.redefine do
  doors 2
end

Public Class Methods

included(base) click to toggle source
# File lib/graphql/define/instance_definable.rb, line 97
def self.included(base)
  base.extend(ClassMethods)
  base.ensure_defined(:metadata)
end

Public Instance Methods

define(**kwargs, &block) click to toggle source

Mutate this instance using functions from its {.definition}s. Keywords or helpers in the block correspond to keys given to `accepts_definitions`.

Note that the block is not called right away – instead, it's deferred until one of the defined fields is needed. @return [void]

# File lib/graphql/define/instance_definable.rb, line 115
def define(**kwargs, &block)
  # make sure the previous definition_proc was executed:
  ensure_defined
  stash_dependent_methods
  @pending_definition = Definition.new(kwargs, block)
  nil
end
initialize_copy(other) click to toggle source
Calls superclass method
# File lib/graphql/define/instance_definable.rb, line 133
def initialize_copy(other)
  super
  @metadata = other.metadata.dup
end
metadata() click to toggle source

`metadata` can store arbitrary key-values with an object.

@return [Hash<Object, Object>] Hash for user-defined storage

# File lib/graphql/define/instance_definable.rb, line 105
def metadata
  @metadata ||= {}
end
redefine(**kwargs, &block) click to toggle source

Shallow-copy this object, then apply new definitions to the copy. @see {#define} for arguments @return [InstanceDefinable] A new instance, with any extended definitions

# File lib/graphql/define/instance_definable.rb, line 126
def redefine(**kwargs, &block)
  ensure_defined
  new_inst = self.dup
  new_inst.define(**kwargs, &block)
  new_inst
end

Private Instance Methods

ensure_defined() click to toggle source

Run the definition block if it hasn't been run yet. This can only be run once: the block is deleted after it's used. You have to call this before using any value which could come from the definition block. @return [void]

# File lib/graphql/define/instance_definable.rb, line 145
def ensure_defined
  if @pending_definition
    defn = @pending_definition
    @pending_definition = nil

    revive_dependent_methods

    begin
      defn_proxy = DefinedObjectProxy.new(self)
      # Apply definition from `define(...)` kwargs
      defn.define_keywords.each do |keyword, value|
        defn_proxy.public_send(keyword, value)
      end
      # and/or apply definition from `define { ... }` block
      if defn.define_proc
        defn_proxy.instance_eval(&defn.define_proc)
      end
    rescue StandardError
      # The definition block failed to run, so make this object pending again:
      stash_dependent_methods
      @pending_definition = defn
      raise
    end
  end
  nil
end
revive_dependent_methods() click to toggle source

Take the pending methods and put them back on this object's singleton class. This reverts the process done by {#stash_dependent_methods} @return [void]

# File lib/graphql/define/instance_definable.rb, line 175
def revive_dependent_methods
  pending_methods = @pending_methods
  self.singleton_class.class_eval {
    pending_methods.each do |method|
      define_method(method.name, method)
    end
  }
  @pending_methods = nil
end
stash_dependent_methods() click to toggle source

Find the method names which were declared as definition-dependent, then grab the method definitions off of this object's class and store them for later.

Then make a dummy method for each of those method names which:

  • Triggers the pending definition, if there is one

  • Calls the same method again.

It's assumed that {#ensure_defined} will put the original method definitions back in place with {#revive_dependent_methods}. @return [void]

# File lib/graphql/define/instance_definable.rb, line 197
def stash_dependent_methods
  method_names = self.class.ensure_defined_method_names
  @pending_methods = method_names.map { |n| self.class.instance_method(n) }
  self.singleton_class.class_eval do
    method_names.each do |method_name|
      define_method(method_name) { |*args, &block|
        ensure_defined
        self.send(method_name, *args, &block)
      }
    end
  end
end