graphql-decorate 0.2.1 → 1.0.2

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: ac0949a542979b4218036dae55013d1b148cda15cfafdab0d741aa23d882d289
4
+ data.tar.gz: ac03a2c2c2d554fadd4a7b1ad489d572954c84d7efb77f8afe301c3e8f5909ab
5
5
  SHA512:
6
- metadata.gz: 457e1cd6d2c62e5f463ccfacd8fa62f3e444c4b980d5e1d0b24b7b59aa960bcbf3c38cb0013055c793bafb3af86a5c0c2a68ae687b0c6af66de0b5b24b96d811
7
- data.tar.gz: 678fb9bd85a0de6e6f0d336f2e812f91e1a326c7e3bff426159ff2bf2a83af5bc93bdf1b4ff490650f3a851f8d42b0370955d552bf57d22a47f183e8c2335462
6
+ metadata.gz: 4646f9ea8f791e8ae56163ee8f1ff67849a697365eed6fc5ac9d3ef4327933d6d650cb2aef6cf8160ebea76826de0152d13c9c0b5719da5bdb02e012d9371587
7
+ data.tar.gz: 02e3384f30868f20d490db4dc3481c77bc94f1ce1e3ad446c15a9bb63a3825fd98d7712341452ed532e4a794379503a0e9d4db518080c521d980579337373fd3
@@ -0,0 +1,38 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+ pull_request:
7
+ branches: [ master ]
8
+
9
+ jobs:
10
+ ci:
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: Check documentation completion
32
+ run: |
33
+ completion_percentage=$(bundle exec yard | tee /dev/stdout | tail -1 | cut -d'%' -f1 | xargs)
34
+ echo $completion_percentage
35
+ if [[ $completion_percentage != "100.00" ]]; then
36
+ echo "YARD documentation must be at 100%"
37
+ exit 2;
38
+ fi
@@ -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: |
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,9 @@
1
+ [![Gem Version](https://badge.fury.io/rb/graphql-decorate.svg)](https://badge.fury.io/rb/graphql-decorate)
2
+ ![CI](https://github.com/TrueCar/graphql-decorate/actions/workflows/ci.yml/badge.svg)
3
+
1
4
  # GraphQL Decorate
2
5
 
3
- `graphql-decorate` adds an easy-to-use interface for decorating types in `graphql-ruby`. It lets
6
+ `graphql-decorate` adds an easy-to-use interface for decorating types in [`graphql-ruby`](https://github.com/rmosolgo/graphql-ruby). It lets
4
7
  you move logic out of your type files and keep them declarative.
5
8
 
6
9
  ## Installation
@@ -19,16 +22,19 @@ Or install it yourself as:
19
22
 
20
23
  $ gem install graphql-decorate
21
24
 
22
- Once the gem is installed, you need to add the integrations to your base type and field classes.
25
+ Once the gem is installed, you need to add the plugin to your schema and the integration into
26
+ your base object class.
23
27
  ```ruby
24
- class BaseType < GraphQL::Schema::Object
25
- extend GraphQL::Decorate::ObjectIntegration
28
+ class Schema < GraphQL::Schema
29
+ use GraphQL::Decorate
26
30
  end
27
31
 
28
- class BaseField < GraphQL::Schema::Field
29
- include GraphQL::Decorate::FieldIntegration
32
+ class BaseObject < GraphQL::Schema::Object
33
+ include GraphQL::Decorate::ObjectIntegration
30
34
  end
31
35
  ```
36
+ Note that `use GraphQL::Decorate` must be included in the schema _after_ `query` and `mutation`
37
+ so that the fields to be extended are initialized first.
32
38
 
33
39
  ## Usage
34
40
 
@@ -48,7 +54,7 @@ class RectangleDecorator < BaseDecorator
48
54
  end
49
55
  end
50
56
 
51
- class Rectangle < GraphQL::Schema::Object
57
+ class RectangleType < BaseObject
52
58
  decorate_with RectangleDecorator
53
59
 
54
60
  field :area, Int, null: false
@@ -59,24 +65,27 @@ In this example, the `Rectangle` type is being decorated with a `RectangleDecora
59
65
  `RectangleDecorator`. All of the methods on the decorator are accessible on the type.
60
66
 
61
67
  ### Decorators
62
- By default, `graphql-decorate` is set up to work with Draper-style decorators. These decorators
68
+ By default, `graphql-decorate` is set up to work with [`draper`](https://github.com/drapergem/draper) style decorators. These decorators
63
69
  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.
70
+ decorator. They can also take in additional metadata.
65
71
  ```ruby
66
- RectangleDecorator.decorate(rectangle, context)
72
+ RectangleDecorator.decorate(rectangle, context: metadata)
67
73
  ```
68
74
  If you are using a different decorator pattern then you can override this default behavior in
69
75
  the configuration.
70
76
  ```ruby
71
77
  GraphQL::Decorate.configure do |config|
72
- config.decorate do |decorator_class, object, _context|
78
+ config.decorate do |decorator_class, object, _metadata|
73
79
  decorator_class.decorate_differently(object)
74
80
  end
75
81
  end
76
82
  ```
77
83
 
78
84
  ### Types
79
- Three methods are made available on your type classes
85
+ Two methods are made available on your type classes: `decorate_with` and `decorate_metadata`.
86
+ Every method that yields the underlying object will also yield the current GraphQL `context`.
87
+ If decoration depends on some context in the current query then you can access it when the field is resolved.
88
+
80
89
  #### decorate_with
81
90
  `decorate_with` accepts a decorator class that will decorate every instance of your type.
82
91
  ```ruby
@@ -85,12 +94,11 @@ class Rectangle < GraphQL::Schema::Object
85
94
  end
86
95
  ```
87
96
 
88
- #### decorate_when
89
- `decorate_when` accepts a block which yields the underlying object. If you have multiple
97
+ `decorate_with` optionally accepts a block which yields the underlying object. If you have multiple
90
98
  possible decorator classes you can return the one intended for the underling object.
91
99
  ```ruby
92
100
  class Rectangle < GraphQL::Schema::Object
93
- decorate_when do |object|
101
+ decorate_with do |object, _graphql_context|
94
102
  if object.length == object.width
95
103
  SquareDecorator
96
104
  else
@@ -100,30 +108,50 @@ class Rectangle < GraphQL::Schema::Object
100
108
  end
101
109
  ```
102
110
 
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.
111
+ #### decorate_metadata
112
+ If your decorator pattern allows additional metadata to be passed into the decorators, you can
113
+ define it here. By default every metadata hash will contain `{ graphql: true }`. This is
114
+ useful if your decorator logic needs to diverge when used in a GraphQL context. Ideally your
115
+ decorators are agnostic to where they are being used, but it is available if needed.
116
+
117
+ `decorate_metadata` yields a `GraphQL::Decorate::Metadata` metadata instance. It responds to two
118
+ methods: `unscoped` and `scoped`. `unscoped` sets metadata for a resolved field. `scoped` sets
119
+ metadata for a resolved field and all of its child fields. `unscoped` and `scoped` are expected
120
+ to return `Hash`s.
121
+
106
122
  ```ruby
107
123
  class Rectangle < GraphQL::Schema::Object
108
- decorator_context do |object|
109
- {
110
- name: object.name
111
- }
124
+ decorate_metadata do |metadata|
125
+ metadata.unscoped do |object, _graphql_context|
126
+ {
127
+ name: object.name
128
+ }
129
+ end
130
+
131
+ metadata.scoped do |object, _graphql_context|
132
+ {
133
+ inside_rectangle: true
134
+ }
135
+ end
112
136
  end
113
137
  end
114
138
  ```
115
- `RectangleDecorator` will be initialized with a context of `{ name: <object_name> }`.
139
+ `RectangleDecorator` will be initialized with metadata `{ name: <object_name>,
140
+ inside_rectangle: true, graphql: true }`. All child fields of `Rectangle` will be initialized
141
+ with metadata `{ inside_rectangle: true, graphql: true }`.
116
142
 
117
143
  #### 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.
144
+ You can mix and match these methods to suit your needs. Note that if `unscoped` and
145
+ `scoped` are both provided for metadata that `scoped` will override any shared keys.
120
146
  ```ruby
121
147
  class Rectangle < GraphQL::Schema::Object
122
148
  decorate_with RectangleDecorator
123
- decorator_context do |object|
124
- {
125
- name: object.name
126
- }
149
+ decorate_metadata do |metadata|
150
+ metadata.scoped do |object, _graphql_context|
151
+ {
152
+ name: object.name
153
+ }
154
+ end
127
155
  end
128
156
  end
129
157
  ```
@@ -141,8 +169,11 @@ end
141
169
 
142
170
  ## Development
143
171
 
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.
172
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to
173
+ run the tests. You can also run `bin/console` for an interactive prompt that will allow you to
174
+ experiment.
145
175
 
146
176
  ## License
147
177
 
148
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
178
+ The gem is available as open source under the terms of the
179
+ [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,45 @@
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
 
7
+ # rubocop:disable Metrics/BlockLength
6
8
  Gem::Specification.new do |spec|
7
- spec.name = "graphql-decorate"
8
- spec.version = GraphQL::Decorate::VERSION
9
- spec.authors = ["Ben Brook"]
10
- spec.email = ["bbrook154@gmail.com"]
9
+ spec.name = 'graphql-decorate'
10
+ spec.version = GraphQL::Decorate::VERSION
11
+ spec.authors = ['Ben Brook']
12
+ spec.email = ['bbrook154@gmail.com']
11
13
 
12
- spec.summary = 'A decorator integration for the GraphQL gem'
13
- spec.homepage = 'https://www.github.com/TrueCar/graphql-decorate'
14
- spec.license = "MIT"
14
+ spec.summary = 'A decorator integration for the GraphQL gem'
15
+ spec.homepage = 'https://www.github.com/TrueCar/graphql-decorate'
16
+ spec.license = 'MIT'
17
+ spec.metadata = {
18
+ 'rubygems_mfa_required' => 'true'
19
+ }
15
20
 
16
21
  # 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)/}) }
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added
23
+ # into git.
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ `git ls-files -z`.split("\x0").reject do |f|
26
+ f.match(%r{^(test|spec|features)/})
27
+ end
20
28
  end
21
- spec.bindir = "exe"
29
+ spec.bindir = 'exe'
22
30
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
- spec.require_paths = ["lib"]
31
+ spec.require_paths = ['lib']
24
32
 
25
- spec.add_runtime_dependency "graphql", ">= 1.3", "< 2"
33
+ spec.required_ruby_version = '>= 2.6.0'
26
34
 
27
- spec.add_development_dependency "bundler", ">= 2"
28
- spec.add_development_dependency "rake", "~> 10.0"
29
- spec.add_development_dependency "rspec", "~> 3.0"
35
+ spec.add_runtime_dependency 'graphql', '>= 1.3', '< 2'
36
+
37
+ spec.add_development_dependency 'bundler', '>= 2'
38
+ spec.add_development_dependency 'rake', '>= 12.3.3'
39
+ spec.add_development_dependency 'rspec', '~> 3.0'
40
+ spec.add_development_dependency 'rubocop', ' >= 1.11.0 '
41
+ spec.add_development_dependency 'rubocop-rspec', '2.2.0'
42
+ spec.add_development_dependency 'simplecov', '~> 0.21.2'
43
+ spec.add_development_dependency 'yard', '~> 0.9.26'
30
44
  end
45
+ # rubocop:enable Metrics/BlockLength
@@ -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,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
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Allows extraction of a type class from a particular field.
4
+ module ExtractType
5
+ private
6
+
7
+ def extract_type(field)
8
+ if field.respond_to?(:of_type)
9
+ extract_type(field.of_type)
10
+ else
11
+ field
12
+ end
13
+ end
14
+ end
@@ -1,36 +1,50 @@
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
7
+ include ExtractType
8
+
6
9
  # Extension to be called after lazy loading.
7
10
  # @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.
11
+ # @param value [Object, Array, GraphQL::Schema::Object] The object being decorated. Can
12
+ # be a schema object if the field hasn't been resolved yet or a connection.
13
+ # @return [Object] Decorated object.
10
14
  def after_resolve(context:, value:, **_rest)
11
15
  return if value.nil?
12
16
 
13
- field_context = GraphQL::Decorate::FieldContext.new(context, options)
14
- 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) }
17
+ resolve_decorated_value(value, context)
18
+ end
19
+
20
+ private
21
+
22
+ def resolve_decorated_value(value, context)
23
+ type = extract_type(context.to_h[:current_field].type)
24
+
25
+ if collection?(value)
26
+ value.each_with_index.map do |item, index|
27
+ decorate(item, type, context, index)
28
+ end
18
29
  else
19
- decorate(value, field_context)
30
+ decorate(value, type, context)
20
31
  end
21
32
  end
22
33
 
23
- private
34
+ def decorate(value, type, context, index = nil)
35
+ undecorated_field = GraphQL::Decorate::UndecoratedField.new(value, type, context, index)
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
27
45
  klasses << ::ActiveRecord::Relation if defined?(ActiveRecord::Relation)
28
46
  klasses
29
47
  end
30
-
31
- def decorate(object, field_context)
32
- GraphQL::Decorate::Object.new(object, field_context).decorate
33
- end
34
48
  end
35
49
  end
36
50
  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,37 @@
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
7
+ # @param base [Class] Base class the module is being included in
8
+ # @return [nil]
9
+ def self.included(base)
10
+ base.extend(self)
11
+ end
12
+
5
13
  # Decorate the type with a decorator class.
6
14
  # @param klass [Class] Class the object should be decorated with.
7
- def decorate_with(klass)
15
+ def decorate_with(klass = nil, &block)
8
16
  @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
17
  @decorator_evaluator = block
16
18
  end
17
19
 
18
20
  # Pass additional data to the decorator context (if supported).
19
21
  # @yield [object] Gives the underlying object to the block.
20
22
  # @return [Proc] Proc to evaluate decorator context. Proc should return Hash.
21
- def decorator_context(&block)
22
- @decorator_context_evaluator = block
23
+ def decorate_metadata
24
+ @decorator_metadata ||= GraphQL::Decorate::Metadata.new
25
+ yield(@decorator_metadata)
23
26
  end
24
27
 
25
28
  # @return [Class, nil] Gets the currently set decorator class.
26
- def decorator_class
27
- @decorator_class
28
- end
29
+ attr_reader :decorator_class
29
30
 
30
31
  # @return [Proc, nil] Gets the currently set decorator evaluator.
31
- def decorator_evaluator
32
- @decorator_evaluator
33
- end
32
+ attr_reader :decorator_evaluator
34
33
 
35
- # @return [Proc, nil] Gets the currently set decorator context evaluator.
36
- def decorator_context_evaluator
37
- @decorator_context_evaluator
38
- end
34
+ attr_reader :decorator_metadata
39
35
  end
40
36
  end
41
37
  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
@@ -33,7 +39,7 @@ module GraphQL
33
39
 
34
40
  # @return [Boolean] True if type is not yet resolved, false if it is resolved
35
41
  def unresolved_type?
36
- type.respond_to?(:resolve_type)
42
+ type.respond_to?(:kind) && [GraphQL::TypeKinds::INTERFACE, GraphQL::TypeKinds::UNION].include?(type.kind)
37
43
  end
38
44
 
39
45
  # @return [Boolean] True if type is resolved, false if it is not resolved
@@ -41,19 +47,10 @@ module GraphQL
41
47
  !unresolved_type?
42
48
  end
43
49
 
44
- # @return [Boolean] True if type is a connection, false if it is resolved
45
- def connection?
46
- resolved_type? && type.respond_to?(:node_type)
47
- end
48
-
49
50
  private
50
51
 
51
52
  def get_attribute(name)
52
- if connection?
53
- type.node_type.respond_to?(name) && type.node_type.public_send(name)
54
- elsif resolved_type?
55
- type.respond_to?(name) ? type.public_send(name) : nil
56
- end
53
+ type.respond_to?(name) ? type.public_send(name) : nil
57
54
  end
58
55
  end
59
56
  end
@@ -0,0 +1,114 @@
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 graphql_context [GraphQL::Query::Context] Current query graphql_context
13
+ def initialize(value, type, graphql_context, index = nil)
14
+ @value = value
15
+ @type = type
16
+ @type_attributes = GraphQL::Decorate::TypeAttributes.new(type)
17
+ @graphql_context = graphql_context
18
+ @default_metadata = { graphql: true }
19
+ @path = graphql_context[:current_path].dup
20
+ @path << index if index
21
+ end
22
+
23
+ # @return [Class] Decorator class for the current field
24
+ def decorator_class
25
+ resolved_class = type_attributes.decorator_class || resolve_decorator_class
26
+ return resolved_class if resolved_class
27
+
28
+ class_evaluator = type_attributes.decorator_evaluator || resolve_decorator_evaluator
29
+ class_evaluator&.call(value, graphql_context)
30
+ end
31
+
32
+ # @return [Hash] Metadata to be provided to a decorator for the current field
33
+ def metadata
34
+ default_metadata.merge(unscoped_metadata, scoped_metadata)
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :type, :type_attributes, :graphql_context, :default_metadata, :path
40
+
41
+ def unscoped_metadata
42
+ unscoped_metadata_proc&.call(value, graphql_context) || {}
43
+ end
44
+
45
+ def scoped_metadata
46
+ insert_scoped_metadata(new_scoped_metadata)
47
+ end
48
+
49
+ def new_scoped_metadata
50
+ scoped_metadata_proc&.call(value, graphql_context) || {}
51
+ end
52
+
53
+ # rubocop:disable Metrics/AbcSize
54
+ def insert_scoped_metadata(metadata)
55
+ # Save metadata at each level in the path of the current execution.
56
+ # If a field's direct parent does not have metadata then it will
57
+ # use the next highest metadata in the tree that matches its path.
58
+ scoped_metadata = graphql_context[:scoped_decorator_metadata] ||= {}
59
+ prev_value = {}
60
+
61
+ path[0...-1].each do |step|
62
+ # Write the parent's metadata to the child if it doesn't already exist
63
+ scoped_metadata[step] = { value: prev_value, children: {} } unless scoped_metadata[step]
64
+ # Update the next parent's metadata to include anything at the current level
65
+ prev_value = prev_value.merge(scoped_metadata[step][:value])
66
+ # Move to the child fields and repeat
67
+ scoped_metadata = scoped_metadata[step][:children]
68
+ end
69
+
70
+ # The last step in the path is the current field, merge in new metadata from
71
+ # the field itself and return it.
72
+ merged_metadata = { value: prev_value.merge(metadata), children: {} }
73
+ scoped_metadata[path[-1]] = merged_metadata
74
+ merged_metadata[:value]
75
+ end
76
+ # rubocop:enable Metrics/AbcSize
77
+
78
+ def unscoped_metadata_proc
79
+ type_attributes.decorator_metadata&.unscoped_proc || resolve_unscoped_proc
80
+ end
81
+
82
+ def scoped_metadata_proc
83
+ type_attributes.decorator_metadata&.scoped_proc || resolve_scoped_proc
84
+ end
85
+
86
+ def resolve_decorator_class
87
+ resolved_type_attributes&.decorator_class
88
+ end
89
+
90
+ def resolve_decorator_evaluator
91
+ resolved_type_attributes&.decorator_evaluator
92
+ end
93
+
94
+ def resolve_unscoped_proc
95
+ resolved_type_attributes&.decorator_metadata&.unscoped_proc
96
+ end
97
+
98
+ def resolve_scoped_proc
99
+ resolved_type_attributes&.decorator_metadata&.scoped_proc
100
+ end
101
+
102
+ def resolved_type_attributes
103
+ @resolved_type_attributes ||= if type_attributes.unresolved_type?
104
+ if type.respond_to?(:resolve_type)
105
+ GraphQL::Decorate::TypeAttributes.new(type.resolve_type(value,
106
+ graphql_context))
107
+ else
108
+ graphql_context.schema.resolve_type(type, value, graphql_context)
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ 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.2'
5
7
  end
6
8
  end
@@ -1,18 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'graphql'
2
4
  require_relative 'decorate/version'
3
5
  require_relative 'decorate/configuration'
6
+ require_relative 'decorate/extract_type'
4
7
  require_relative 'decorate/object_integration'
5
- 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/metadata'
11
13
 
12
14
  # Matching the graphql-ruby namespace
13
15
  module GraphQL
14
16
  # Entry point for graphql-decorate. Handles configuration.
15
17
  module Decorate
18
+ extend ExtractType
19
+
16
20
  # @return [Configuration] Returns a new instance of GraphQL::Decorate::Configuration.
17
21
  def self.configuration
18
22
  @configuration ||= Configuration.new
@@ -27,5 +31,19 @@ module GraphQL
27
31
  def self.reset_configuration!
28
32
  @configuration = Configuration.new
29
33
  end
34
+
35
+ # @param schema_defn [GraphQL::Schema] Current schema class
36
+ # @return [nil]
37
+ def self.use(schema_defn)
38
+ schema_defn.types.each do |_name, type|
39
+ next unless type.respond_to?(:fields)
40
+
41
+ type.fields.each do |_name, field|
42
+ field_type = extract_type(field.type_class.type)
43
+ type_attributes = GraphQL::Decorate::TypeAttributes.new(field_type)
44
+ field.type_class.extension(GraphQL::Decorate::FieldExtension) if type_attributes.decoratable?
45
+ end
46
+ end
47
+ end
30
48
  end
31
49
  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.2
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: 2022-04-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -48,16 +48,16 @@ dependencies:
48
48
  name: rake
49
49
  requirement: !ruby/object:Gem::Requirement
50
50
  requirements:
51
- - - "~>"
51
+ - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: '10.0'
53
+ version: 12.3.3
54
54
  type: :development
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
57
57
  requirements:
58
- - - "~>"
58
+ - - ">="
59
59
  - !ruby/object:Gem::Version
60
- version: '10.0'
60
+ version: 12.3.3
61
61
  - !ruby/object:Gem::Dependency
62
62
  name: rspec
63
63
  requirement: !ruby/object:Gem::Requirement
@@ -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
@@ -79,9 +135,10 @@ executables: []
79
135
  extensions: []
80
136
  extra_rdoc_files: []
81
137
  files:
138
+ - ".github/workflows/ci.yml"
82
139
  - ".github/workflows/gem-push-on-release.yml"
83
- - ".github/workflows/rspec.yml"
84
140
  - ".gitignore"
141
+ - ".rubocop.yml"
85
142
  - Gemfile
86
143
  - LICENSE.txt
87
144
  - README.md
@@ -91,18 +148,19 @@ 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/decoration.rb
152
+ - lib/graphql/decorate/extract_type.rb
96
153
  - lib/graphql/decorate/field_extension.rb
97
- - lib/graphql/decorate/field_integration.rb
98
- - lib/graphql/decorate/object.rb
154
+ - lib/graphql/decorate/metadata.rb
99
155
  - lib/graphql/decorate/object_integration.rb
100
156
  - lib/graphql/decorate/type_attributes.rb
157
+ - lib/graphql/decorate/undecorated_field.rb
101
158
  - lib/graphql/decorate/version.rb
102
159
  homepage: https://www.github.com/TrueCar/graphql-decorate
103
160
  licenses:
104
161
  - MIT
105
- metadata: {}
162
+ metadata:
163
+ rubygems_mfa_required: 'true'
106
164
  post_install_message:
107
165
  rdoc_options: []
108
166
  require_paths:
@@ -111,14 +169,14 @@ 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
  - - ">="
118
176
  - !ruby/object:Gem::Version
119
177
  version: '0'
120
178
  requirements: []
121
- rubygems_version: 3.0.3
179
+ rubygems_version: 3.0.3.1
122
180
  signing_key:
123
181
  specification_version: 4
124
182
  summary: A decorator integration for the GraphQL gem
@@ -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,38 +0,0 @@
1
- module GraphQL
2
- module Decorate
3
- # Extends default field behavior and adds extension to the field if it should be decorated.
4
- module FieldIntegration
5
- # Overridden field initializer
6
- # @param type [GraphQL::Schema::Object] The type to add the extension to.
7
- # @return [Void]
8
- def initialize(type:, **rest, &block)
9
- super
10
- 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
35
- end
36
- end
37
- end
38
- 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