jsonapi-realizer 4.4.0 → 5.0.0

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -104
  3. data/lib/jsonapi/realizer.rb +26 -25
  4. data/lib/jsonapi/realizer/action.rb +2 -2
  5. data/lib/jsonapi/realizer/adapter.rb +21 -58
  6. data/lib/jsonapi/realizer/adapter/active_record.rb +38 -17
  7. data/lib/jsonapi/realizer/adapter_spec.rb +3 -2
  8. data/lib/jsonapi/realizer/configuration.rb +22 -0
  9. data/lib/jsonapi/realizer/context.rb +8 -0
  10. data/lib/jsonapi/realizer/controller.rb +55 -0
  11. data/lib/jsonapi/realizer/error.rb +10 -9
  12. data/lib/jsonapi/realizer/error/invalid_content_type_header.rb +5 -0
  13. data/lib/jsonapi/realizer/error/invalid_data_type_property.rb +13 -0
  14. data/lib/jsonapi/realizer/error/invalid_root_property.rb +13 -0
  15. data/lib/jsonapi/realizer/error/{duplicate_registration.rb → missing_data_type_property.rb} +1 -1
  16. data/lib/jsonapi/realizer/error/resource_attribute_not_found.rb +14 -0
  17. data/lib/jsonapi/realizer/error/resource_relationship_not_found.rb +14 -0
  18. data/lib/jsonapi/realizer/resource.rb +278 -73
  19. data/lib/jsonapi/realizer/resource/attribute.rb +23 -0
  20. data/lib/jsonapi/realizer/resource/configuration.rb +27 -0
  21. data/lib/jsonapi/realizer/resource/relation.rb +31 -0
  22. data/lib/jsonapi/realizer/resource_spec.rb +55 -8
  23. data/lib/jsonapi/realizer/version.rb +1 -1
  24. data/lib/jsonapi/realizer_spec.rb +22 -119
  25. metadata +70 -20
  26. data/lib/jsonapi/realizer/action/create.rb +0 -36
  27. data/lib/jsonapi/realizer/action/create_spec.rb +0 -165
  28. data/lib/jsonapi/realizer/action/destroy.rb +0 -27
  29. data/lib/jsonapi/realizer/action/destroy_spec.rb +0 -81
  30. data/lib/jsonapi/realizer/action/index.rb +0 -29
  31. data/lib/jsonapi/realizer/action/index_spec.rb +0 -75
  32. data/lib/jsonapi/realizer/action/show.rb +0 -35
  33. data/lib/jsonapi/realizer/action/show_spec.rb +0 -81
  34. data/lib/jsonapi/realizer/action/update.rb +0 -37
  35. data/lib/jsonapi/realizer/action/update_spec.rb +0 -170
  36. data/lib/jsonapi/realizer/action_spec.rb +0 -46
  37. data/lib/jsonapi/realizer/adapter/memory.rb +0 -31
  38. data/lib/jsonapi/realizer/error/invalid_accept_header.rb +0 -9
  39. data/lib/jsonapi/realizer/error/malformed_data_root_property.rb +0 -9
  40. data/lib/jsonapi/realizer/error/missing_accept_header.rb +0 -9
  41. data/lib/jsonapi/realizer/error/missing_type_resource_property.rb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a8bebae1837baec43c16e73cc362203f65387459f992a81732d035ffdb0c61e
4
- data.tar.gz: dad4dfe6f56dd8c85f35c9e3f829939d5278e652f7489d9a01958d15071e3aa8
3
+ metadata.gz: 56892c3c8c68851f388d17eb94937a079fadddb1490cac12f45f46497a4b9f5b
4
+ data.tar.gz: 46e26b62903cf861f7114b44a762912608fe52100674cad749e63e68ac6c731a
5
5
  SHA512:
6
- metadata.gz: 101883f640d8a56b561212abc84fd4cdf34613b41a0f14e6d8625eb6439b31c1e236eece7663366aa1ba6b7660279da05547d1f9eba9050ded2a12c04aa4fec4
7
- data.tar.gz: cb0f5eb6555aa35c64e7b8bafbeced9bad365ff337bd26ea5a023e26e529a6ecd3b3c2f4cde93de4090f9243d31df486b28a2f59dd99c02dac73f9c6488ca60c
6
+ metadata.gz: 355c9f15d66d5784a95731bb81f1977f7adc8da4e28e4c470d9fe90ebe697d8583f3c0262ee8d9c2c524c83869ccde408b4b6d78199f2655b64700507d85720a
7
+ data.tar.gz: 7faacba0d5f46021462ba6722a2305f5eff92c7cad9bad7bdf5411cd6bbeaff06ab84ec0651fa939e406875b5d2b275b2e448f39d7b9c1a0436acc5ce3529ec9
data/README.md CHANGED
@@ -48,12 +48,12 @@ class ProfileRealizer
48
48
  end
