graphql-decorate 0.2.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
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