graphql-decorate 0.2.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e23e910653e03c46982bf8916ab3f15cde1be96a6436cabbf0271c40a11f1b4d
4
- data.tar.gz: d71940ac1feabb05b64228812ad1768f8f4f288e2ff105a619ee8cd02b00ce8a
3
+ metadata.gz: f51b30fb5841e235310b83ab3d9b697d86b420c1a807bb0500b73dcf231d295b
4
+ data.tar.gz: 4c02e6130d6955d6d9fbde06a9fe45352e1bd7f5dba7ec7e1d60c9bb846242c0
5
5
  SHA512:
6
- metadata.gz: 457e1cd6d2c62e5f463ccfacd8fa62f3e444c4b980d5e1d0b24b7b59aa960bcbf3c38cb0013055c793bafb3af86a5c0c2a68ae687b0c6af66de0b5b24b96d811
7
- data.tar.gz: 678fb9bd85a0de6e6f0d336f2e812f91e1a326c7e3bff426159ff2bf2a83af5bc93bdf1b4ff490650f3a851f8d42b0370955d552bf57d22a47f183e8c2335462
6
+ metadata.gz: 2aaa588eb463291941b8c4678daaaead6479b2526d9799998889186cccece07770cded6ec469108e2b92346317ab804fed0e7fd24acb80679251b2e0cb63dc43
7
+ data.tar.gz: 1c2a48487fd6b82739b501968578f039c89e7769db849daf955f152b72ec2e353b43790ea159400580f0e99f8b464c7a8f7cd98e11b7bd7a818ce42df7b2299f
@@ -11,10 +11,11 @@ jobs:
11
11
 
12
12
  steps:
13
13
  - uses: actions/checkout@v2
14
- - name: Set up Ruby 2.6
15
- uses: actions/setup-ruby@v1
14
+ - name: Set up Ruby
15
+ uses: ruby/setup-ruby@v1
16
16
  with:
17
- ruby-version: 2.6.x
17
+ ruby-version: 2.6
18
+ bundler-cache: true
18
19
 
19
20
  - name: Verify version number matches
20
21
  run: |
@@ -0,0 +1,44 @@
1
+ name: Ruby
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+ pull_request:
7
+ branches: [ master ]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ ruby-version: ['2.6', '2.7', '3.0']
15
+
16
+ steps:
17
+ - uses: actions/checkout@v2
18
+
19
+ - name: Set up Ruby
20
+ uses: ruby/setup-ruby@v1
21
+ with:
22
+ ruby-version: ${{ matrix.ruby-version }}
23
+ bundler-cache: true
24
+
25
+ - name: Run tests
26
+ run: bundle exec rake
27
+
28
+ - name: Run RuboCop
29
+ run: bundle exec rubocop
30
+
31
+ - name: Upload coverage results
32
+ uses: actions/upload-artifact@master
33
+ with:
34
+ name: coverage-report
35
+ path: coverage
36
+
37
+ - name: Check documentation completion
38
+ run: |
39
+ completion_percentage=$(bundle exec yard | tee /dev/stdout | tail -1 | cut -d'%' -f1 | xargs)
40
+ echo $completion_percentage
41
+ if [[ $completion_percentage != "100.00" ]]; then
42
+ echo "YARD documentation must be at 100%"
43
+ exit 2;
44
+ fi
data/.rubocop.yml ADDED
@@ -0,0 +1,14 @@
1
+ require:
2
+ rubocop-rspec
3
+ AllCops:
4
+ NewCops: enable
5
+ TargetRubyVersion: 2.6
6
+ SuggestExtensions: false
7
+ RSpec/FilePath:
8
+ CustomTransform:
9
+ GraphQL: graphql
10
+ Metrics/BlockLength:
11
+ Exclude:
12
+ - 'spec/**/*.rb'
13
+ RSpec/MessageSpies:
14
+ EnforcedStyle: receive
data/Gemfile CHANGED
@@ -1,6 +1,8 @@
1
- source "https://rubygems.org"
1
+ # frozen_string_literal: true
2
2
 
3
- git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
6
 
5
7
  # Specify your gem's dependencies in graphql-decorate.gemspec
6
8
  gemspec
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # GraphQL Decorate
2
2
 
