Skip to content

Commit db116e2

Browse files
committed
Accept user-defined metadata
1 parent 68f1102 commit db116e2

File tree

8 files changed

+141
-34
lines changed

8 files changed

+141
-34
lines changed

guides/defining_your_schema.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,3 +352,40 @@ If the last value of `memo` (or the return of `.final_value`) is a `GraphQL::Ana
352352
`graphql-ruby` includes a few query analyzers:
353353
- `GraphQL::Analysis::QueryDepth` and `GraphQL::Analysis::QueryComplexity` for inspecting query depth and complexity
354354
- `GraphQL::Analysis::MaxQueryDepth` and `GraphQL::Analysis::MaxQueryComplexity` are used internally to implement `max_depth:` and `max_complexity:` options
355+
356+
## Extending type and field definitions
357+
358+
Types, fields, and arguments have a `metadata` hash which accepts values during definition.
359+
360+
First, make a custom definition:
361+
362+
```ruby
363+
GraphQL::ObjectType.accepts_definitions resolves_to_class_names: GraphQL::Define.assign_metadata_key(:resolves_to_class_names)
364+
# or:
365+
# GraphQL::Field.accepts_definitions(...)
366+
# GraphQL::Argument.accepts_definitions(...)
367+
```
368+
369+
370+
Then, use the custom definition:
371+
372+
```ruby
373+
Post = GraphQL::ObjectType.define do
374+
# ...
375+
resolves_to_class_names ["Post", "StaffUpdate"]
376+
end
377+
```
378+
379+
Access `type.metadata` later:
380+
381+
```ruby
382+
SearchResultUnion = GraphQL::Union.define do
383+
# ...
384+
# Use the type's declared `resolves_to_class_names`
385+
# to figure out if `obj` is a member of that type
386+
resolve_type -> (obj, ctx) {
387+
class_name = obj.class.name
388+
possible_types.find { |type| type.metadata[:resolves_to_class_names].include?(class_name) }
389+
}
390+
end
391+
```

lib/graphql/define.rb

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,26 @@
33
require "graphql/define/assign_enum_value"
44
require "graphql/define/assign_global_id_field"
55
require "graphql/define/assign_object_field"
6-
require "graphql/define/assignment_dictionary"
76
require "graphql/define/defined_object_proxy"
87
require "graphql/define/instance_definable"
98
require "graphql/define/non_null_with_bang"
109
require "graphql/define/type_definer"
10+
11+
module GraphQL
12+
module Define
13+
# A helper for definitions that store their value in `#metadata`.
14+
#
15+
# @example Storing application classes with GraphQL types
16+
# # Make a custom definition
17+
# GraphQL::ObjectType.accepts_definitions(resolves_to_class_names: GraphQL::Define.assign_metadata_key(:resolves_to_class_names))
18+
#
19+
# # After definition, read the key from metadata
20+
# PostType.metadata[:resolves_to_class_names] # => [...]
21+
#
22+
# @param [Object] the key to assign in metadata
23+
# @return [#call(defn, value)] an assignment for `.accepts_definitions` which writes `key` to `#metadata`
24+
def self.assign_metadata_key(key)
25+
GraphQL::Define::InstanceDefinable::AssignMetadataKey.new(key)
26+
end
27+
end
28+
end

lib/graphql/define/assignment_dictionary.rb

Lines changed: 0 additions & 26 deletions
This file was deleted.

lib/graphql/define/instance_definable.rb

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,36 @@
11
module GraphQL
22
module Define
3-
# This module provides the `.define { ... }` API for {GraphQL::BaseType}, {GraphQL::Field}, {GraphQL::Argument} and {GraphQL::Directive}.
3+
# This module provides the `.define { ... }` API for
4+
# {GraphQL::BaseType}, {GraphQL::Field} and others.
5+
#
6+
# Calling `.accepts_definitions(...)` creates:
7+
#
8+
# - a keyword to the `.define` method
9+
# - a helper method in the `.define { ... }` block
10+
#
11+
# The `.define { ... }` block will be called lazily. To be sure it has been
12+
# called, use the private method `#ensure_defined`. That will call the
13+
# definition block if it hasn't been called already.
414
#
515
# The goals are:
16+
#
617
# - Minimal overhead in consuming classes
718
# - Independence between consuming classes
819
# - Extendable by third-party libraries without monkey-patching or other nastiness
920
#
1021
# @example Make a class definable
1122
# class Car
12-
# attr_accessor :make, :model, :all_wheel_drive
13-
#
23+
# attr_accessor :make, :model
1424
# accepts_definitions(
1525
# # These attrs will be defined with plain setters, `{attr}=`
1626
# :make, :model,
1727
# # This attr has a custom definition which applies the config to the target
1828
# doors: -> (car, doors_count) { doors_count.times { car.doors << Door.new } }
1929
# )
30+
#
31+
# def initialize
32+
# @doors = []
33+
# end
2034
# end
2135
#
2236
# # Create an instance with `.define`:
@@ -31,14 +45,17 @@ module Define
3145
#
3246
# @example Extending the definition of a class
3347
# # Add some definitions:
34-
# Car.accepts_definitions(:all_wheel_drive)
48+
# Car.accepts_definitions(all_wheel_drive: GraphQL::Define.assign_metadata_key(:all_wheel_drive))
3549
#
3650
# # Use it in a definition
3751
# subaru_baja = Car.define do
3852
# # ...
3953
# all_wheel_drive true
4054
# end
4155
#
56+
# # Access it from metadata
57+
# subaru_baja.metadata[:all_wheel_drive] # => true
58+
#
4259
module InstanceDefinable
4360
def self.included(base)
4461
base.extend(ClassMethods)
@@ -50,6 +67,14 @@ def definition_proc=(defn_block)
5067
@definition_proc = defn_block
5168
end
5269