49
49
  ```
50
50
 
51
- You can define special properties on attributes and relationships realizers:
51
+ You can define aliases for these properties:
52
52
 
53
53
  ``` ruby
54
- has_many :doctors, as: :users, includable: false
54
+ has_many :doctors, as: :users
55
55
 
56
- has :title, selectable: false
56
+ has :title, as: :name
57
57
  ```
58
58
 
59
59
  Once you've designed your resources, we just need to use them! In this example, we'll use controllers from Rails:
@@ -78,73 +78,6 @@ end
78
78
 
79
79
  Notice that we pass `realization.model` to `ProcessPhotosService`, that's because `jsonapi-realizer` doesn't do the act of saving, creating, or destroying! We just ready up the records for you to handle (including errors).
80
80
 
81
- ### Policies
82
-
83
- Most times you will want to control what a person sees when they as for your data. We have created interfaces for this use-case and we'll show how you can use pundit (or any PORO) to constrain your in/out.
84
-
85
- First up is the policy itself:
86
-
87
- ``` ruby
88
- class PhotoPolicy < ApplicationPolicy
89
- class Scope < ApplicationPolicy::Scope
90
- def resolve
91
- case
92
- when relation.with_role_state?(:administrator)
93
- relation
94
- when requester.with_onboarding_state?(:completed)
95
- relation.where(photographer: requester)
96
- else
97
- relation.none
98
- end
99
- end
100
-
101
- def sanitize(action, params)
102
- case action
103
- when :index
104
- params.permit(:fields, :include, :filter)
105
- else
106
- params
107
- end
108
- end
109
- end
110
-
111
- def index?
112
- requester.with_onboarding_state?(:completed)
113
- end
114
- end
115
- ```
116
-
117
- ``` ruby
118
- class PhotoRealizer
119
- include JSONAPI::Realizer::Resource
120
-
121
- register :photos, class_name: "Photo", adapter: :active_record
122
-
123
- has_one :photographer, as: :profiles
124
-
125
- has :title
126
- has :src
127
- end
128
- ```
129
-
130
- ``` ruby
131
- class PhotosController < ApplicationController
132
- def index
133
- # See: pundit for `policy_scope()`
134
- realization = JSONAPI::Realizer.index(
135
- policy(Photo).sanitize(:index, params),
136
- headers: request.headers,
137
- type: :posts,
138
- scope: policy_scope(Photo)
139
- )
140
-
141
- # See: pundit for `authorize()`
142
- authorize(realization.relation)
143
-
144
- render json: JSONAPI::Serializer.serialize(realization.models, is_collection: true)
145
- end
146
- end
147
- ```
148
81
 
149
82
  ### Adapters
150
83
 
@@ -155,14 +88,14 @@ There are two core adapters:
155
88
 
156
89
  An adapter must provide the following interfaces:
157
90
 
158
- 0. `find_via`, describes how to find the model
159
- 0. `find_many_via`, describes how to find many models
160
- 0. `assign_attributes_via`, describes how to write a set of properties
161
- 0. `assign_relationships_via`, describes how to write a set of relationships
91
+ 0. `find_one`, describes how to find the model
92
+ 0. `find_many`, describes how to find many models
93
+ 0. `write_attributes`, describes how to write a set of properties
94
+ 0. `write_relationships`, describes how to write a set of relationships
162
95
  0. `includes_via`, describes how to eager include related models
163
- 0. `sparse_fields_via`, describes how to only return certain fields
96
+ 0. `sparse_fields`, describes how to only return certain fields
164
97
 
165
- You can also provide custom adapter interfaces like below, which will use `active_record`'s `find_many_via`, `assign_relationships_via`, `update_via`, `includes_via`, and `sparse_fields_via`:
98
+ You can also provide custom adapter interfaces like below, which will use `active_record`'s `find_many`, `write_relationships`, `update_via`, `includes_via`, and `sparse_fields`:
166
99
 
167
100
  ``` ruby
168
101
  class PhotoRealizer
@@ -170,11 +103,11 @@ class PhotoRealizer
170
103
 
171
104
  register :photos, class_name: "Photo", adapter: :active_record
172
105
 
173
- adapter.find_via do |model_class, id|
106
+ adapter.find_one do |model_class, id|
174
107
  model_class.where { id == id or slug == id }.first