3
- `graphql-decorate` adds an easy-to-use interface for decorating types in `graphql-ruby`. It lets
3
+ `graphql-decorate` adds an easy-to-use interface for decorating types in [`graphql-ruby`](https://github.com/rmosolgo/graphql-ruby). It lets
4
4
  you move logic out of your type files and keep them declarative.
5
5
 
6
6
  ## Installation
@@ -59,24 +59,28 @@ In this example, the `Rectangle` type is being decorated with a `RectangleDecora
59
59
  `RectangleDecorator`. All of the methods on the decorator are accessible on the type.
60
60
 
61
61
  ### Decorators
62
- By default, `graphql-decorate` is set up to work with Draper-style decorators. These decorators
62
+ By default, `graphql-decorate` is set up to work with [`draper`](https://github.com/drapergem/draper) style decorators. These decorators
63
63
  provide a `decorate` method that wraps the original object and returns an instance of the
64
- decorator. They can also take in an additional context hash.
64
+ decorator. They can also take in additional metadata.
65
65
  ```ruby
66
- RectangleDecorator.decorate(rectangle, context)
66
+ RectangleDecorator.decorate(rectangle, context: metadata)
67
67
  ```
68
68
  If you are using a different decorator pattern then you can override this default behavior in
69
69
  the configuration.
70
70
  ```ruby
71
71
  GraphQL::Decorate.configure do |config|
72
- config.decorate do |decorator_class, object, _context|
72
+ config.decorate do |decorator_class, object, _metadata|
73
73
  decorator_class.decorate_differently(object)
74
74
  end
75
75
  end
76
76
  ```
77
77
 
78
78
  ### Types
79
- Three methods are made available on your type classes
79
+ Two methods are made available on your type classes: `decorate_with` and `decorate_metadata`.
80
+ Every method that yields the underlying object will also yield the current GraphQL `context`.
81
+ If decoration depends on some context in the current query then you can access it when the field
82
+ is resolved.
83
+
80
84
  #### decorate_with
81
85
  `decorate_with` accepts a decorator class that will decorate every instance of your type.
82
86
  ```ruby
@@ -85,12 +89,11 @@ class Rectangle < GraphQL::Schema::Object
85
89
  end
86
90
  ```
87
91
 
88
- #### decorate_when
89
- `decorate_when` accepts a block which yields the underlying object. If you have multiple
92
+ `decorate_with` optionally accepts a block which yields the underlying object. If you have multiple
90
93
  possible decorator classes you can return the one intended for the underling object.
91
94
  ```ruby
92
95
  class Rectangle < GraphQL::Schema::Object
93
- decorate_when do |object|
96
+ decorate_with do |object, _graphql_context|
94
97
  if object.length == object.width
95
98
  SquareDecorator
96
99
  else
@@ -100,30 +103,50 @@ class Rectangle < GraphQL::Schema::Object
100
103
  end
101
104
  ```
102
105
 
103
- #### decorator_context
104
- `decorator_context` accepts a block which yields the underlying object. If your decorator pattern
105
- allows additional context being passed into the decorators, you can define it here.
106
+ #### decorate_metadata
107
+ If your decorator pattern allows additional metadata to be passed into the decorators, you can
108
+ define it here. By default every metadata hash will contain `{ graphql: true }`. This is
109
+ useful if your decorator logic needs to diverge when used in a GraphQL context. Ideally your
110
+ decorators are agnostic to where they are being used, but it is available if needed.
111
+
112
+ `decorate_metadata` yields a `GraphQL::Decorate::Metadata` metadata instance. It responds to two
113
+ methods: `unscoped` and `scoped`. `unscoped` sets metadata for a resolved field. `scoped` sets
114
+ metadata for a resolved field and all of its child fields. `unscoped` and `scoped` are expected
115
+ to return `Hash`s.
116
+
106
117
  ```ruby
107
118
  class Rectangle < GraphQL::Schema::Object
108
- decorator_context do |object|
109
- {
110
- name: object.name
111
- }
119
+ decorate_metadata do |metadata|
120
+ metadata.unscoped do |object, _graphql_context|
121
+ {
122
+ name: object.name
123
+ }
124
+ end
125
+
126
+ metadata.scoped do |object, _graphql_context|
127
+ {
128
+ inside_rectangle: true
129
+ }
130
+ end
112
131
  end
113
132
  end
114
133
  ```
115
- `RectangleDecorator` will be initialized with a context of `{ name: <object_name> }`.
134
+ `RectangleDecorator` will be initialized with metadata `{ name: <object_name>,
135
+ inside_rectangle: true, graphql: true }`. All child fields of `Rectangle` will be initialized
136
+ with metadata `{ inside_rectangle: true, graphql: true }`.
116
137
 
117
138
  #### Combinations
118
- You can mix and match these methods to suit your needs. Note that if `decorate_with` and
119
- `decorate_when` are both provided that `decorate_with` will take precedence.
139
+ You can mix and match these methods to suit your needs. Note that if `unscoped` and
140
+ `scoped` are both provided for metadata that `scoped` will override any shared keys.
120
141
  ```ruby
121
142
  class Rectangle < GraphQL::Schema::Object
122
143
  decorate_with RectangleDecorator
123
- decorator_context do |object|
124
- {
125
- name: object.name
126
- }
144
+ decorate_metadata do |metadata|
145
+ metadata.scoped do |object, _graphql_context|
146
+ {
147
+ name: object.name
148
+ }
149
+ end
127
150
  end
128
151
  end
129
152
  ```
@@ -141,8 +164,11 @@ end
141
164
 
142
165
  ## Development
143
166
 
144
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
167
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to
168
+ run the tests. You can also run `bin/console` for an interactive prompt that will allow you to
169
+ experiment.
145
170
 
146
171
  ## License
147
172
 
148
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
173
+ The gem is available as open source under the terms of the
174
+ [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile CHANGED
@@ -1,6 +1,8 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
3
5
 
4
6
  RSpec::Core::RakeTask.new(:spec)
5
7
 
6
- task :default => :spec
8
+ task default: :spec
data/bin/console CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- require "bundler/setup"
4
- require "graphql/decorate"
4
+ require 'bundler/setup'
5
+ require 'graphql/decorate'
5
6
 
6
7
  # You can add fixtures and/or initialization code here to make experimenting
7
8
  # with your gem easier. You can also use a different console, if you like.
@@ -10,5 +11,5 @@ require "graphql/decorate"
10
11
  # require "pry"
11
12
  # Pry.start
12
13
 
13
- require "irb"
14
+ require 'irb'
14
15
  IRB.start(__FILE__)
@@ -1,30 +1,40 @@
1
+ # frozen_string_literal: true
1
2
 
2
- lib = File.expand_path("../lib", __FILE__)
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "graphql/decorate/version"
5
+ require 'graphql/decorate/version'
5
6
 
6
7
  Gem::Specification.new do |spec|
7
- spec.name = "graphql-decorate"
8
+ spec.name = 'graphql-decorate'
8
9
  spec.version = GraphQL::Decorate::VERSION
9
- spec.authors = ["Ben Brook"]
10
- spec.email = ["bbrook154@gmail.com"]
10
+ spec.authors = ['Ben Brook']
11
+ spec.email = ['bbrook154@gmail.com']
11
12
 
12
13
  spec.summary = 'A decorator integration for the GraphQL gem'
13
14
  spec.homepage = 'https://www.github.com/TrueCar/graphql-decorate'
14
- spec.license = "MIT"
15
+ spec.license = 'MIT'
15
16
 
16
17
  # Specify which files should be added to the gem when it is released.
17
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
18
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
19
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ # The `git ls-files -z` loads the files in the RubyGem that have been added
19
+ # into git.
20
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
21
+ `git ls-files -z`.split("\x0").reject do |f|
22
+ f.match(%r{^(test|spec|features)/})
23
+ end
20
24
  end
21
- spec.bindir = "exe"
25
+ spec.bindir = 'exe'
22
26
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
- spec.require_paths = ["lib"]
27
+ spec.require_paths = ['lib']
24
28
 
25
- spec.add_runtime_dependency "graphql", ">= 1.3", "< 2"
29
+ spec.required_ruby_version = '>= 2.6.0'
26
30
 
27
- spec.add_development_dependency "bundler", ">= 2"
28
- spec.add_development_dependency "rake", "~> 10.0"
29
- spec.add_development_dependency "rspec", "~> 3.0"
31
+ spec.add_runtime_dependency 'graphql', '>= 1.3', '< 2'
32
+
33
+ spec.add_development_dependency 'bundler', '>= 2'
34
+ spec.add_development_dependency 'rake', '~> 10.0'
35
+ spec.add_development_dependency 'rspec', '~> 3.0'
36
+ spec.add_development_dependency 'rubocop', ' >= 1.11.0 '
37
+ spec.add_development_dependency 'rubocop-rspec', '2.2.0'
38
+ spec.add_development_dependency 'simplecov', '~> 0.21.2'
39
+ spec.add_development_dependency 'yard', '~> 0.9.26'
30
40
  end
@@ -1,13 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'graphql'
2
4
  require_relative 'decorate/version'
3
5
  require_relative 'decorate/configuration'
4
6
  require_relative 'decorate/object_integration'
5
7
  require_relative 'decorate/field_integration'
6
8
  require_relative 'decorate/field_extension'
7
- require_relative 'decorate/object'
9
+ require_relative 'decorate/decoration'
8
10
  require_relative 'decorate/type_attributes'
9
- require_relative 'decorate/field_context'
10
- require_relative 'decorate/connection'
11
+ require_relative 'decorate/undecorated_field'
12
+ require_relative 'decorate/connection_wrapper'
13
+ require_relative 'decorate/metadata'
11
14
 
12
15
  # Matching the graphql-ruby namespace
13
16
  module GraphQL
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module GraphQL
2
4
  module Decorate
3
5
  # Allows overriding default decoration and custom collection class behavior.
@@ -9,8 +11,8 @@ module GraphQL
9
11
  attr_accessor :custom_collection_classes
10
12
 
11
13
  def initialize
12
- @evaluate_decorator = lambda do |decorator_class, object, context|
13
- decorator_class.decorate(object, context: context)
14
+ @evaluate_decorator = lambda do |decorator_class, object, metadata|
15
+ decorator_class.decorate(object, context: metadata)
14
16
  end
15
17
  @custom_collection_classes = []
16
18
  end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Decorate
5
+ # Wraps a GraphQL::Pagination::ConnectionWrapper object to decorate values after pagination is applied.
6
+ class ConnectionWrapper
7
+ # @param connection [GraphQL::Pagination::Connection] ConnectionWrapper being decorated
8
+ # @param node_type [GraphQL::Schema::Object] Type class of the connection's node
9
+ # @param context [GraphQL::Query::Context] Current query context
10
+ def self.wrap(connection, node_type, context)
11
+ @connection_class = connection.class
12
+ new(connection, node_type, context)
13
+ end
14
+
15
+ # @return [GraphQL::Pagination::Connection] ConnectionWrapper being decorated
16
+ attr_reader :connection
17
+
18
+ # @param connection [GraphQL::Pagination::Connection] ConnectionWrapper being decorated
19
+ # # @param node_type [GraphQL::Schema::Object] Type class of the connection's node
20
+ # @param context [GraphQL::Query::Context] Current query context
21
+ def initialize(connection, node_type, context)
22
+ @connection = connection
23
+ @node_type = node_type
24
+ @context = context
25
+ end
26
+
27
+ # @return [Array] Decorated nodes after pagination is applied
28
+ def nodes
29
+ nodes = @connection.nodes
30
+ nodes.map do |node|
31
+ unresolved_field = GraphQL::Decorate::UndecoratedField.new(node, node_type, connection.parent,
32
+ connection.field.owner, context)
33
+ GraphQL::Decorate::Decoration.decorate(unresolved_field)
34
+ end
35
+ end
36
+
37
+ # @see nodes
38
+ # @return [Array] Decorated nodes after pagination is applied
39
+ def edge_nodes
40
+ nodes
41
+ end
42
+
43
+ class << self
44
+ private
45
+
46
+ def method_missing(symbol, *args, &block)
47
+ @connection_class.send(symbol, *args, &block) || super
48
+ end
49
+
50
+ def respond_to_missing?(method, include_private = false)
51
+ @connection_class.respond_to?(method, include_private)
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ attr_reader :node_type, :context
58
+
59
+ def method_missing(symbol, *args, &block)
60
+ @connection.send(symbol, *args, &block) || super
61
+ end
62
+
63
+ def respond_to_missing?(method, include_private = false)
64
+ @connection.respond_to?(method, include_private)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Decorate
5
+ # Handles decorating an value at runtime given its current field.
6
+ class Decoration
7
+ # Resolve the undecorated_field.value with decoration.
8
+ # @param undecorated_field [GraphQL::Decorate::UndecoratedField]
9
+ # @return [Object] Decorated undecorated_field.value if possible, otherwise the original undecorated_field.value.
10
+ def self.decorate(undecorated_field)
11
+ new(undecorated_field).decorate
12
+ end
13
+
14
+ # @param undecorated_field [GraphQL::Decorate::UndecoratedField]
15
+ def initialize(undecorated_field)
16
+ @undecorated_field = undecorated_field
17
+ end
18
+
19
+ # @return [Object] Decorated undecorated_field.value if possible, otherwise the original undecorated_field.value.
20
+ def decorate
21
+ if undecorated_field.decorator_class
22
+ GraphQL::Decorate.configuration.evaluate_decorator.call(undecorated_field.decorator_class,
23
+ undecorated_field.value, undecorated_field.metadata)
24
+ else
25
+ undecorated_field.value
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :undecorated_field
32
+ end
33
+ end
34
+ end
@@ -1,26 +1,44 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GraphQL
3
4
  module Decorate
4
5
  # Extension run after fields are resolved to decorate their value.
5
6
  class FieldExtension < GraphQL::Schema::FieldExtension
6
7
  # Extension to be called after lazy loading.
7
8
  # @param context [GraphQL::Query::Context] The current GraphQL query context.
8
- # @param value [Object, GraphQL::Schema::Object] The object being decorated. Can be a schema object if the field hasn't been resolved yet.
9
- # @return [::Object, GraphQL::Decorate::Connection] Decorated object.
10
- def after_resolve(context:, value:, **_rest)
9
+ # @param value [Object, GraphQL::Schema::Object, GraphQL::Pagination::Connection] The object being decorated. Can
10
+ # be a schema object if the field hasn't been resolved yet or a connection.
11
+ # @param object [Object] Object the field is being resolved on.
12
+ # @return [Object, GraphQL::Decorate::ConnectionWrapper] Decorated object.
13
+ def after_resolve(context:, value:, object:, **_rest)
11
14
  return if value.nil?
12
15
 
13
- field_context = GraphQL::Decorate::FieldContext.new(context, options)
16
+ resolve_decorated_value(value, object, context)
17
+ end
18
+
19
+ private
20
+
21
+ def resolve_decorated_value(value, parent_object, context)
22
+ type = extract_type(context.to_h[:current_field].type)
14
23
  if value.is_a?(GraphQL::Pagination::Connection)
15
- GraphQL::Decorate::Connection.new(value, field_context)
16
- elsif collection_classes.any? { |c| value.is_a?(c) }
17
- value.map { |item| decorate(item, field_context) }
24
+ GraphQL::Decorate::ConnectionWrapper.wrap(value, type, context)
25
+ elsif collection?(value)
26
+ value.map do |item|
27
+ decorate(item, type, parent_object.object, parent_object.class, context)
28
+ end
18
29
  else
19
- decorate(value, field_context)
30
+ decorate(value, type, parent_object.object, parent_object.class, context)
20
31
  end
21
32
  end
22
33
 
23
- private
34
+ def decorate(value, type, parent_object, parent_type, context)
35
+ undecorated_field = GraphQL::Decorate::UndecoratedField.new(value, type, parent_object, parent_type, context)
36
+ GraphQL::Decorate::Decoration.decorate(undecorated_field)
37
+ end
38
+
39
+ def collection?(value)
40
+ collection_classes.any? { |c| value.is_a?(c) }
41
+ end
24
42
 
25
43
  def collection_classes
26
44
  klasses = [Array] + GraphQL::Decorate.configuration.custom_collection_classes
@@ -28,8 +46,12 @@ module GraphQL
28
46
  klasses
29
47
  end
30
48
 
31
- def decorate(object, field_context)
32
- GraphQL::Decorate::Object.new(object, field_context).decorate
49
+ def extract_type(field)
50
+ if field.respond_to?(:of_type)
51
+ extract_type(field.of_type)
52
+ else
53
+ field
54
+ end
33
55
  end
34
56
  end
35
57
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module GraphQL
2
4
  module Decorate
3
5
  # Extends default field behavior and adds extension to the field if it should be decorated.
@@ -8,30 +10,8 @@ module GraphQL
8
10
  def initialize(type:, **rest, &block)
9
11
  super
10
12
  field_type = [type].flatten(1).first
11
- extension_options = get_extension_options(field_type)
12
- extend_with_decorator(extension_options) if extension_options
13
- end
14
-
15
- private
16
-
17
- def get_extension_options(type)
18
- type_attributes = GraphQL::Decorate::TypeAttributes.new(type)
19
- return unless type_attributes.decorator_class
20
-
21
- {
22
- decorator_class: type_attributes.decorator_class,
23
- decorator_evaluator: type_attributes.decorator_evaluator,
24
- decorator_context_evaluator: type_attributes.decorator_context_evaluator,
25
- unresolved_type: type_attributes.unresolved_type
26
- }
27
- end
28
-
29
- def extend_with_decorator(options)
30
- extension(GraphQL::Decorate::FieldExtension, options)
31
- # ext = GraphQL::Decorate::FieldExtension.new(field: self, options: options)
32
- # @extensions = @extensions.dup
33
- # @extensions.unshift(ext)
34
- # @extensions.freeze
13
+ type_attributes = GraphQL::Decorate::TypeAttributes.new(field_type)
14
+ extension(GraphQL::Decorate::FieldExtension) if type_attributes.decoratable?
35
15
  end
36
16
  end
37
17
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Decorate
5
+ # Contains methods to evaluate different types of metadata
6
+ class Metadata
7
+ # @return [Proc]
8
+ attr_reader :unscoped_proc
9
+
10
+ # @return [Proc]
11
+ attr_reader :scoped_proc
12
+
13
+ def initialize
14
+ @unscoped_proc = nil
15
+ @scoped_proc = nil
16
+ end
17
+
18
+ # @yield [object, graphql_context] Evaluate metadata for a single resolved field
19
+ def unscoped(&block)
20
+ @unscoped_proc = block
21
+ end
22
+
23
+ # @yield [object, graphql_context] Evaluate metadata for a resolved field and all child fields
24
+ def scoped(&block)
25
+ @scoped_proc = block
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,41 +1,31 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module GraphQL
2
4
  module Decorate
3
5
  # Extends GraphQL::Schema::Object classes with methods to set the desired decorator class and context.
4
6
  module ObjectIntegration
5
7
  # Decorate the type with a decorator class.
6
8
  # @param klass [Class] Class the object should be decorated with.
7
- def decorate_with(klass)
9
+ def decorate_with(klass = nil, &block)
8
10
  @decorator_class = klass
9
- end
10
-
11
- # Dynamically choose the decorator class based on the underlying object.
12
- # @yield [object] Gives the underlying object to the block.
13
- # @return [Proc] Proc to evaluate decorator class. Proc should return a decorator class.
14
- def decorate_when(&block)
15
11
  @decorator_evaluator = block
16
12
  end
17
13
 
18
14
  # Pass additional data to the decorator context (if supported).
19
15
  # @yield [object] Gives the underlying object to the block.
20
16
  # @return [Proc] Proc to evaluate decorator context. Proc should return Hash.
21
- def decorator_context(&block)
22
- @decorator_context_evaluator = block
17
+ def decorate_metadata
18
+ @decorator_metadata ||= GraphQL::Decorate::Metadata.new
19
+ yield(@decorator_metadata)
23
20
  end
24
21
 
25
22
  # @return [Class, nil] Gets the currently set decorator class.
26
- def decorator_class
27
- @decorator_class
28
- end
23
+ attr_reader :decorator_class
29
24
 
30
25
  # @return [Proc, nil] Gets the currently set decorator evaluator.
31
- def decorator_evaluator
32
- @decorator_evaluator
33
- end
26
+ attr_reader :decorator_evaluator
34
27
 
35
- # @return [Proc, nil] Gets the currently set decorator context evaluator.
36
- def decorator_context_evaluator
37
- @decorator_context_evaluator
38
- end
28
+ attr_reader :decorator_metadata
39
29
  end
40
30
  end
41
31
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GraphQL
3
4
  module Decorate
4
5
  # Extracts configured decorator attributes from a GraphQL::Schema::Object type.
@@ -11,6 +12,11 @@ module GraphQL
11
12
  @type = type
12
13
  end
13
14
 
15
+ # @return [Boolean] True if the type can be decorated, false otherwise
16
+ def decoratable?
17
+ !!(decorator_class || decorator_evaluator || unresolved_type?)
18
+ end
19
+
14
20
  # @return [Class, nil] Decorator class for the type if available
15
21
  def decorator_class
16
22
  get_attribute(:decorator_class)
@@ -21,9 +27,9 @@ module GraphQL
21
27
  get_attribute(:decorator_evaluator)
22
28
  end
23
29
 
24
- # @return [Proc, nil] Decorator context evaluator for the type if available
25
- def decorator_context_evaluator
26
- get_attribute(:decorator_context_evaluator)
30
+ # @return [Proc, nil] Decorator metadata evaluator for the type if available
31
+ def decorator_metadata
32
+ get_attribute(:decorator_metadata)
27
33
  end
28
34
 
29
35
  # @return [GraphQL::Schema::Object, nil] Decorator evaluator for the type if available
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Decorate
5
+ # Wraps current value, parents, and graphql_context and extracts relevant decoration data to resolve the field.
6
+ class UndecoratedField
7
+ # @return [Object] Value to be decorated
8
+ attr_reader :value
9
+
10
+ # @param value [Object] Value to be decorated
11
+ # @param type [GraphQL::Schema::Object] Type class of value to be decorated
12
+ # @param parent_value [Object] Value of the parent field
13
+ # @param parent_type [GraphQL::Schema::Object] Type class of the parent field
14
+ # @param graphql_context [GraphQL::Query::Context] Current query graphql_context
15
+ def initialize(value, type, parent_value, parent_type, graphql_context)
16
+ @value = value
17
+ @type_attributes = GraphQL::Decorate::TypeAttributes.new(type)
18
+ @parent_value = parent_value
19
+ @parent_type = parent_type
20
+ @graphql_context = graphql_context
21
+ @default_metadata = { graphql: true }
22
+ end
23
+
24
+ # @return [Class] Decorator class for the current field
25
+ def decorator_class
26
+ resolved_class = type_attributes.decorator_class || resolve_decorator_class
27
+ return resolved_class if resolved_class
28
+
29
+ class_evaluator = type_attributes.decorator_evaluator || resolve_decorator_evaluator
30
+ class_evaluator&.call(value, graphql_context)
31
+ end
32
+
33
+ # @return [Hash] Metadata to be provided to a decorator for the current field
34
+ def metadata
35
+ default_metadata.merge(unscoped_metadata, scoped_metadata)
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :type_attributes, :graphql_context, :parent_value, :parent_type, :default_metadata
41
+
42
+ def unscoped_metadata
43
+ unscoped_metadata_proc&.call(value, graphql_context) || {}
44
+ end
45
+
46
+ def scoped_metadata
47
+ merged_scoped_metadata = existing_scoped_metadata.merge(new_scoped_metadata)
48
+ graphql_context.scoped_set!(:scoped_decorator_metadata, merged_scoped_metadata)
49
+ merged_scoped_metadata
50
+ end
51
+
52
+ def new_scoped_metadata
53
+ scoped_metadata = scoped_metadata_proc&.call(value, graphql_context) || {}
54
+ parent_scoped_metadata.merge(scoped_metadata)
55
+ end
56
+
57
+ def parent_scoped_metadata
58
+ parent_type_attributes.decorator_metadata&.scoped_proc&.call(parent_value, graphql_context) || {}
59
+ end
60
+
61
+ def parent_type_attributes
62
+ GraphQL::Decorate::TypeAttributes.new(parent_type)
63
+ end
64
+
65
+ def existing_scoped_metadata
66
+ graphql_context[:scoped_decorator_metadata] || {}
67
+ end
68
+
69
+ def unscoped_metadata_proc
70
+ type_attributes.decorator_metadata&.unscoped_proc || resolve_unscoped_proc
71
+ end
72
+
73
+ def scoped_metadata_proc
74
+ type_attributes.decorator_metadata&.scoped_proc || resolve_scoped_proc
75
+ end
76
+
77
+ def resolve_decorator_class
78
+ resolved_type_attributes&.decorator_class
79
+ end
80
+
81
+ def resolve_decorator_evaluator
82
+ resolved_type_attributes&.decorator_evaluator
83
+ end
84
+
85
+ def resolve_unscoped_proc
86
+ resolved_type_attributes&.decorator_metadata&.unscoped_proc
87
+ end
88
+
89
+ def resolve_scoped_proc
90
+ resolved_type_attributes&.decorator_metadata&.scoped_proc
91
+ end
92
+
93
+ def resolved_type_attributes
94
+ @resolved_type_attributes ||= begin
95
+ if type_attributes.unresolved_type?
96
+ GraphQL::Decorate::TypeAttributes.new(type_attributes.type.resolve_type(value, graphql_context))
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module GraphQL
2
4
  module Decorate
3
5
  # Current version number
4
- VERSION = "0.2.1"
6
+ VERSION = '1.0.0'
5
7
  end
6
8
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql-decorate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Brook
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-02-27 00:00:00.000000000 Z
11
+ date: 2021-03-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -72,6 +72,62 @@ dependencies:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
74
  version: '3.0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rubocop
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 1.11.0
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 1.11.0
89
+ - !ruby/object:Gem::Dependency
90
+ name: rubocop-rspec
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - '='
94
+ - !ruby/object:Gem::Version
95
+ version: 2.2.0
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - '='
101
+ - !ruby/object:Gem::Version
102
+ version: 2.2.0
103
+ - !ruby/object:Gem::Dependency
104
+ name: simplecov
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: 0.21.2
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: 0.21.2
117
+ - !ruby/object:Gem::Dependency
118
+ name: yard
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: 0.9.26
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: 0.9.26
75
131
  description:
76
132
  email:
77
133
  - bbrook154@gmail.com
@@ -80,8 +136,9 @@ extensions: []
80
136
  extra_rdoc_files: []
81
137
  files:
82
138
  - ".github/workflows/gem-push-on-release.yml"
83
- - ".github/workflows/rspec.yml"
139
+ - ".github/workflows/ruby.yml"
84
140
  - ".gitignore"
141
+ - ".rubocop.yml"
85
142
  - Gemfile
86
143
  - LICENSE.txt
87
144
  - README.md
@@ -91,13 +148,14 @@ files:
91
148
  - graphql-decorate.gemspec
92
149
  - lib/graphql/decorate.rb
93
150
  - lib/graphql/decorate/configuration.rb
94
- - lib/graphql/decorate/connection.rb
95
- - lib/graphql/decorate/field_context.rb
151
+ - lib/graphql/decorate/connection_wrapper.rb
152
+ - lib/graphql/decorate/decoration.rb
96
153
  - lib/graphql/decorate/field_extension.rb
97
154
  - lib/graphql/decorate/field_integration.rb
98
- - lib/graphql/decorate/object.rb
155
+ - lib/graphql/decorate/metadata.rb
99
156
  - lib/graphql/decorate/object_integration.rb
100
157
  - lib/graphql/decorate/type_attributes.rb
158
+ - lib/graphql/decorate/undecorated_field.rb
101
159
  - lib/graphql/decorate/version.rb
102
160
  homepage: https://www.github.com/TrueCar/graphql-decorate
103
161
  licenses:
@@ -111,7 +169,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
111
169
  requirements:
112
170
  - - ">="
113
171
  - !ruby/object:Gem::Version
114
- version: '0'
172
+ version: 2.6.0
115
173
  required_rubygems_version: !ruby/object:Gem::Requirement
116
174
  requirements:
117
175
  - - ">="
@@ -1,32 +0,0 @@
1
- # This workflow uses actions that are not certified by GitHub.
2
- # They are provided by a third-party and are governed by
3
- # separate terms of service, privacy policy, and support
4
- # documentation.
5
- # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
- # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
-
8
- name: Ruby
9
-
10
- on:
11
- push:
12
- branches: [ master ]
13
- pull_request:
14
- branches: [ master ]
15
-
16
- jobs:
17
- test:
18
-
19
- runs-on: ubuntu-latest
20
- strategy:
21
- matrix:
22
- ruby-version: ['2.6', '2.7', '3.0']
23
-
24
- steps:
25
- - uses: actions/checkout@v2
26
- - name: Set up Ruby
27
- uses: ruby/setup-ruby@v1
28
- with:
29
- ruby-version: ${{ matrix.ruby-version }}
30
- bundler-cache: true # runs 'bundle install' and caches installed gems automatically
31
- - name: Run tests
32
- run: bundle exec rake
@@ -1,52 +0,0 @@
1
- # frozen_string_literal: true
2
- module GraphQL
3
- module Decorate
4
- # Wraps a GraphQL::Pagination::Connection object to decorate values after pagination is applied.
5
- class Connection
6
- # @return [GraphQL::Pagination::Connection] Connection being decorated
7
- attr_reader :connection
8
-
9
- # @return [GraphQL::Decorate::FieldContext] Current field context
10
- attr_reader :field_context
11
-
12
- def initialize(connection, field_context)
13
- @connection = connection
14
- @field_context = field_context
15
- end
16
-
17
- # @return [Array] Decorated nodes after pagination is applied
18
- def nodes
19
- nodes = @connection.nodes
20
- nodes.map { |node| GraphQL::Decorate::Object.new(node, field_context).decorate }
21
- end
22
-
23
- # @see nodes
24
- # @return [Array] Decorated nodes after pagination is applied
25
- def edge_nodes
26
- nodes
27
- end
28
-
29
- class << self
30
- private
31
-
32
- def method_missing(symbol, *args, &block)
33
- @connection.class.send(symbol, *args, &block)
34
- end
35
-
36
- def respond_to_missing?(method, include_private = false)
37
- @connection.class.respond_to_missing(method, include_private)
38
- end
39
- end
40
-
41
- private
42
-
43
- def method_missing(symbol, *args, &block)
44
- @connection.send(symbol, *args, &block)
45
- end
46
-
47
- def respond_to_missing?(method, include_private = false)
48
- @connection.respond_to_missing(method, include_private)
49
- end
50
- end
51
- end
52
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
- module GraphQL
3
- module Decorate
4
- # Wraps current GraphQL::Query::Context and options provided to a field for portability.
5
- class FieldContext
6
- # @return [GraphQL::Query::Context] Current GraphQL query context
7
- attr_reader :context
8
-
9
- # @return [Hash] Options provided to the field being decorated
10
- attr_reader :options
11
-
12
- def initialize(context, options)
13
- @context = context
14
- @options = options
15
- end
16
- end
17
- end
18
- end
@@ -1,71 +0,0 @@
1
- # frozen_string_literal: true
2
- module GraphQL
3
- module Decorate
4
- # Handles decorating an object given its current field context.
5
- class Object
6
- # @param object [Object] Object being decorated.
7
- # @param field_context [GraphQL::Decorate::FieldContext] Current GraphQL field context and options.
8
- def initialize(object, field_context)
9
- @object = object
10
- @field_context = field_context
11
- @default_decorator_context = { graphql: true }
12
- end
13
-
14
- # Resolve the object with decoration.
15
- # @return [Object] Decorated object if possible, otherwise the original object.
16
- def decorate
17
- if decorator_class
18
- GraphQL::Decorate.configuration.evaluate_decorator.call(decorator_class, object, decorator_context)
19
- else
20
- object
21
- end
22
- end
23
-
24
- private
25
-
26
- attr_reader :object, :field_context, :default_decorator_context
27
-
28
- def decorator_class
29
- if field_context.options[:decorator_class]
30
- field_context.options[:decorator_class]
31
- elsif field_context.options[:decorator_evaluator]
32
- field_context.options[:decorator_evaluator].call(object)
33
- else
34
- resolve_decorator_class
35
- end
36
- end
37
-
38
- def decorator_context_evaluator
39
- field_context.options[:decorator_context_evaluator] || resolve_decorator_context_evaluator
40
- end
41
-
42
- private
43
-
44
- def evaluate_decoration_context
45
- decorator_context_evaluator ? decorator_context_evaluator.call(object) : {}
46
- end
47
-
48
- def decorator_context
49
- evaluate_decoration_context.merge(default_decorator_context)
50
- end
51
-
52
- def resolve_decorator_class
53
- type = resolve_type
54
- if type.respond_to?(:decorator_class) && type.decorator_class
55
- type.decorator_class
56
- end
57
- end
58
-
59
- def resolve_decorator_context_evaluator
60
- type = resolve_type
61
- if type.respond_to?(:decorator_context_evaluator) && type.decorator_context_evaluator
62
- type.decorator_context_evaluator
63
- end
64
- end
65
-
66
- def resolve_type
67
- field_context.options[:unresolved_type]&.resolve_type(object, field_context.context)
68
- end
69
- end
70
- end
71
- end