70+
# `metadata` can store arbitrary key-values with an object.
71+
#
72+
# @return [Hash<Object, Object>] Hash for user-defined storage
73+
def metadata
74+
ensure_defined
75+
@metadata ||= {}
76+
end
77+
5378
private
5479

5580
# Run the definition block if it hasn't been run yet.
@@ -91,7 +116,17 @@ def define(**kwargs, &block)
91116
# Each symbol in `accepts` will be assigned with `{key}=`.
92117
# The last entry in accepts may be a hash of name-proc pairs for custom definitions.
93118
def accepts_definitions(*accepts)
94-
@own_dictionary = own_dictionary.merge(AssignmentDictionary.create(*accepts))
119+
new_assignments = if accepts.last.is_a?(Hash)
120+
accepts.pop.dup
121+
else
122+
{}
123+
end
124+
125+
accepts.each do |key|
126+
new_assignments[key] = AssignAttribute.new(key)
127+
end
128+
129+
@own_dictionary = own_dictionary.merge(new_assignments)
95130
end
96131

97132
# Define a reader and writer for each of `attr_names` which
@@ -125,6 +160,26 @@ def own_dictionary
125160
@own_dictionary ||= {}
126161
end
127162
end
163+
164+
class AssignMetadataKey
165+
def initialize(key)
166+
@key = key
167+
end
168+
169+
def call(defn, value)
170+
defn.metadata[@key] = value
171+
end
172+
end
173+
174+
class AssignAttribute
175+
def initialize(attr_name)
176+
@attr_assign_method = :"#{attr_name}="
177+
end
178+
179+
def call(defn, value)
180+
defn.public_send(@attr_assign_method, value)
181+
end
182+
end
128183
end
129184
end
130185
end

spec/graphql/base_type_spec.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,8 @@
2121
GraphQL::ListType.new(of_type: !MilkType)
2222
)
2323
end
24+
25+
it "Accepts arbitrary metadata" do
26+
assert_equal ["Cheese"], CheeseType.metadata[:class_names]
27+
end
2428
end

spec/graphql/define/instance_definable_spec.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def self.call(plant, plant_range)
1212
class Vegetable
1313
include GraphQL::Define::InstanceDefinable
1414
lazy_defined_attr_accessor :name, :start_planting_on, :end_planting_on
15-
accepts_definitions :name, plant_between: DefinePlantBetween
15+
accepts_definitions :name, plant_between: DefinePlantBetween, color: GraphQL::Define.assign_metadata_key(:color)
1616

1717
# definition added later:
1818
lazy_defined_attr_accessor :height
@@ -61,4 +61,11 @@ class Vegetable
6161
assert_equal Date.new(2000, 7, 1), okra.end_planting_on
6262
end
6363
end
64+
65+
describe "#metadata" do
66+
it "gets values from definitions" do
67+
arugula = Garden::Vegetable.define(name: "Arugula", color: :green)
68+
assert_equal :green, arugula.metadata[:color]
69+
end
70+
end
6471
end

spec/graphql/field_spec.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
assert_equal(DairyProductUnion, field.type)
1818
end
1919

20-
2120
describe ".property " do
2221
let(:field) do
2322
GraphQL::Field.define do
@@ -107,4 +106,11 @@ def acts_like_default_resolver(field, old_prop, new_prop)
107106
assert_equal "Xyz", resolved_source
108107
end
109108
end
109+
110+
describe "#metadata" do
111+
it "accepts user-defined metadata" do
112+
similar_cheese_field = CheeseType.get_field("similarCheese")
113+
assert_equal [:cheeses, :milks], similar_cheese_field.metadata[:joins]
114+
end
115+
end
110116
end

spec/support/dairy_app.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
class NoSuchDairyError < StandardError; end
44

5+
GraphQL::Field.accepts_definitions(joins: GraphQL::Define.assign_metadata_key(:joins))
6+
GraphQL::BaseType.accepts_definitions(class_names: GraphQL::Define.assign_metadata_key(:class_names))
7+
58
EdibleInterface = GraphQL::InterfaceType.define do
69
name "Edible"
710
description "Something you can eat, yum"
@@ -26,6 +29,7 @@ class NoSuchDairyError < StandardError; end
2629

2730
CheeseType = GraphQL::ObjectType.define do
2831
name "Cheese"
32+
class_names ["Cheese"]
2933
description "Cultured dairy product"
3034
interfaces [EdibleInterface, AnimalProductInterface]
3135

@@ -39,6 +43,8 @@ class NoSuchDairyError < StandardError; end
3943

4044
# Or can define by block, `resolve ->` should override `property:`
4145
field :similarCheese, CheeseType, "Cheeses like this one", property: :this_should_be_overriden do
46+
# metadata test
47+
joins [:cheeses, :milks]
4248
argument :source, !types[!DairyAnimalEnum]
4349
resolve -> (t, a, c) {
4450
# get the strings out:

0 commit comments

Comments
 (0)