graphiti-rb 1.0.alpha.1

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.
Files changed (95) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +20 -0
  6. data/.yardopts +2 -0
  7. data/Appraisals +11 -0
  8. data/CODE_OF_CONDUCT.md +49 -0
  9. data/Gemfile +12 -0
  10. data/Guardfile +32 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +75 -0
  13. data/Rakefile +15 -0
  14. data/bin/appraisal +17 -0
  15. data/bin/console +14 -0
  16. data/bin/rspec +17 -0
  17. data/bin/setup +8 -0
  18. data/gemfiles/rails_4.gemfile +17 -0
  19. data/gemfiles/rails_5.gemfile +17 -0
  20. data/graphiti.gemspec +34 -0
  21. data/lib/generators/jsonapi/resource_generator.rb +169 -0
  22. data/lib/generators/jsonapi/templates/application_resource.rb.erb +15 -0
  23. data/lib/generators/jsonapi/templates/controller.rb.erb +61 -0
  24. data/lib/generators/jsonapi/templates/create_request_spec.rb.erb +30 -0
  25. data/lib/generators/jsonapi/templates/destroy_request_spec.rb.erb +20 -0
  26. data/lib/generators/jsonapi/templates/index_request_spec.rb.erb +22 -0
  27. data/lib/generators/jsonapi/templates/resource.rb.erb +11 -0
  28. data/lib/generators/jsonapi/templates/resource_reads_spec.rb.erb +62 -0
  29. data/lib/generators/jsonapi/templates/resource_writes_spec.rb.erb +63 -0
  30. data/lib/generators/jsonapi/templates/show_request_spec.rb.erb +21 -0
  31. data/lib/generators/jsonapi/templates/update_request_spec.rb.erb +34 -0
  32. data/lib/graphiti-rb.rb +1 -0
  33. data/lib/graphiti.rb +121 -0
  34. data/lib/graphiti/adapters/abstract.rb +516 -0
  35. data/lib/graphiti/adapters/active_record.rb +6 -0
  36. data/lib/graphiti/adapters/active_record/base.rb +249 -0
  37. data/lib/graphiti/adapters/active_record/belongs_to_sideload.rb +17 -0
  38. data/lib/graphiti/adapters/active_record/has_many_sideload.rb +17 -0
  39. data/lib/graphiti/adapters/active_record/has_one_sideload.rb +17 -0
  40. data/lib/graphiti/adapters/active_record/inferrence.rb +12 -0
  41. data/lib/graphiti/adapters/active_record/many_to_many_sideload.rb +30 -0
  42. data/lib/graphiti/adapters/null.rb +236 -0
  43. data/lib/graphiti/base.rb +70 -0
  44. data/lib/graphiti/configuration.rb +21 -0
  45. data/lib/graphiti/context.rb +16 -0
  46. data/lib/graphiti/deserializer.rb +208 -0
  47. data/lib/graphiti/errors.rb +309 -0
  48. data/lib/graphiti/extensions/boolean_attribute.rb +33 -0
  49. data/lib/graphiti/extensions/extra_attribute.rb +70 -0
  50. data/lib/graphiti/extensions/temp_id.rb +26 -0
  51. data/lib/graphiti/filter_operators.rb +25 -0
  52. data/lib/graphiti/hash_renderer.rb +57 -0
  53. data/lib/graphiti/jsonapi_serializable_ext.rb +50 -0
  54. data/lib/graphiti/query.rb +251 -0
  55. data/lib/graphiti/rails.rb +28 -0
  56. data/lib/graphiti/railtie.rb +74 -0
  57. data/lib/graphiti/renderer.rb +60 -0
  58. data/lib/graphiti/resource.rb +110 -0
  59. data/lib/graphiti/resource/configuration.rb +239 -0
  60. data/lib/graphiti/resource/dsl.rb +138 -0
  61. data/lib/graphiti/resource/interface.rb +32 -0
  62. data/lib/graphiti/resource/polymorphism.rb +68 -0
  63. data/lib/graphiti/resource/sideloading.rb +102 -0
  64. data/lib/graphiti/resource_proxy.rb +127 -0
  65. data/lib/graphiti/responders.rb +19 -0
  66. data/lib/graphiti/runner.rb +25 -0
  67. data/lib/graphiti/scope.rb +98 -0
  68. data/lib/graphiti/scoping/base.rb +99 -0
  69. data/lib/graphiti/scoping/default_filter.rb +58 -0
  70. data/lib/graphiti/scoping/extra_attributes.rb +29 -0
  71. data/lib/graphiti/scoping/filter.rb +93 -0
  72. data/lib/graphiti/scoping/filterable.rb +36 -0
  73. data/lib/graphiti/scoping/paginate.rb +87 -0
  74. data/lib/graphiti/scoping/sort.rb +64 -0
  75. data/lib/graphiti/sideload.rb +281 -0
  76. data/lib/graphiti/sideload/belongs_to.rb +34 -0
  77. data/lib/graphiti/sideload/has_many.rb +16 -0
  78. data/lib/graphiti/sideload/has_one.rb +9 -0
  79. data/lib/graphiti/sideload/many_to_many.rb +24 -0
  80. data/lib/graphiti/sideload/polymorphic_belongs_to.rb +108 -0
  81. data/lib/graphiti/stats/dsl.rb +89 -0
  82. data/lib/graphiti/stats/payload.rb +49 -0
  83. data/lib/graphiti/types.rb +172 -0
  84. data/lib/graphiti/util/attribute_check.rb +88 -0
  85. data/lib/graphiti/util/field_params.rb +16 -0
  86. data/lib/graphiti/util/hash.rb +51 -0
  87. data/lib/graphiti/util/hooks.rb +33 -0
  88. data/lib/graphiti/util/include_params.rb +39 -0
  89. data/lib/graphiti/util/persistence.rb +219 -0
  90. data/lib/graphiti/util/relationship_payload.rb +64 -0
  91. data/lib/graphiti/util/serializer_attributes.rb +97 -0
  92. data/lib/graphiti/util/sideload.rb +33 -0
  93. data/lib/graphiti/util/validation_response.rb +78 -0
  94. data/lib/graphiti/version.rb +3 -0
  95. metadata +317 -0