175
108
  end
176
109
 
177
- adapter.assign_attributes_via do |model, attributes|
110
+ adapter.write_attributes do |model, attributes|
178
111
  model.update_columns(attributes)
179
112
  end
180
113
 
@@ -303,38 +236,14 @@ end
303
236
 
304
237
  ### jsonapi-home
305
238
 
306
- I'm already using jsonapi-realizer and it's sister project jsonapi-serializers in a new gem of mine that allows services to be discoverable: [jsonapi-home](https://github.com/krainboltgreene/jsonapi-home).
307
-
308
- ### Notes
309
-
310
- A successful JSON:API request can be annotated as:
311
-
312
- ```
313
- JSONAPIRequest -> (BusinessLayer -> JSONAPIRequest -> (Record | Array<Record>)) -> JSONAPIResponse
314
- ```
315
-
316
- The `jsonapi-serializers` library provides this shape:
317
-
318
- ```
319
- JSONAPIRequest -> (Record | Array<Record>) -> JSONAPIResponse
320
- ```
321
-
322
- But it leaves fetching/creating/updating/destroying the records up to you! This is where jsonapi-realizer comes into play, as it provides this shape:
323
-
324
- ```
325
- BusinessLayer -> JSONAPIRequest -> (Record | Array<Record>)
326
- ```
239
+ I'm already using jsonapi-realizer and it's sister project jsonapi-serializers in a new gem of mine that allows services to be discoverable: [jsonapi-home](https://github.com/krainboltgreene/jsonapi-home.rb).
327
240
 
328
241
 
329
242
  ## Installing
330
243
 
331
244
  Add this line to your application's Gemfile:
332
245
 
333
- gem "jsonapi-realizer", "4.1.0"
334
-
335
- And then execute:
336
-
337
- $ bundle
246
+ $ bundle add jsonapi-realizer
338
247
 
339
248
  Or install it yourself with:
340
249
 
@@ -1,36 +1,37 @@
1
- require "ostruct"
2
- require "active_support/concern"
3
- require "active_support/core_ext/enumerable"
4
- require "active_support/core_ext/string"
1
+ require("ostruct")
2
+ require("addressable")
3
+ require("active_model")
4
+ require("active_support/concern")
5
+ require("active_support/core_ext/enumerable")
6
+ require("active_support/core_ext/string")
7
+ require("active_support/core_ext/module")
5
8
 
6
9
  module JSONAPI
7
10
  MEDIA_TYPE = "application/vnd.api+json" unless const_defined?("MEDIA_TYPE")
8
11
 
9
12
  module Realizer
10
- require_relative "realizer/version"
11
- require_relative "realizer/error"
12
- require_relative "realizer/action"
13
- require_relative "realizer/adapter"
14
- require_relative "realizer/resource"
13
+ require_relative("realizer/version")
14
+ require_relative("realizer/error")
15
+ require_relative("realizer/configuration")
16
+ require_relative("realizer/controller")
15
17
 
16
- def self.create(payload, headers:, scope: nil)
17
- enact(Action::Create.new(payload: payload, headers: headers, scope: scope))
18
- end
19
-
20
- def self.update(payload, headers:, scope: nil)
21
- enact(Action::Update.new(payload: payload, headers: headers, scope: scope))
22
- end
18
+ @configuration ||= Configuration.new(
19
+ :default_invalid_content_type_exception => JSONAPI::Realizer::Error::InvalidContentTypeHeader,
20
+ :default_missing_content_type_exception => JSONAPI::Realizer::Error::MissingContentTypeHeader,
21
+ :default_identifier => :id,
22
+ :adapter_mappings => {}
23
+ )
23
24
 
24
- def self.show(payload, headers:, type:, scope: nil)
25
- enact(Action::Show.new(payload: payload, headers: headers, type: type, scope: scope))
26
- end
27
-
28
- def self.index(payload, headers:, type:, scope: nil)
29
- enact(Action::Index.new(payload: payload, headers: headers, type: type, scope: scope))
30
- end
25
+ require_relative("realizer/adapter")
26
+ require_relative("realizer/context")
27
+ require_relative("realizer/resource")
31
28
 
32
- private_class_method def self.enact(action)
33
- action.tap(&:call)
29
+ def self.configuration
30
+ if block_given?
31
+ yield(@configuration)
32
+ else
33
+ @configuration
34
+ end
34
35
  end
35
36
  end
36
37
  end
@@ -16,9 +16,9 @@ module JSONAPI
16
16
  @payload = payload
17
17
 
18
18
  raise Error::MissingAcceptHeader unless @headers.key?("Accept")
19
- raise Error::InvalidAcceptHeader unless @headers.fetch("Accept") == JSONAPI::MEDIA_TYPE
19
+ raise Error::InvalidAcceptHeader, given: @headers.fetch("Accept"), wanted: JSONAPI::MEDIA_TYPE unless @headers.fetch("Accept") == JSONAPI::MEDIA_TYPE
20
20
  raise Error::IncludeWithoutDataProperty if @payload.key?("include") && !@payload.key?("data")
21
- raise Error::MalformedDataRootProperty if @payload.key?("data") && !(data.kind_of?(Array) || data.kind_of?(Hash) || data.nil?)
21
+ raise Error::MalformedDataRootProperty, given: data if @payload.key?("data") && !(data.kind_of?(Array) || data.kind_of?(Hash) || data.nil?)
22
22
  end
23
23
 
24
24
  def call; end
@@ -1,75 +1,38 @@
1
1
  module JSONAPI
2
2
  module Realizer
3
3
  class Adapter
4
- require_relative "adapter/active_record"
5
- require_relative "adapter/memory"
4
+ include(ActiveModel::Model)
5
+
6
+ require_relative("adapter/active_record")
6
7
 
7
8
  MAPPINGS = {
8
- memory: JSONAPI::Realizer::Adapter::MEMORY,
9
- active_record: JSONAPI::Realizer::Adapter::ACTIVE_RECORD,
9
+ :active_record => JSONAPI::Realizer::Adapter::ActiveRecord
10
10
  }
11
+ private_constant :MAPPINGS
11
12
 
12
- def initialize(interface)
13
- if JSONAPI::Realizer::Adapter::MAPPINGS.key?(interface.to_sym)
14
- instance_eval(&JSONAPI::Realizer::Adapter::MAPPINGS.fetch(interface.to_sym))
15
- else
16
- raise ArgumentError, "you've given an invalid adapter alias: #{interface}, we support #{JSONAPI::Realizer::Adapter::MAPPINGS.keys}"
17
- end
13
+ attr_accessor :interface
18
14
 
19
- raise ArgumentError, "need to provide a Adapter.find_via interface" unless instance_variable_defined?(:@find_via_call)
20
- raise ArgumentError, "need to provide a Adapter.find_many_via_call interface" unless instance_variable_defined?(:@find_many_via_call)
21
- raise ArgumentError, "need to provide a Adapter.assign_attributes_via interface" unless instance_variable_defined?(:@assign_attributes_via_call)
22
- raise ArgumentError, "need to provide a Adapter.assign_relationships_via interface" unless instance_variable_defined?(:@assign_relationships_via_call)
23
- raise ArgumentError, "need to provide a Adapter.sparse_fields interface" unless instance_variable_defined?(:@sparse_fields_call)
24
- raise ArgumentError, "need to provide a Adapter.include_via interface" unless instance_variable_defined?(:@include_via_call)
25
- end
15
+ validates_presence_of(:interface)
26
16
 
27
- def find_via(&callback)
28
- @find_via_call = callback
29
- end
17
+ def initialize(interface:)
18
+ super(interface: interface)
30
19
 
31
- def find_many_via(&callback)
32
- @find_many_via_call = callback
33
- end
20
+ validate!
34
21
 
35
- def assign_attributes_via(&callback)
36
- @assign_attributes_via_call = callback
37
- end
22
+ mappings = MAPPINGS.merge(JSONAPI::Realizer.configuration.adapter_mappings).with_indifferent_access
38
23
 
39
- def assign_relationships_via(&callback)
40
- @assign_relationships_via_call = callback
41
- end
42
-
43
- def sparse_fields(&callback)
44
- @sparse_fields_call = callback
45
- end
46
-
47
- def include_via(&callback)
48
- @include_via_call = callback
49
- end
50
-
51
- def find_via_call(model_class, id)
52
- @find_via_call.call(model_class, id)
53
- end
54
-
55
- def find_many_via_call(model_class)
56
- @find_many_via_call.call(model_class)
57
- end
58
-
59
- def assign_attributes_via_call(model, attributes)
60
- @assign_attributes_via_call.call(model, attributes)
61
- end
62
-
63
- def assign_relationships_via_call(model, relationships)
64
- @assign_relationships_via_call.call(model, relationships)
65
- end
24
+ unless mappings.key?(interface)
25
+ raise(ArgumentError, "you've given an invalid adapter alias: #{interface}, we support #{mappings.keys.to_sentence}")
26
+ end
66
27
 
67
- def sparse_fields_call(model_class, fields)
68
- @sparse_fields_call.call(model_class, fields)
69
- end
28
+ self.singleton_class.prepend(mappings.fetch(interface))
70
29
 
71
- def include_via_call(model_class, includes)
72
- @include_via_call.call(model_class, includes)
30
+ raise(ArgumentError, "need to provide a Adapter#find_one interface") unless respond_to?(:find_one)
31
+ raise(ArgumentError, "need to provide a Adapter#find_many interface") unless respond_to?(:find_many)
32
+ raise(ArgumentError, "need to provide a Adapter#write_attributes interface") unless respond_to?(:write_attributes)
33
+ raise(ArgumentError, "need to provide a Adapter#write_relationships interface") unless respond_to?(:write_relationships)
34
+ raise(ArgumentError, "need to provide a Adapter#include_relationships interface") unless respond_to?(:include_relationships)
35
+ raise(ArgumentError, "need to provide a Adapter#paginate interface") unless respond_to?(:paginate)
73
36
  end
74
37
  end
75
38
  end
@@ -1,35 +1,56 @@
1
1
  module JSONAPI
2
2
  module Realizer
3
3
  class Adapter
4
- ACTIVE_RECORD = Proc.new do
5
- find_many_via do |model_class|
6
- model_class.all
4
+ module ActiveRecord
5
+ def find_many(scope)
6
+ scope.all
7
7
  end
8
8
 
9
- find_via do |model_class, id|
10
- model_class.find(id)
9
+ def find_one(scope, id)
10
+ scope.find(id)
11
11
  end
12
12
 
13
- assign_attributes_via do |model, attributes|
13
+ def filtering(scope, filters)
14
+ scope.where(filters.slice(*scope.column_names))
15
+ end
16
+
17
+ def sorting(scope, sorts)
18
+ scope.order(
19
+ *sorts.
20
+ map do |(keychain, direction)|
21
+ [keychain, if direction == "-" then :DESC else :ASC end]
22
+ end.
23
+ map do |(keychain, direction)|
24
+ [keychain.map {|key| key.inspect}.join("."), direction]
25
+ end.
26
+ map do |pair|
27
+ Arel.sql(pair.join(" "))
28
+ end
29
+ )
30
+ end
31
+
32
+ def paginate(scope, per, offset)
33
+ scope.page(offset).per(per)
34
+ end
35
+
36
+ def write_attributes(model, attributes)
14
37
  model.assign_attributes(attributes)
15
38
  end
16
39
 
17
- assign_relationships_via do |model, relationships|
40
+ def write_relationships(model, relationships)
18
41
  model.assign_attributes(relationships)
19
42
  end
20
43
 
21
- sparse_fields do |model_class, fields|
22
- model_class.select(fields)
44
+ def include_relationships(scope, includes)
45
+ scope.eager_load(*includes.map(&method(:arel_chain)))
23
46
  end
24
47
 
25
- include_via do |model_class, includes|
26
- model_class.includes(includes.map(&(recursively_nest = -> (chains) do
27
- if chains.size == 1
28
- chains.first
29
- else
30
- {chains.first => recursively_nest.call(chains.drop(1))}
31
- end
32
- end)))
48
+ private def arel_chain(chains)
49
+ if chains.size == 1
50
+ chains.first
51
+ else
52
+ {chains.first => arel_chain(chains.drop(1))}
53
+ end
33
54
  end
34
55
  end
35
56
  end
@@ -1,4 +1,5 @@
1
- require "spec_helper"
1
+ require("spec_helper")
2
+
3
+ RSpec.describe(JSONAPI::Realizer::Adapter) do
2
4
 
3
- RSpec.describe JSONAPI::Realizer::Adapter do
4
5
  end
@@ -0,0 +1,22 @@
1
+ module JSONAPI
2
+ module Realizer
3
+ class Configuration
4
+ include(ActiveModel::Model)
5
+
6
+ attr_accessor(:default_origin)
7
+ attr_accessor(:default_identifier)
8
+ attr_accessor(:adapter_mappings)
9
+ attr_accessor(:default_missing_content_type_exception)
10
+ attr_accessor(:default_invalid_content_type_exception)
11
+
12
+ validates_presence_of(:default_missing_content_type_exception)
13
+ validates_presence_of(:default_invalid_content_type_exception)
14
+
15
+ def initialize(**keyword_arguments)
16
+ super(**keyword_arguments)
17
+
18
+ validate!
19
+ end
20
+ end
21
+ end
22
+ end