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.
- checksums.yaml +4 -4
- data/README.md +13 -104
- data/lib/jsonapi/realizer.rb +26 -25
- data/lib/jsonapi/realizer/action.rb +2 -2
- data/lib/jsonapi/realizer/adapter.rb +21 -58
- data/lib/jsonapi/realizer/adapter/active_record.rb +38 -17
- data/lib/jsonapi/realizer/adapter_spec.rb +3 -2
- data/lib/jsonapi/realizer/configuration.rb +22 -0
- data/lib/jsonapi/realizer/context.rb +8 -0
- data/lib/jsonapi/realizer/controller.rb +55 -0
- data/lib/jsonapi/realizer/error.rb +10 -9
- data/lib/jsonapi/realizer/error/invalid_content_type_header.rb +5 -0
- data/lib/jsonapi/realizer/error/invalid_data_type_property.rb +13 -0
- data/lib/jsonapi/realizer/error/invalid_root_property.rb +13 -0
- data/lib/jsonapi/realizer/error/{duplicate_registration.rb → missing_data_type_property.rb} +1 -1
- data/lib/jsonapi/realizer/error/resource_attribute_not_found.rb +14 -0
- data/lib/jsonapi/realizer/error/resource_relationship_not_found.rb +14 -0
- data/lib/jsonapi/realizer/resource.rb +278 -73
- data/lib/jsonapi/realizer/resource/attribute.rb +23 -0
- data/lib/jsonapi/realizer/resource/configuration.rb +27 -0
- data/lib/jsonapi/realizer/resource/relation.rb +31 -0
- data/lib/jsonapi/realizer/resource_spec.rb +55 -8
- data/lib/jsonapi/realizer/version.rb +1 -1
- data/lib/jsonapi/realizer_spec.rb +22 -119
- metadata +70 -20
- data/lib/jsonapi/realizer/action/create.rb +0 -36
- data/lib/jsonapi/realizer/action/create_spec.rb +0 -165
- data/lib/jsonapi/realizer/action/destroy.rb +0 -27
- data/lib/jsonapi/realizer/action/destroy_spec.rb +0 -81
- data/lib/jsonapi/realizer/action/index.rb +0 -29
- data/lib/jsonapi/realizer/action/index_spec.rb +0 -75
- data/lib/jsonapi/realizer/action/show.rb +0 -35
- data/lib/jsonapi/realizer/action/show_spec.rb +0 -81
- data/lib/jsonapi/realizer/action/update.rb +0 -37
- data/lib/jsonapi/realizer/action/update_spec.rb +0 -170
- data/lib/jsonapi/realizer/action_spec.rb +0 -46
- data/lib/jsonapi/realizer/adapter/memory.rb +0 -31
- data/lib/jsonapi/realizer/error/invalid_accept_header.rb +0 -9
- data/lib/jsonapi/realizer/error/malformed_data_root_property.rb +0 -9
- data/lib/jsonapi/realizer/error/missing_accept_header.rb +0 -9
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 56892c3c8c68851f388d17eb94937a079fadddb1490cac12f45f46497a4b9f5b
|
4
|
+
data.tar.gz: 46e26b62903cf861f7114b44a762912608fe52100674cad749e63e68ac6c731a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
51
|
+
You can define aliases for these properties:
|
52
52
|
|
53
53
|
``` ruby
|
54
|
-
has_many :doctors, as: :users
|
54
|
+
has_many :doctors, as: :users
|
55
55
|
|
56
|
-
has :title,
|
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. `
|
159
|
-
0. `
|
160
|
-
0. `
|
161
|
-
0. `
|
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. `
|
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 `
|
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.
|
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.
|
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
|
-
|
334
|
-
|
335
|
-
And then execute:
|
336
|
-
|
337
|
-
$ bundle
|
246
|
+
$ bundle add jsonapi-realizer
|
338
247
|
|
339
248
|
Or install it yourself with:
|
340
249
|
|
data/lib/jsonapi/realizer.rb
CHANGED
@@ -1,36 +1,37 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
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
|
11
|
-
require_relative
|
12
|
-
require_relative
|
13
|
-
require_relative
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
33
|
-
|
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
|
-
|
5
|
-
|
4
|
+
include(ActiveModel::Model)
|
5
|
+
|
6
|
+
require_relative("adapter/active_record")
|
6
7
|
|
7
8
|
MAPPINGS = {
|
8
|
-
|
9
|
-
active_record: JSONAPI::Realizer::Adapter::ACTIVE_RECORD,
|
9
|
+
:active_record => JSONAPI::Realizer::Adapter::ActiveRecord
|
10
10
|
}
|
11
|
+
private_constant :MAPPINGS
|
11
12
|
|
12
|
-
|
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
|
-
|
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
|
28
|
-
|
29
|
-
end
|
17
|
+
def initialize(interface:)
|
18
|
+
super(interface: interface)
|
30
19
|
|
31
|
-
|
32
|
-
@find_many_via_call = callback
|
33
|
-
end
|
20
|
+
validate!
|
34
21
|
|
35
|
-
|
36
|
-
@assign_attributes_via_call = callback
|
37
|
-
end
|
22
|
+
mappings = MAPPINGS.merge(JSONAPI::Realizer.configuration.adapter_mappings).with_indifferent_access
|
38
23
|
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
68
|
-
@sparse_fields_call.call(model_class, fields)
|
69
|
-
end
|
28
|
+
self.singleton_class.prepend(mappings.fetch(interface))
|
70
29
|
|
71
|
-
|
72
|
-
|
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
|
-
|
5
|
-
|
6
|
-
|
4
|
+
module ActiveRecord
|
5
|
+
def find_many(scope)
|
6
|
+
scope.all
|
7
7
|
end
|
8
8
|
|
9
|
-
|
10
|
-
|
9
|
+
def find_one(scope, id)
|
10
|
+
scope.find(id)
|
11
11
|
end
|
12
12
|
|
13
|
-
|
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
|
-
|
40
|
+
def write_relationships(model, relationships)
|
18
41
|
model.assign_attributes(relationships)
|
19
42
|
end
|
20
43
|
|
21
|
-
|
22
|
-
|
44
|
+
def include_relationships(scope, includes)
|
45
|
+
scope.eager_load(*includes.map(&method(:arel_chain)))
|
23
46
|
end
|
24
47
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
@@ -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
|