graphql-decorate 0.2.1 → 1.0.0

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: 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