graphiti-rb 1.0.alpha.1

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