Class: Rails::GraphQL::TypeMap
- Inherits:
-
Object
- Object
- Rails::GraphQL::TypeMap
- Extended by:
- ActiveSupport::Autoload
- Defined in:
- lib/rails/graphql/type_map.rb
Overview
GraphQL Type Map
Inspired by ActiveRecord::Type::TypeMap, this class stores all the things defined, their unique name, their basic settings, and correctly index them so they are easy to find whenever necessary.
Items are stored as procs because aliases should fetch whatever the base object is, even if they change in the another point.
The cache stores in the following structure: Namespace -> BaseClass -> ItemKey -> Item
Constant Summary collapse
- FILTER_REGISTER_TRACE =
/((inherited|initialize)'$|schema\.rb:\d+)/.freeze
- NESTED_MODULE =
Type::Creator::NESTED_MODULE
Instance Method Summary collapse
-
#add_dependencies(*list, to:) ⇒ Object
Add a list of dependencies to the type map, so it can lazy load them.
-
#after_register(name_or_key, base_class: :Type, **xargs, &block) ⇒ Object
Add a callback that will trigger when a type is registered under the given set of settings of this method.
-
#associate(namespace, mod) ⇒ Object
Associate the given
moduleto a givennamespace. -
#associated_namespace_of(object) ⇒ Object
Grab all the
module_parentsfrom the object and try to return the first matching result. -
#each_from(namespaces, base_class: nil, exclusive: false, base_classes: nil, &block) ⇒ Object
Iterate over the types of the given
base_classthat are defined on the givennamespaces. -
#exist?(name_or_key, **xargs) ⇒ Boolean
Checks if a given key or name is already defined under the same base class and namespace.
-
#fetch(key_or_name, prevent_register: nil, **xargs) ⇒ Object
Find the given key or name inside the base class either on the given namespace or in the base
:basenamespace. -
#fetch!(key_or_name, base_class: :Type, fallback: nil, **xargs) ⇒ Object
Same as
fetchbut it will raise an exception or retry depending if the base type was already loaded or not. - #inspect ⇒ Object
-
#object_exist?(object, **xargs) ⇒ Boolean
Find if a given object is already defined.
-
#objects(base_classes: nil, namespaces: nil) ⇒ Object
Get the list of all registered objects TODO: Maybe keep it as a lazy enumerator.
-
#postpone_registration(object) ⇒ Object
Mark the given object to be registered later, when a fetch is triggered TODO: Improve this with a Backtrace Cleaner.
-
#register(object) ⇒ Object
Register a given object, which must be a class where the namespaces and the base class can be inferred.
-
#register_alias(name_or_key, key = nil, **xargs, &block) ⇒ Object
Register an item alias.
-
#reset! ⇒ Object
(also: #initialize)
Reset the state of the type mapper.
-
#unregister(*objects) ⇒ Object
Unregister all the provided objects by simply assigning nil to their final value on the index.
-
#version ⇒ Object
Get the current version of the Type Map.
Instance Method Details
#add_dependencies(*list, to:) ⇒ Object
Add a list of dependencies to the type map, so it can lazy load them
74 75 76 |
# File 'lib/rails/graphql/type_map.rb', line 74 def add_dependencies(*list, to:) @dependencies[to].concat(list.flatten.compact) end |
#after_register(name_or_key, base_class: :Type, **xargs, &block) ⇒ Object
Add a callback that will trigger when a type is registered under the given set of settings of this method
289 290 291 292 293 294 295 296 297 298 299 300 301 |
# File 'lib/rails/graphql/type_map.rb', line 289 def after_register(name_or_key, base_class: :Type, **xargs, &block) item = fetch(name_or_key, prevent_register: true, base_class: base_class, **xargs) return block.call(item) unless item.nil? namespaces = sanitize_namespaces(**xargs) callback = ->(n, b, result) do return unless b === base_class && (n === :base || namespaces.include?(n)) block.call(result) true end callbacks[name_or_key].unshift(callback) end |
#associate(namespace, mod) ⇒ Object
Associate the given module to a given namespace. If registered objects have no namespaces, but its module_parents have been associated, then use the value TODO: Maybe turn this into a 1-to-Many association
89 90 91 |
# File 'lib/rails/graphql/type_map.rb', line 89 def associate(namespace, mod) @module_namespaces[mod] = namespace end |
#associated_namespace_of(object) ⇒ Object
Grab all the module_parents from the object and try to return the first matching result
95 96 97 98 99 100 101 102 103 |
# File 'lib/rails/graphql/type_map.rb', line 95 def associated_namespace_of(object) return if @module_namespaces.empty? object.module_parents.find do |mod| ns = @module_namespaces[mod] break ns unless ns.nil? end rescue ::NameError # If any module parent can't be found, there is no much we can do end |
#each_from(namespaces, base_class: nil, exclusive: false, base_classes: nil, &block) ⇒ Object
Iterate over the types of the given base_class that are defined on the given namespaces.
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 |
# File 'lib/rails/graphql/type_map.rb', line 254 def each_from(namespaces, base_class: nil, exclusive: false, base_classes: nil, &block) namespaces = sanitize_namespaces(namespaces: namespaces, exclusive: exclusive) load_dependencies!(_ns: namespaces) register_pending! iterated = Set.new base_classes = GraphQL.enumerate(base_class || base_classes || :Type) enumerator = Enumerator::Lazy.new(namespaces) do |yielder, namespace| next unless @index.key?(namespace) base_classes.each do |a_base_class| @index[namespace][a_base_class]&.each do |key, value| value = value.is_a?(Proc) ? value.call : value next if value.blank? || iterated.include?(value.gql_name) iterated << value.gql_name yielder << value end end end block.present? ? enumerator.each(&block) : enumerator end |
#exist?(name_or_key, **xargs) ⇒ Boolean
Checks if a given key or name is already defined under the same base class and namespace. If exclusive is set to false, then it won’t check the :base namespace when not found on the given namespace.
240 241 242 |
# File 'lib/rails/graphql/type_map.rb', line 240 def exist?(name_or_key, **xargs) !fetch(name_or_key, **xargs, prevent_register: true).nil? end |
#fetch(key_or_name, prevent_register: nil, **xargs) ⇒ Object
Find the given key or name inside the base class either on the given namespace or in the base :base namespace
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 |
# File 'lib/rails/graphql/type_map.rb', line 214 def fetch(key_or_name, prevent_register: nil, **xargs) prevent_register = true if @pending.blank? if prevent_register != true items = prevent_register == true ? nil : ::Array.wrap(prevent_register) skip_register << items.to_set register_pending! end possibilities = ::Array.wrap(key_or_name) possibilities << xargs[:fallback] if xargs.key?(:fallback) base_class = xargs.fetch(:base_class, :Type) sanitize_namespaces(**xargs).find do |namespace| possibilities.find do |item| next if (result = dig(namespace, base_class, item)).nil? next if (result.is_a?(Proc) && (result = result.call).nil?) return result end end ensure skip_register.pop if prevent_register != true end |
#fetch!(key_or_name, base_class: :Type, fallback: nil, **xargs) ⇒ Object
Same as fetch but it will raise an exception or retry depending if the base type was already loaded or not
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
# File 'lib/rails/graphql/type_map.rb', line 191 def fetch!(key_or_name, base_class: :Type, fallback: nil, **xargs) xargs[:base_class] = base_class result = fetch(key_or_name, **xargs) return result unless result.nil? new_loads = load_dependencies!(**xargs) result = fetch(key_or_name, **xargs) if new_loads if result.nil? && fallback result = fetch(fallback, **xargs) report_fallback(key_or_name, result, base_class) end raise NotFoundError, (+<<~MSG).squish if result.nil? Unable to find #{key_or_name.inspect} #{base_class} object. MSG result end |
#inspect ⇒ Object
303 304 305 306 307 308 309 310 311 312 313 314 315 316 |
# File 'lib/rails/graphql/type_map.rb', line 303 def inspect dependencies = @dependencies.each_pair.map do |key, list| +("#{key}: #{list.size}") end.join(', ') (+<<~INFO).squish << '>' #<Rails::GraphQL::TypeMap [index] @namespaces=#{@index.size} @base_classes=#{base_classes.size} @objects=#{@objects} @pending=#{@pending.size} @dependencies={#{dependencies}} INFO end |
#object_exist?(object, **xargs) ⇒ Boolean
Find if a given object is already defined. If exclusive is set to false, then it won’t check the :base namespace
246 247 248 249 250 |
# File 'lib/rails/graphql/type_map.rb', line 246 def object_exist?(object, **xargs) xargs[:base_class] = find_base_class(object) xargs[:namespaces] ||= object.namespaces exist?(object, **xargs) end |
#objects(base_classes: nil, namespaces: nil) ⇒ Object
Get the list of all registered objects TODO: Maybe keep it as a lazy enumerator
280 281 282 283 284 285 |
# File 'lib/rails/graphql/type_map.rb', line 280 def objects(base_classes: nil, namespaces: nil) base_classes ||= self.class.base_classes each_from(namespaces || @index.keys, base_classes: base_classes).select do |obj| obj.is_a?(Helpers::Registerable) end.force end |
#postpone_registration(object) ⇒ Object
Mark the given object to be registered later, when a fetch is triggered TODO: Improve this with a Backtrace Cleaner
80 81 82 83 |
# File 'lib/rails/graphql/type_map.rb', line 80 def postpone_registration(object) source = caller(3).find { |item| item !~ FILTER_REGISTER_TRACE } @pending << [object, source] end |
#register(object) ⇒ Object
Register a given object, which must be a class where the namespaces and the base class can be inferred
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/rails/graphql/type_map.rb', line 107 def register(object) namespaces = sanitize_namespaces(namespaces: object.namespaces, exclusive: true) namespaces << :base if namespaces.empty? base_class = find_base_class(object) ensure_base_class!(base_class) # Cache the name, the key, and the alias proc object_base = namespaces.first object_name = object.gql_name object_key = object.to_sym alias_proc = -> do value = dig(object_base, base_class, object_key) value.is_a?(Proc) ? value.call : value end # TODO Warn when the base key is being assigned to a different object # Register the main type object for both key and name add(object_base, base_class, object_key, object) add(object_base, base_class, object_name, alias_proc) # Register all the aliases plus the object name aliases = object.try(:aliases) aliases&.each do |alias_name| add(object_base, base_class, alias_name, alias_proc) end # For each remaining namespace, register a key and a name alias if namespaces.size > 1 keys_and_names = [object_key, object_name, *aliases] namespaces.drop(1).product(keys_and_names) do |(namespace, key_or_name)| add(namespace, base_class, key_or_name, alias_proc) end end # Return the object for chain purposes @objects += 1 object end |
#register_alias(name_or_key, key = nil, **xargs, &block) ⇒ Object
Register an item alias. Either provide a block that trigger the fetch method to return that item, or a key from the same namespace and base class
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
# File 'lib/rails/graphql/type_map.rb', line 171 def register_alias(name_or_key, key = nil, **xargs, &block) raise ArgumentError, (+<<~MSG).squish unless key.nil? ^ block.nil? Provide either a key or a block in order to register an alias. MSG base_class = xargs.delete(:base_class) || :Type ensure_base_class!(base_class) namespaces = sanitize_namespaces(**xargs, exclusive: true) namespaces << :base if namespaces.empty? block ||= -> do fetch(key, base_class: base_class, namespaces: namespaces, exclusive: true) end namespaces.each { |ns| add(ns, base_class, name_or_key, block) } end |
#reset! ⇒ Object Also known as: initialize
Reset the state of the type mapper
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/rails/graphql/type_map.rb', line 35 def reset! @objects = 0 # Number of types and directives defined @version = nil # Make sure to not keep the same version @skip_register = nil @pending = Concurrent::Array.new @reported_fallbacks = Concurrent::Set.new # Initialize the callbacks @callbacks = Concurrent::Map.new do |hc, key| hc.fetch_or_store(key, Concurrent::Array.new) end # Initialize the dependencies @dependencies = Concurrent::Map.new do |hd, key| hd.fetch_or_store(key, Concurrent::Array.new) end # A registered list of modules and to which namespaces they are # associated with @module_namespaces = Concurrent::Map.new # Initialize the index structure @index = Concurrent::Map.new do |h1, key1| # Namespaces base_class = Concurrent::Map.new do |h2, key2| # Base classes ensure_base_class!(key2) h2.fetch_or_store(key2, Concurrent::Map.new) # Items end h1.fetch_or_store(key1, base_class) end # Provide the first dependencies seed_dependencies! end |
#unregister(*objects) ⇒ Object
Unregister all the provided objects by simply assigning nil to their final value on the index
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/rails/graphql/type_map.rb', line 149 def unregister(*objects) objects.each do |object| namespaces = sanitize_namespaces(namespaces: object.namespaces, exclusive: true) namespaces << :base if namespaces.empty? base_class = find_base_class(object) if object.kind != :source @index[namespaces.first][base_class][object.to_sym] = nil @objects -= 1 end return unless object.const_defined?(NESTED_MODULE, false) nested_mod = object.const_get(NESTED_MODULE, false) nested_mod.constants.each { |name| nested_mod.const_get(name, false).unregister! } object.send(:remove_const, NESTED_MODULE) end end |
#version ⇒ Object
Get the current version of the Type Map. On each reset, the version is changed and can be used to invalidate cache and similar things
30 31 32 |
# File 'lib/rails/graphql/type_map.rb', line 30 def version @version ||= GraphQL.config.version&.first(8) || SecureRandom.hex(8) end |