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,28 @@
1
+ module Graphiti
2
+ # Rails Integration. Mix this in to ApplicationController.
3
+ #
4
+ # * Mixes in Base
5
+ # * Adds a global around_action (see Base#wrap_context)
6
+ #
7
+ # @see Base#render_jsonapi
8
+ # @see Base#wrap_context
9
+ module Rails
10
+ def self.included(klass)
11
+ klass.class_eval do
12
+ include Graphiti::Context
13
+ include JsonapiErrorable
14
+ around_action :wrap_context
15
+ end
16
+ end
17
+
18
+ def wrap_context
19
+ Graphiti.with_context(jsonapi_context, action_name.to_sym) do
20
+ yield
21
+ end
22
+ end
23
+
24
+ def jsonapi_context
25
+ self
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,74 @@
1
+ module Graphiti
2
+ class Railtie < ::Rails::Railtie
3
+
4
+ initializer "graphiti.require_activerecord_adapter" do
5
+ config.after_initialize do |app|
6
+ ActiveSupport.on_load(:active_record) do
7
+ require 'graphiti/adapters/active_record'
8
+ end
9
+ end
10
+ end
11
+
12
+ initializer 'graphiti.init' do
13
+ if Mime[:jsonapi].nil? # rails 4
14
+ Mime::Type.register('application/vnd.api+json', :jsonapi)
15
+ end
16
+ register_parameter_parser
17
+ register_renderers
18
+ establish_concurrency
19
+ end
20
+
21
+ # from jsonapi-rails
22
+ PARSER = lambda do |body|
23
+ data = JSON.parse(body)
24
+ data[:format] = :jsonapi
25
+ data.with_indifferent_access
26
+ end
27
+
28
+ def register_parameter_parser
29
+ if ::Rails::VERSION::MAJOR >= 5
30
+ ActionDispatch::Request.parameter_parsers[:jsonapi] = PARSER
31
+ else
32
+ ActionDispatch::ParamsParser::DEFAULT_PARSERS[Mime[:jsonapi]] = PARSER
33
+ end
34
+ end
35
+
36
+ def register_renderers
37
+ ActiveSupport.on_load(:action_controller) do
38
+ ::ActionController::Renderers.add(:jsonapi) do |proxy, options|
39
+ self.content_type ||= Mime[:jsonapi]
40
+
41
+ opts = {}
42
+ if respond_to?(:default_jsonapi_render_options)
43
+ opts = default_jsonapi_render_options
44
+ end
45
+
46
+ if proxy.is_a?(Hash) # for destroy
47
+ render(options.merge(json: proxy))
48
+ else
49
+ proxy.to_jsonapi(options)
50
+ end
51
+ end
52
+ end
53
+
54
+ ActiveSupport.on_load(:action_controller) do
55
+ ::ActionController::Renderers.add(:jsonapi_errors) do |proxy, options|
56
+ self.content_type ||= Mime[:jsonapi]
57
+
58
+ validation = JsonapiErrorable::Serializers::Validation.new \
59
+ proxy.data, proxy.payload.relationships
60
+
61
+ render \
62
+ json: { errors: validation.errors },
63
+ status: :unprocessable_entity
64
+ end
65
+ end
66
+ end
67
+
68
+ # Only run concurrently if our environment supports it
69
+ def establish_concurrency
70
+ Graphiti.config.concurrency = !::Rails.env.test? &&
71
+ ::Rails.application.config.cache_classes
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,60 @@
1
+ module Graphiti
2
+ class Renderer
3
+ CONTENT_TYPE = 'application/vnd.api+json'
4
+
5
+ attr_reader :proxy, :options
6
+
7
+ def initialize(proxy, options)
8
+ @proxy = proxy
9
+ @options = options
10
+ end
11
+
12
+ def records
13
+ @records ||= @proxy.data
14
+ end
15
+
16
+ def to_jsonapi
17
+ render(JSONAPI::Renderer.new).to_json
18
+ end
19
+
20
+ def to_json
21
+ render(Graphiti::HashRenderer.new(@proxy.resource)).to_json
22
+ end
23
+
24
+ def to_xml
25
+ render(Graphiti::HashRenderer.new(@proxy.resource)).to_xml(root: :data)
26
+ end
27
+
28
+ private
29
+
30
+ def render(implementation)
31
+ notify do
32
+ instance = JSONAPI::Serializable::Renderer.new(implementation)
33
+ options[:fields] = proxy.fields
34
+ options[:expose] ||= {}
35
+ options[:expose][:extra_fields] = proxy.extra_fields
36
+ options[:include] = proxy.include_hash
37
+ options[:meta] ||= {}
38
+ options[:meta].merge!(stats: proxy.stats) unless proxy.stats.empty?
39
+ instance.render(records, options)
40
+ end
41
+ end
42
+
43
+ # TODO: more generic notification pattern
44
+ # Likely comes out of debugger work
45
+ def notify
46
+ if defined?(ActiveSupport::Notifications)
47
+ opts = [
48
+ 'render.jsonapi-compliable',
49
+ records: records,
50
+ options: options
51
+ ]
52
+ ActiveSupport::Notifications.instrument(*opts) do
53
+ yield
54
+ end
55
+ else
56
+ yield
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,110 @@
1
+ module Graphiti
2
+ class Resource
3
+ include DSL
4
+ include Interface
5
+ include Configuration
6
+ include Sideloading
7
+
8
+ attr_reader :context
9
+
10
+ def around_scoping(scope, query_hash)
11
+ yield scope
12
+ end
13
+
14
+ def serializer_for(model)
15
+ serializer
16
+ end
17
+
18
+ def with_context(object, namespace = nil)
19
+ Graphiti.with_context(object, namespace) do
20
+ yield
21
+ end
22
+ end
23
+
24
+ def context
25
+ Graphiti.context[:object]
26
+ end
27
+
28
+ def context_namespace
29
+ Graphiti.context[:namespace]
30
+ end
31
+
32
+ def build_scope(base, query, opts = {})
33
+ Scope.new(base, self, query, opts)
34
+ end
35
+
36
+ def base_scope
37
+ adapter.base_scope(model)
38
+ end
39
+
40
+ def typecast(name, value, flag)
41
+ att = get_attr!(name, flag)
42
+ type = Graphiti::Types[att[:type]]
43
+ begin
44
+ flag = :read if flag == :readable
45
+ flag = :write if flag == :writable
46
+ flag = :params if [:sortable, :filterable].include?(flag)
47
+ type[flag][value]
48
+ rescue Exception => e
49
+ raise Errors::TypecastFailed.new(self, name, value, e)
50
+ end
51
+ end
52
+
53
+ def create(create_params)
54
+ adapter.create(model, create_params)
55
+ end
56
+
57
+ def update(update_params)
58
+ adapter.update(model, update_params)
59
+ end
60
+
61
+ def destroy(id)
62
+ adapter.destroy(model, id)
63
+ end
64
+
65
+ def associate_all(parent, children, association_name, type)
66
+ adapter.associate_all(parent, children, association_name, type)
67
+ end
68
+
69
+ def associate(parent, child, association_name, type)
70
+ adapter.associate(parent, child, association_name, type)
71
+ end
72
+
73
+ def disassociate(parent, child, association_name, type)
74
+ adapter.disassociate(parent, child, association_name, type)
75
+ end
76
+
77
+ def persist_with_relationships(meta, attributes, relationships, caller_model = nil)
78
+ persistence = Graphiti::Util::Persistence \
79
+ .new(self, meta, attributes, relationships, caller_model)
80
+ persistence.run
81
+ end
82
+
83
+ def stat(attribute, calculation)
84
+ stats_dsl = stats[attribute] || stats[attribute.to_sym]
85
+ raise Errors::StatNotFound.new(attribute, calculation) unless stats_dsl
86
+ stats_dsl.calculation(calculation)
87
+ end
88
+
89
+ def resolve(scope)
90
+ adapter.resolve(scope)
91
+ end
92
+
93
+ def before_commit(model, method)
94
+ hook = self.class.config[:before_commit][method]
95
+ hook.call(model) if hook
96
+ end
97
+
98
+ def transaction
99
+ response = nil
100
+ begin
101
+ adapter.transaction(model) do
102
+ response = yield
103
+ end
104
+ rescue Errors::ValidationError => e
105
+ response = e.validation_response
106
+ end
107
+ response
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,239 @@
1
+ module Graphiti
2
+ class Resource
3
+ module Configuration
4
+ extend ActiveSupport::Concern
5
+
6
+ module Overrides
7
+ def serializer=(val)
8
+ if val
9
+ if super(Class.new(val))
10
+ apply_attributes_to_serializer
11
+ apply_sideloads_to_serializer
12
+ end
13
+ else
14
+ super
15
+ end
16
+ end
17
+
18
+ def polymorphic=(klasses)
19
+ super
20
+ send(:prepend, Polymorphism)
21
+ end
22
+
23
+ def type=(val)
24
+ if val = super
25
+ self.serializer.type(val)
26
+ end
27
+ end
28
+
29
+ def model
30
+ klass = super
31
+ unless klass || abstract_class?
32
+ if klass = infer_model
33
+ self.model = klass
34
+ else
35
+ raise Errors::ModelNotFound.new(self)
36
+ end
37
+ end
38
+ klass
39
+ end
40
+ end
41
+
42
+ included do
43
+ class << self
44
+ attr_writer :config
45
+ end
46
+
47
+ class_attribute :adapter,
48
+ :model,
49
+ :type,
50
+ :polymorphic,
51
+ :polymorphic_child,
52
+ :serializer,
53
+ :default_page_size,
54
+ :default_sort,
55
+ :attributes_readable_by_default,
56
+ :attributes_writable_by_default,
57
+ :attributes_sortable_by_default,
58
+ :attributes_filterable_by_default,
59
+ :relationships_readable_by_default,
60
+ :relationships_writable_by_default
61
+
62
+ class << self
63
+ prepend Overrides
64
+ end
65
+
66
+ def self.inherited(klass)
67
+ super
68
+ klass.config = Util::Hash.deep_dup(config)
69
+ klass.adapter ||= Adapters::Abstract.new
70
+ klass.default_sort ||= []
71
+ klass.default_page_size ||= 20
72
+ # re-assigning causes a new Class.new
73
+ if klass.serializer
74
+ klass.serializer = klass.serializer
75
+ else
76
+ klass.serializer = JSONAPI::Serializable::Resource
77
+ end
78
+ klass.type ||= klass.infer_type
79
+ default(klass, :attributes_readable_by_default, true)
80
+ default(klass, :attributes_writable_by_default, true)
81
+ default(klass, :attributes_sortable_by_default, true)
82
+ default(klass, :attributes_filterable_by_default, true)
83
+ default(klass, :relationships_readable_by_default, true)
84
+ default(klass, :relationships_writable_by_default, true)
85
+
86
+ unless klass.config[:attributes][:id]
87
+ klass.attribute :id, :integer_id
88
+ end
89
+ klass.stat total: [:count]
90
+ end
91
+ end
92
+
93
+ class_methods do
94
+ def get_attr!(name, flag, opts = {})
95
+ opts[:raise_error] = true
96
+ get_attr(name, flag, opts)
97
+ end
98
+
99
+ def get_attr(name, flag, opts = {})
100
+ defaults = { request: false }
101
+ opts = defaults.merge(opts)
102
+ new.get_attr(name, flag, opts)
103
+ end
104
+
105
+ def abstract_class?
106
+ !!abstract_class
107
+ end
108
+
109
+ def abstract_class
110
+ @abstract_class
111
+ end
112
+
113
+ def abstract_class=(val)
114
+ if @abstract_class = val
115
+ self.serializer = nil
116
+ self.type = nil
117
+ end
118
+ end
119
+
120
+ def infer_type
121
+ if name.present?
122
+ name.demodulize.gsub('Resource','').underscore.pluralize.to_sym
123
+ else
124
+ :undefined_jsonapi_type
125
+ end
126
+ end
127
+
128
+ def infer_model
129
+ name.gsub('Resource', '').safe_constantize if name
130
+ end
131
+
132
+ def default(object, attr, value)
133
+ prior = object.send(attr)
134
+ unless prior || prior == false
135
+ object.send(:"#{attr}=", value)
136
+ end
137
+ end
138
+ private :default
139
+
140
+ def config
141
+ @config ||=
142
+ {
143
+ filters: {},
144
+ default_filters: {},
145
+ stats: {},
146
+ sort_all: nil,
147
+ sorts: {},
148
+ pagination: nil,
149
+ before_commit: {},
150
+ attributes: {},
151
+ extra_attributes: {},
152
+ sideloads: {}
153
+ }
154
+ end
155
+
156
+ def attributes
157
+ config[:attributes]
158
+ end
159
+
160
+ def extra_attributes
161
+ config[:extra_attributes]
162
+ end
163
+
164
+ def all_attributes
165
+ attributes.merge(extra_attributes)
166
+ end
167
+
168
+ def sideloads
169
+ config[:sideloads]
170
+ end
171
+
172
+ def filters
173
+ config[:filters]
174
+ end
175
+
176
+ def sorts
177
+ config[:sorts]
178
+ end
179
+
180
+ def stats
181
+ config[:stats]
182
+ end
183
+
184
+ def pagination
185
+ config[:pagination]
186
+ end
187
+
188
+ def default_filters
189
+ config[:default_filters]
190
+ end
191
+ end
192
+
193
+ def get_attr!(name, flag, options = {})
194
+ options[:raise_error] = true
195
+ get_attr(name, flag, options)
196
+ end
197
+
198
+ def get_attr(name, flag, request: false, raise_error: false)
199
+ Util::AttributeCheck.run(self, name, flag, request, raise_error)
200
+ end
201
+
202
+ def filters
203
+ self.class.filters
204
+ end
205
+
206
+ def sort_all
207
+ self.class.sort_all
208
+ end
209
+
210
+ def sorts
211
+ self.class.sorts
212
+ end
213
+
214
+ def stats
215
+ self.class.stats
216
+ end
217
+
218
+ def pagination
219
+ self.class.pagination
220
+ end
221
+
222
+ def attributes
223
+ self.class.attributes
224
+ end
225
+
226
+ def extra_attributes
227
+ self.class.extra_attributes
228
+ end
229
+
230
+ def all_attributes
231
+ self.class.all_attributes
232
+ end
233
+
234
+ def default_filters
235
+ self.class.default_filters
236
+ end
237
+ end
238
+ end
239
+ end