@@ -0,0 +1,88 @@
1
+ # Private, tested in resource specs
2
+ module Graphiti
3
+ module Util
4
+ class AttributeCheck
5
+ attr_reader :resource, :name, :flag, :request, :raise_error
6
+
7
+ def self.run(resource, name, flag, request, raise_error)
8
+ new(resource, name, flag, request, raise_error).run
9
+ end
10
+
11
+ def initialize(resource, name, flag, request, raise_error)
12
+ @resource = resource
13
+ @name = name.to_sym
14
+ @flag = flag
15
+ @request = request
16
+ @raise_error = raise_error
17
+ end
18
+
19
+ def run
20
+ if attribute?
21
+ if supported?
22
+ if guarded?
23
+ if guard_passes?
24
+ attribute
25
+ else
26
+ maybe_raise(request: true, guard: attribute[flag])
27
+ end
28
+ else
29
+ attribute
30
+ end
31
+ else
32
+ maybe_raise(exists: true)
33
+ end
34
+ else
35
+ maybe_raise(exists: false)
36
+ end
37
+ end
38
+
39
+ def maybe_raise(opts = {})
40
+ default = { request: request, exists: true }
41
+ opts = default.merge(opts)
42
+ if raise_error?(opts[:exists])
43
+ raise error_class.new(resource, name, flag, opts)
44
+ else
45
+ false
46
+ end
47
+ end
48
+
49
+ def guard_passes?
50
+ !!resource.send(attribute[flag])
51
+ end
52
+
53
+ def guarded?
54
+ request? &&
55
+ attribute[flag].is_a?(Symbol) &&
56
+ attribute[flag] != :required
57
+ end
58
+
59
+ def error_class
60
+ Errors::AttributeError
61
+ end
62
+
63
+ def supported?
64
+ attribute[flag] != false
65
+ end
66
+
67
+ def attribute
68
+ resource.all_attributes[name]
69
+ end
70
+
71
+ def attribute?
72
+ !!attribute
73
+ end
74
+
75
+ def raise_error?(exists)
76
+ if raise_error == :only_unsupported
77
+ exists ? true : false
78
+ else
79
+ !!raise_error
80
+ end
81
+ end
82
+
83
+ def request?
84
+ !!request
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,16 @@
1
+ module Graphiti
2
+ module Util
3
+ # @api private
4
+ class FieldParams
5
+ def self.parse(params)
6
+ return {} if params.nil?
7
+
8
+ {}.tap do |parsed|
9
+ params.each_pair do |key, value|
10
+ parsed[key.to_sym] = value.split(',').map(&:to_sym)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,51 @@
1
+ module Graphiti
2
+ module Util
3
+ # @api private
4
+ class Hash
5
+ # Grab all keys at any level of the hash.
6
+ #
7
+ # { foo: { bar: { baz: {} } } }
8
+ #
9
+ # Becomes
10
+ #
11
+ # [:foo, :bar, :bar]
12
+ #
13
+ # @param hash the hash we want to process
14
+ # @param [Array<Symbol, String>] collection the memoized collection of keys
15
+ # @return [Array<Symbol, String>] the keys
16
+ # @api private
17
+ def self.keys(hash, collection = [])
18
+ hash.each_pair do |key, value|
19
+ collection << key
20
+ keys(value, collection)
21
+ end
22
+
23
+ collection
24
+ end
25
+
26
+ # Like ActiveSupport's #deep_merge
27
+ # @return [Hash] the merged hash
28
+ # @api private
29
+ def self.deep_merge!(hash, other)
30
+ merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
31
+ hash.merge!(other, &merger)
32
+ end
33
+
34
+ # Like ActiveSupport's #deep_dup
35
+ # @api private
36
+ def self.deep_dup(hash)
37
+ if hash.respond_to?(:deep_dup)
38
+ hash.deep_dup
39
+ else
40
+ {}.tap do |duped|
41
+ hash.each_pair do |key, value|
42
+ value = deep_dup(value) if value.is_a?(Hash)
43
+ value = value.dup if value && value.respond_to?(:dup) && ![Symbol, Fixnum].include?(value.class)
44
+ duped[key] = value
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,33 @@
1
+ module Graphiti
2
+ module Util
3
+ class Hooks
4
+ def self.record
5
+ self.hooks = []
6
+ begin
7
+ yield.tap { run }
8
+ ensure
9
+ self.hooks = []
10
+ end
11
+ end
12
+
13
+ def self._hooks
14
+ Thread.current[:_compliable_hooks] ||= []
15
+ end
16
+ private_class_method :_hooks
17
+
18
+ def self.hooks=(val)
19
+ Thread.current[:_compliable_hooks] = val
20
+ end
21
+
22
+ # Because hooks will be added from the outer edges of
23
+ # the graph, working inwards
24
+ def self.add(prc)
25
+ _hooks.unshift(prc)
26
+ end
27
+
28
+ def self.run
29
+ _hooks.each { |h| h.call }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,39 @@
1
+ module Graphiti
2
+ module Util
3
+ # Utility class for dealing with Include Directives
4
+ class IncludeParams
5
+ class << self
6
+ # Let's say the user requested these sideloads:
7
+ #
8
+ # GET /posts?include=comments.author
9
+ #
10
+ # But our resource had this code:
11
+ #
12
+ # sideload_whitelist({ index: [:comments] })
13
+ #
14
+ # We should drop the 'author' sideload from the request.
15
+ #
16
+ # Hashes become 'include directive hashes' within the library. ie
17
+ #
18
+ # [:tags, { comments: :author }]
19
+ #
20
+ # Becomes
21
+ #
22
+ # { tags: {}, comments: { author: {} } }
23
+ #
24
+ # @param [Hash] requested_includes the nested hash the user requested
25
+ # @param [Hash] allowed_includes the nested hash configured via DSL
26
+ # @return [Hash] the scrubbed hash
27
+ def scrub(requested_includes, allowed_includes)
28
+ {}.tap do |valid|
29
+ requested_includes.each_pair do |key, sub_hash|
30
+ if allowed_includes[key]
31
+ valid[key] = scrub(sub_hash, allowed_includes[key])
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,219 @@
1
+ # Save the given Resource#model, and all of its nested relationships.
2
+ # @api private
3
+ class Graphiti::Util::Persistence
4
+ # @param [Resource] resource the resource instance
5
+ # @param [Hash] meta see (Deserializer#meta)
6
+ # @param [Hash] attributes see (Deserializer#attributes)
7
+ # @param [Hash] relationships see (Deserializer#relationships)
8
+ def initialize(resource, meta, attributes, relationships, caller_model)
9
+ @resource = resource
10
+ @meta = meta
11
+ @attributes = attributes
12
+ @relationships = relationships
13
+ @caller_model = caller_model
14
+
15
+ @attributes.each_pair do |key, value|
16
+ @attributes[key] = @resource.typecast(key, value, :writable)
17
+ end
18
+ end
19
+
20
+ # Perform the actual save logic.
21
+ #
22
+ # belongs_to must be processed before/separately from has_many -
23
+ # we need to know the primary key value of the parent before
24
+ # persisting the child.
25
+ #
26
+ # Flow:
27
+ # * process parents
28
+ # * update attributes to reflect parent primary keys
29
+ # * persist current object
30
+ # * associate temp id with current object
31
+ # * associate parent objects with current object
32
+ # * process children
33
+ # * associate children
34
+ # * record hooks for later playback
35
+ # * run post-process sideload hooks
36
+ # * return current object
37
+ #
38
+ # @return a model instance
39
+ def run
40
+ parents = process_belongs_to(@relationships)
41
+ update_foreign_key_for_parents(parents)
42
+
43
+ persisted = persist_object(@meta[:method], @attributes)
44
+ persisted.instance_variable_set(:@__serializer_klass, @resource.serializer)
45
+ assign_temp_id(persisted, @meta[:temp_id])
46
+
47
+ associate_parents(persisted, parents)
48
+
49
+ children = process_has_many(@relationships, persisted) do |x|
50
+ update_foreign_key(persisted, x[:attributes], x)
51
+ end
52
+
53
+ associate_children(persisted, children) unless @meta[:method] == :destroy
54
+
55
+ post_process(persisted, parents)
56
+ post_process(persisted, children)
57
+ before_commit = -> { @resource.before_commit(persisted, @meta[:method]) }
58
+ add_hook(before_commit)
59
+ persisted
60
+ end
61
+
62
+ private
63
+
64
+ def add_hook(prc)
65
+ ::Graphiti::Util::Hooks.add(prc)
66
+ end
67
+
68
+ # The child's attributes should be modified to nil-out the
69
+ # foreign_key when the parent is being destroyed or disassociated
70
+ #
71
+ # This is not the case for HABTM, whose "foreign key" is a join table
72
+ def update_foreign_key(parent_object, attrs, x)
73
+ return if x[:sideload].type == :many_to_many
74
+
75
+ if [:destroy, :disassociate].include?(x[:meta][:method])
76
+ attrs[x[:foreign_key]] = nil
77
+ update_foreign_type(attrs, x, null: true) if x[:is_polymorphic]
78
+ else
79
+ attrs[x[:foreign_key]] = parent_object.send(x[:primary_key])
80
+ update_foreign_type(attrs, x) if x[:is_polymorphic]
81
+ end
82
+ end
83
+
84
+ def update_foreign_type(attrs, x, null: false)
85
+ grouping_field = x[:sideload].parent.grouper.field_name
86
+ attrs[grouping_field] = null ? nil : x[:sideload].group_name
87
+ end
88
+
89
+ def update_foreign_key_for_parents(parents)
90
+ parents.each do |x|
91
+ update_foreign_key(x[:object], @attributes, x)
92
+ end
93
+ end
94
+
95
+ def associate_parents(object, parents)
96
+ # No need to associate to destroyed objects
97
+ parents = parents.select { |x| x[:meta][:method] != :destroy }
98
+
99
+ parents.each do |x|
100
+ if x[:object] && object
101
+ if x[:meta][:method] == :disassociate
102
+ if x[:sideload].type == :belongs_to
103
+ x[:sideload].disassociate(object, x[:object])
104
+ else
105
+ x[:sideload].disassociate(x[:object], object)
106
+ end
107
+ else
108
+ if x[:sideload].type == :belongs_to
109
+ x[:sideload].associate(object, x[:object])
110
+ else
111
+ if [:has_many, :many_to_many].include?(x[:sideload].type)
112
+ x[:sideload].associate_all(object, Array(x[:object]))
113
+ else
114
+ x[:sideload].associate(x[:object], object)
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ def associate_children(object, children)
123
+ children.each do |x|
124
+ if x[:object] && object
125
+ if x[:meta][:method] == :disassociate
126
+ x[:sideload].disassociate(object, x[:object])
127
+ elsif x[:meta][:method] == :destroy
128
+ if x[:sideload].type == :many_to_many
129
+ x[:sideload].disassociate(object, x[:object])
130
+ end # otherwise, no need to disassociate destroyed objects
131
+ else
132
+ if [:has_many, :many_to_many].include?(x[:sideload].type)
133
+ x[:sideload].associate_all(object, Array(x[:object]))
134
+ else
135
+ x[:sideload].associate(object, x[:object])
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+
142
+ def persist_object(method, attributes)
143
+ case method
144
+ when :destroy
145
+ call_resource_method(:destroy, attributes[:id], @caller_model)
146
+ when :update, nil, :disassociate
147
+ call_resource_method(:update, attributes, @caller_model)
148
+ else
149
+ call_resource_method(:create, attributes, @caller_model)
150
+ end
151
+ end
152
+
153
+ def process_has_many(relationships, caller_model)
154
+ [].tap do |processed|
155
+ iterate(except: [:polymorphic_belongs_to, :belongs_to]) do |x|
156
+ yield x
157
+
158
+ x[:object] = x[:sideload].resource
159
+ .persist_with_relationships(x[:meta], x[:attributes], x[:relationships], caller_model)
160
+ processed << x
161
+ end
162
+ end
163
+ end
164
+
165
+ def process_belongs_to(relationships)
166
+ [].tap do |processed|
167
+ iterate(only: [:polymorphic_belongs_to, :belongs_to]) do |x|
168
+ x[:object] = x[:sideload].resource
169
+ .persist_with_relationships(x[:meta], x[:attributes], x[:relationships])
170
+ processed << x
171
+ end
172
+ end
173
+ end
174
+
175
+ def post_process(caller_model, processed)
176
+ groups = processed.group_by { |x| x[:meta][:method] }
177
+ groups.each_pair do |method, group|
178
+ group.group_by { |g| g[:sideload] }.each_pair do |sideload, members|
179
+ objects = members.map { |x| x[:object] }
180
+ hook = -> { sideload.fire_hooks!(caller_model, objects, method) }
181
+ add_hook(hook)
182
+ end
183
+ end
184
+ end
185
+
186
+ def assign_temp_id(object, temp_id)
187
+ object.instance_variable_set(:@_jsonapi_temp_id, temp_id)
188
+ end
189
+
190
+ def iterate(only: [], except: [])
191
+ opts = {
192
+ resource: @resource,
193
+ relationships: @relationships,
194
+ }.merge(only: only, except: except)
195
+
196
+ Graphiti::Util::RelationshipPayload.iterate(opts) do |x|
197
+ yield x
198
+ end
199
+ end
200
+
201
+ # In the Resource, we want to allow:
202
+ #
203
+ # def create(attrs)
204
+ #
205
+ # and
206
+ #
207
+ # def create(attrs, parent = nil)
208
+ #
209
+ # 'parent' is an optional parameter that should not be part of the
210
+ # method signature in most use cases.
211
+ def call_resource_method(method_name, attributes, caller_model)
212
+ method = @resource.method(method_name)
213
+ if [2,-2].include?(method.arity)
214
+ method.call(attributes, caller_model)
215
+ else
216
+ method.call(attributes)
217
+ end
218
+ end
219
+ end