jsonapi-realizer 2.0.3 → 3.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 +88 -28
- data/lib/jsonapi/realizer.rb +2 -0
- data/lib/jsonapi/realizer/action.rb +49 -18
- data/lib/jsonapi/realizer/action/create.rb +14 -0
- data/lib/jsonapi/realizer/action/create_spec.rb +57 -51
- data/lib/jsonapi/realizer/action/index.rb +11 -0
- data/lib/jsonapi/realizer/action/index_spec.rb +19 -13
- data/lib/jsonapi/realizer/action/show.rb +17 -0
- data/lib/jsonapi/realizer/action/show_spec.rb +22 -16
- data/lib/jsonapi/realizer/action/update.rb +16 -0
- data/lib/jsonapi/realizer/action/update_spec.rb +139 -60
- data/lib/jsonapi/realizer/action_spec.rb +44 -0
- data/lib/jsonapi/realizer/error.rb +15 -0
- data/lib/jsonapi/realizer/error/include_without_data_property.rb +9 -0
- data/lib/jsonapi/realizer/error/invalid_accept_header.rb +9 -0
- data/lib/jsonapi/realizer/error/invalid_content_type_header.rb +9 -0
- data/lib/jsonapi/realizer/error/malformed_data_root_property.rb +9 -0
- data/lib/jsonapi/realizer/error/missing_accept_header.rb +9 -0
- data/lib/jsonapi/realizer/error/missing_content_type_header.rb +9 -0
- data/lib/jsonapi/realizer/error/missing_root_property.rb +9 -0
- data/lib/jsonapi/realizer/error/missing_type_resource_property.rb +9 -0
- data/lib/jsonapi/realizer/error/too_many_root_properties.rb +9 -0
- data/lib/jsonapi/realizer/resource.rb +6 -6
- data/lib/jsonapi/realizer/version.rb +1 -1
- data/lib/jsonapi/realizer_spec.rb +2 -2
- metadata +27 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 161aa73d85429aefb08d235f632784acb2275bd05516394b77053b224acbfbd0
|
4
|
+
data.tar.gz: 33731346eaf5fbd9e8bdba67100208762b3848702d9a761c005bd68548384379
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d2e2a566ec1566f01174e21acea1a9ebe28b6eeddb2795eb7be29b3598e41e6cfe8c9762d662cb460177a3ea4136cc86855da84ad39227b51703c30e4613e881
|
7
|
+
data.tar.gz: 65c70a8803f5e66496cb807ae42571b3a3535272a8e8df7c86d087ce27256446f90ab66cc7ef869b14eff8c70c1c7dc963690a1b716dc338b31f368df7e7f1ce
|
data/README.md
CHANGED
@@ -7,25 +7,6 @@
|
|
7
7
|
|
8
8
|
This library handles incoming [json:api](https://www.jsonapi.org) payloads and turns them, via an adapter system, into data models for your business logic.
|
9
9
|
|
10
|
-
A successful JSON:API request can be annotated as:
|
11
|
-
|
12
|
-
```
|
13
|
-
JSONAPIRequest -> (BusinessLayer -> JSONAPIRequest -> (Record | Array<Record>)) -> JSONAPIResponse
|
14
|
-
```
|
15
|
-
|
16
|
-
The `jsonapi-serializers` library provides this shape:
|
17
|
-
|
18
|
-
```
|
19
|
-
JSONAPIRequest -> (Record | Array<Record>) -> JSONAPIResponse
|
20
|
-
```
|
21
|
-
|
22
|
-
But it leaves fetching/createing/updating/destroying the records up to you! This is where jsonapi-realizer comes into play, as it provides this shape:
|
23
|
-
|
24
|
-
```
|
25
|
-
BusinessLayer -> JSONAPIRequest -> (Record | Array<Record>)
|
26
|
-
```
|
27
|
-
|
28
|
-
|
29
10
|
## Using
|
30
11
|
|
31
12
|
In order to use this library you'll want to have some models:
|
@@ -59,7 +40,7 @@ class ProfileRealizer
|
|
59
40
|
|
60
41
|
register :profiles, class_name: "Profile", adapter: :active_record
|
61
42
|
|
62
|
-
has_many :photos
|
43
|
+
has_many :photos
|
63
44
|
|
64
45
|
has :name
|
65
46
|
end
|
@@ -78,9 +59,6 @@ Once you've designed your resources, we just need to use them! In this example,
|
|
78
59
|
``` ruby
|
79
60
|
class PhotosController < ApplicationController
|
80
61
|
def create
|
81
|
-
validate_parameters!
|
82
|
-
authenticate_session!
|
83
|
-
|
84
62
|
realization = JSONAPI::Realizer.create(params, headers: request.headers)
|
85
63
|
|
86
64
|
ProcessPhotosService.new(realization.model)
|
@@ -89,9 +67,6 @@ class PhotosController < ApplicationController
|
|
89
67
|
end
|
90
68
|
|
91
69
|
def index
|
92
|
-
validate_parameters!
|
93
|
-
authenticate_session!
|
94
|
-
|
95
70
|
realization = JSONAPI::Realizer.index(params, headers: request.headers, type: :photos)
|
96
71
|
|
97
72
|
# See: pundit for `authorize()`
|
@@ -103,6 +78,67 @@ class PhotosController < ApplicationController
|
|
103
78
|
end
|
104
79
|
```
|
105
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
|
+
realization = JSONAPI::Realizer.index(policy(Photo).sanitize(:index, params), headers: request.headers, type: :posts)
|
134
|
+
|
135
|
+
# See: pundit for `policy_scope()`
|
136
|
+
# See: pundit for `authorize()`
|
137
|
+
render json: JSONAPI::Serializer.serialize(authorize(policy_scope(realization.models)), is_collection: true)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
```
|
141
|
+
|
106
142
|
### Adapters
|
107
143
|
|
108
144
|
There are two core adapters:
|
@@ -121,7 +157,7 @@ An adapter must provide the following interfaces:
|
|
121
157
|
0. `includes_via`, describes how to eager include related models
|
122
158
|
0. `sparse_fields_via`, describes how to only return certain fields
|
123
159
|
|
124
|
-
You can also provide custom adapter interfaces
|
160
|
+
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`:
|
125
161
|
|
126
162
|
``` ruby
|
127
163
|
class PhotoRealizer
|
@@ -149,12 +185,32 @@ class PhotoRealizer
|
|
149
185
|
end
|
150
186
|
```
|
151
187
|
|
188
|
+
### Notes
|
189
|
+
|
190
|
+
A successful JSON:API request can be annotated as:
|
191
|
+
|
192
|
+
```
|
193
|
+
JSONAPIRequest -> (BusinessLayer -> JSONAPIRequest -> (Record | Array<Record>)) -> JSONAPIResponse
|
194
|
+
```
|
195
|
+
|
196
|
+
The `jsonapi-serializers` library provides this shape:
|
197
|
+
|
198
|
+
```
|
199
|
+
JSONAPIRequest -> (Record | Array<Record>) -> JSONAPIResponse
|
200
|
+
```
|
201
|
+
|
202
|
+
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:
|
203
|
+
|
204
|
+
```
|
205
|
+
BusinessLayer -> JSONAPIRequest -> (Record | Array<Record>)
|
206
|
+
```
|
207
|
+
|
152
208
|
|
153
209
|
## Installing
|
154
210
|
|
155
211
|
Add this line to your application's Gemfile:
|
156
212
|
|
157
|
-
gem "jsonapi-realizer", "
|
213
|
+
gem "jsonapi-realizer", "3.0.0"
|
158
214
|
|
159
215
|
And then execute:
|
160
216
|
|
@@ -164,6 +220,10 @@ Or install it yourself with:
|
|
164
220
|
|
165
221
|
$ gem install jsonapi-realizer
|
166
222
|
|
223
|
+
### Rails
|
224
|
+
|
225
|
+
There's nothing extremely special about a rails application, but if you want to use jsonapi-realizer in development mode you'll probably want to turn on `eager_loading` (by setting it to `true` in `config/environments/development.rb`) or by adding `app/realizers` to the `eager_load_paths`.
|
226
|
+
|
167
227
|
|
168
228
|
## Contributing
|
169
229
|
|
data/lib/jsonapi/realizer.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
require "ostruct"
|
2
2
|
require "active_support/concern"
|
3
3
|
require "active_support/core_ext/enumerable"
|
4
|
+
require "active_support/core_ext/string"
|
4
5
|
|
5
6
|
module JSONAPI
|
6
7
|
module Realizer
|
7
8
|
require_relative "realizer/version"
|
9
|
+
require_relative "realizer/error"
|
8
10
|
require_relative "realizer/action"
|
9
11
|
require_relative "realizer/adapter"
|
10
12
|
require_relative "realizer/resource"
|
@@ -7,9 +7,14 @@ module JSONAPI
|
|
7
7
|
require_relative "action/index"
|
8
8
|
|
9
9
|
attr_reader :payload
|
10
|
+
attr_reader :headers
|
10
11
|
|
11
|
-
def initialize
|
12
|
-
raise
|
12
|
+
def initialize(payload:, headers:)
|
13
|
+
raise Error::MissingAcceptHeader unless headers.key?("Accept")
|
14
|
+
raise Error::InvalidAcceptHeader unless headers.fetch("Accept") == "application/vnd.api+json"
|
15
|
+
raise Error::TooManyRootProperties if payload.key?("data") && payload.key?("errors")
|
16
|
+
raise Error::IncludeWithoutDataProperty if payload.key?("include") && !payload.key?("data")
|
17
|
+
raise Error::MalformedDataRootProperty unless payload.key?("data") && data.kind_of?(Array) || data.kind_of?(Hash) || data.nil?
|
13
18
|
end
|
14
19
|
|
15
20
|
def call; end
|
@@ -35,7 +40,7 @@ module JSONAPI
|
|
35
40
|
end
|
36
41
|
|
37
42
|
private def relation_after_fields(relation)
|
38
|
-
if
|
43
|
+
if fields.any?
|
39
44
|
resource_class.sparse_fields_call(relation, fields)
|
40
45
|
else
|
41
46
|
relation
|
@@ -51,22 +56,18 @@ module JSONAPI
|
|
51
56
|
end
|
52
57
|
|
53
58
|
private def data
|
54
|
-
payload.fetch("data"
|
55
|
-
end
|
56
|
-
|
57
|
-
private def id
|
58
|
-
data.fetch("id", nil) || payload.fetch("id", nil)
|
59
|
+
payload.fetch("data")
|
59
60
|
end
|
60
61
|
|
61
62
|
private def type
|
62
|
-
|
63
|
+
data["type"].to_s.dasherize if data
|
63
64
|
end
|
64
65
|
|
65
66
|
private def attributes
|
66
67
|
data.
|
67
68
|
fetch("attributes", {}).
|
68
69
|
transform_keys(&:underscore).
|
69
|
-
select(&resource_class.method(:valid_attribute?))
|
70
|
+
select(&resource_class.method(:valid_attribute?)) if data
|
70
71
|
end
|
71
72
|
|
72
73
|
private def relationships
|
@@ -74,7 +75,7 @@ module JSONAPI
|
|
74
75
|
fetch("relationships", {}).
|
75
76
|
transform_keys(&:underscore).
|
76
77
|
select(&resource_class.method(:valid_relationship?)).
|
77
|
-
transform_values(&method(:as_relationship))
|
78
|
+
transform_values(&method(:as_relationship)) if data
|
78
79
|
end
|
79
80
|
|
80
81
|
private def as_relationship(value)
|
@@ -86,24 +87,54 @@ module JSONAPI
|
|
86
87
|
)
|
87
88
|
end
|
88
89
|
|
89
|
-
|
90
|
+
def includes
|
91
|
+
return [] unless payload.key?("include")
|
92
|
+
|
90
93
|
payload.
|
91
|
-
fetch("include"
|
94
|
+
fetch("include").
|
92
95
|
# "carts.cart-items,carts.cart-items.product,carts.billing-information,payments"
|
93
|
-
|
96
|
+
split(/\s*,\s*/).
|
94
97
|
# ["carts.cart-items", "carts.cart-items.product", "carts.billing-information", "payments"]
|
95
98
|
map { |path| path.gsub("-", "_") }.
|
96
99
|
# ["carts.cart_items", "carts.cart_items.product", "carts.billing_information", "payments"]
|
97
100
|
map { |path| path.split(".") }.
|
98
101
|
# [["carts", "cart_items"], ["carts", "cart_items", "product"], ["carts", "billing_information"], ["payments"]]
|
99
|
-
select
|
102
|
+
select do |chain|
|
103
|
+
# ["carts", "cart_items"]
|
104
|
+
chain.reduce(resource_class) do |last_resource_class, key|
|
105
|
+
break unless last_resource_class
|
106
|
+
|
107
|
+
JSONAPI::Realizer.type_mapping.fetch(last_resource_class.relationship(key).as).resource_class if last_resource_class.valid_includes?(key)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
# [["carts", "cart_items", "product"], ["payments"]]
|
100
111
|
end
|
101
112
|
|
102
|
-
|
113
|
+
def fields
|
114
|
+
return [] unless payload.key?("fields")
|
115
|
+
|
103
116
|
payload.
|
104
|
-
fetch("fields"
|
117
|
+
fetch("fields").
|
118
|
+
# "title,active-photographer.email,active-photographer.posts.title"
|
105
119
|
split(/\s*,\s*/).
|
106
|
-
|
120
|
+
# ["title", "active-photographer.email", "active-photographer.posts.title"]
|
121
|
+
map { |path| path.gsub("-", "_") }.
|
122
|
+
# ["title", "active_photographer.email", "active_photographer.posts.title"]
|
123
|
+
map { |path| path.split(".") }.
|
124
|
+
# [["title"], ["active_photographer", "email"], ["active_photographer", "posts", "title"]]
|
125
|
+
select do |chain|
|
126
|
+
# ["active_photographer", "email"]
|
127
|
+
chain.reduce(resource_class) do |last_resource_class, key|
|
128
|
+
break unless last_resource_class
|
129
|
+
|
130
|
+
if last_resource_class.valid_includes?(key)
|
131
|
+
JSONAPI::Realizer.type_mapping.fetch(last_resource_class.relationship(key).as).resource_class
|
132
|
+
elsif last_resource_class.valid_sparse_field?(key)
|
133
|
+
last_resource_class
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
# [["title"], ["active_photographer", "email"]]
|
107
138
|
end
|
108
139
|
|
109
140
|
private def configuration
|
@@ -7,7 +7,17 @@ module JSONAPI
|
|
7
7
|
def initialize(payload:, headers:)
|
8
8
|
@payload = payload
|
9
9
|
@headers = headers
|
10
|
+
|
11
|
+
raise Error::MissingContentTypeHeader unless headers.key?("Content-Type")
|
12
|
+
raise Error::InvalidContentTypeHeader unless headers.fetch("Content-Type") == "application/vnd.api+json"
|
13
|
+
|
14
|
+
super(payload: payload, headers: headers)
|
15
|
+
|
10
16
|
@resource = resource_class.new(relation.new)
|
17
|
+
|
18
|
+
raise Error::MissingRootProperty unless payload.key?("data") || payload.key?("errors") || payload.key?("meta")
|
19
|
+
raise Error::MissingTypeResourceProperty if payload.key?("data") && data.kind_of?(Hash) && !data.key?("type")
|
20
|
+
raise Error::MissingTypeResourceProperty if payload.key?("data") && data.kind_of?(Array) && !data.all? {|resource| resource.key?("type")}
|
11
21
|
end
|
12
22
|
|
13
23
|
def call
|
@@ -20,6 +30,10 @@ module JSONAPI
|
|
20
30
|
def model
|
21
31
|
resource.model
|
22
32
|
end
|
33
|
+
|
34
|
+
private def id
|
35
|
+
payload.fetch("id", nil)
|
36
|
+
end
|
23
37
|
end
|
24
38
|
end
|
25
39
|
end
|
@@ -23,7 +23,7 @@ RSpec.describe JSONAPI::Realizer::Action::Create do
|
|
23
23
|
},
|
24
24
|
"relationships" => {
|
25
25
|
"active-photographer" => {
|
26
|
-
"data" => { "type" => "photographer-
|
26
|
+
"data" => { "type" => "photographer-accounts", "id" => "4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9" }
|
27
27
|
}
|
28
28
|
}
|
29
29
|
}
|
@@ -37,72 +37,78 @@ RSpec.describe JSONAPI::Realizer::Action::Create do
|
|
37
37
|
}
|
38
38
|
end
|
39
39
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
name: "Kurtis Rainbolt-Greene"
|
44
|
-
}
|
45
|
-
end
|
46
|
-
|
47
|
-
it "is the right model" do
|
48
|
-
subject
|
40
|
+
shared_examples "api" do
|
41
|
+
it "is the right model" do
|
42
|
+
subject
|
49
43
|
|
50
|
-
|
51
|
-
|
44
|
+
expect(action.model).to be_a_kind_of(Photo)
|
45
|
+
end
|
52
46
|
|
53
|
-
|
54
|
-
|
47
|
+
it "assigns the id attribute" do
|
48
|
+
subject
|
55
49
|
|
56
|
-
|
57
|
-
|
50
|
+
expect(action.model).to have_attributes(id: "550e8400-e29b-41d4-a716-446655440000")
|
51
|
+
end
|
58
52
|
|
59
|
-
|
60
|
-
|
53
|
+
it "assigns the title attribute" do
|
54
|
+
subject
|
61
55
|
|
62
|
-
|
63
|
-
|
56
|
+
expect(action.model).to have_attributes(title: "Ember Hamster")
|
57
|
+
end
|
64
58
|
|
65
|
-
|
66
|
-
|
59
|
+
it "assigns the alt_text attribute" do
|
60
|
+
subject
|
67
61
|
|
68
|
-
|
69
|
-
|
62
|
+
expect(action.model).to have_attributes(alt_text: "A hamster logo.")
|
63
|
+
end
|
70
64
|
|
71
|
-
|
72
|
-
|
65
|
+
it "assigns the src attribute" do
|
66
|
+
subject
|
73
67
|
|
74
|
-
|
75
|
-
|
68
|
+
expect(action.model).to have_attributes(src: "http://example.com/images/productivity.png")
|
69
|
+
end
|
76
70
|
|
77
|
-
|
78
|
-
|
71
|
+
it "assigns the updated_at attribute" do
|
72
|
+
subject
|
79
73
|
|
80
|
-
|
81
|
-
|
74
|
+
expect(action.model).to have_attributes(updated_at: a_kind_of(Time))
|
75
|
+
end
|
82
76
|
|
83
|
-
|
84
|
-
|
77
|
+
it "assigns the active_photographer attribute" do
|
78
|
+
subject
|
85
79
|
|
86
|
-
|
80
|
+
expect(action.model).to have_attributes(active_photographer: a_kind_of(Account))
|
81
|
+
end
|
87
82
|
end
|
88
83
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
}.from(
|
95
|
-
{}
|
96
|
-
).to(
|
97
|
-
{
|
98
|
-
"550e8400-e29b-41d4-a716-446655440000" => hash_including(
|
99
|
-
id: "550e8400-e29b-41d4-a716-446655440000",
|
100
|
-
title: "Ember Hamster",
|
101
|
-
alt_text: "A hamster logo.",
|
102
|
-
src: "http://example.com/images/productivity.png"
|
103
|
-
)
|
84
|
+
context "in a memory store", memory: true do
|
85
|
+
before do
|
86
|
+
Account::STORE["4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9"] = {
|
87
|
+
id: "4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9",
|
88
|
+
name: "Kurtis Rainbolt-Greene"
|
104
89
|
}
|
105
|
-
|
90
|
+
end
|
91
|
+
|
92
|
+
include_examples "api"
|
93
|
+
|
94
|
+
it "creates the new record" do
|
95
|
+
expect {
|
96
|
+
subject
|
97
|
+
}.to change {
|
98
|
+
Photo::STORE
|
99
|
+
}.from(
|
100
|
+
{}
|
101
|
+
).to(
|
102
|
+
{
|
103
|
+
"550e8400-e29b-41d4-a716-446655440000" => hash_including(
|
104
|
+
id: "550e8400-e29b-41d4-a716-446655440000",
|
105
|
+
title: "Ember Hamster",
|
106
|
+
alt_text: "A hamster logo.",
|
107
|
+
src: "http://example.com/images/productivity.png"
|
108
|
+
)
|
109
|
+
}
|
110
|
+
)
|
111
|
+
end
|
106
112
|
end
|
107
113
|
end
|
108
114
|
end
|
@@ -8,12 +8,23 @@ module JSONAPI
|
|
8
8
|
@payload = payload
|
9
9
|
@headers = headers
|
10
10
|
@type = type
|
11
|
+
|
12
|
+
super(payload: payload, headers: headers)
|
13
|
+
|
11
14
|
@resources = adapter.find_many_via_call(relation).map(&resource_class.method(:new))
|
12
15
|
end
|
13
16
|
|
14
17
|
def models
|
15
18
|
resources.map(&:model)
|
16
19
|
end
|
20
|
+
|
21
|
+
private def data
|
22
|
+
payload["data"]
|
23
|
+
end
|
24
|
+
|
25
|
+
private def type
|
26
|
+
@type.to_s.dasherize if @type
|
27
|
+
end
|
17
28
|
end
|
18
29
|
end
|
19
30
|
end
|
@@ -21,21 +21,27 @@ RSpec.describe JSONAPI::Realizer::Action::Index do
|
|
21
21
|
}
|
22
22
|
end
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
src: "http://example.com/images/productivity.png"
|
29
|
-
}
|
30
|
-
Photo::STORE["d09ae4c6-0fc3-4c42-8fe8-6029530c3bed"] = {
|
31
|
-
id: "d09ae4c6-0fc3-4c42-8fe8-6029530c3bed",
|
32
|
-
title: "Ember Fox",
|
33
|
-
src: "http://example.com/images/productivity-2.png"
|
34
|
-
}
|
24
|
+
shared_examples "api" do
|
25
|
+
it "returns a list of photos" do
|
26
|
+
expect(subject).to include(a_kind_of(Photo), a_kind_of(Photo))
|
27
|
+
end
|
35
28
|
end
|
36
29
|
|
37
|
-
|
38
|
-
|
30
|
+
context "in a memory store", memory: true do
|
31
|
+
before do
|
32
|
+
Photo::STORE["550e8400-e29b-41d4-a716-446655440000"] = {
|
33
|
+
id: "550e8400-e29b-41d4-a716-446655440000",
|
34
|
+
title: "Ember Hamster",
|
35
|
+
src: "http://example.com/images/productivity.png"
|
36
|
+
}
|
37
|
+
Photo::STORE["d09ae4c6-0fc3-4c42-8fe8-6029530c3bed"] = {
|
38
|
+
id: "d09ae4c6-0fc3-4c42-8fe8-6029530c3bed",
|
39
|
+
title: "Ember Fox",
|
40
|
+
src: "http://example.com/images/productivity-2.png"
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
include_examples "api"
|
39
45
|
end
|
40
46
|
end
|
41
47
|
end
|
@@ -9,12 +9,29 @@ module JSONAPI
|
|
9
9
|
@payload = payload
|
10
10
|
@headers = headers
|
11
11
|
@type = type
|
12
|
+
|
13
|
+
super(payload: payload, headers: headers)
|
14
|
+
|
12
15
|
@resource = resource_class.new(adapter.find_via_call(relation, id))
|
13
16
|
end
|
14
17
|
|
15
18
|
def model
|
16
19
|
resource.model
|
17
20
|
end
|
21
|
+
|
22
|
+
private def data
|
23
|
+
payload["data"]
|
24
|
+
end
|
25
|
+
|
26
|
+
private def type
|
27
|
+
@type.to_s.dasherize if @type
|
28
|
+
end
|
29
|
+
|
30
|
+
private def id
|
31
|
+
return data.fetch("id", nil) if data
|
32
|
+
|
33
|
+
payload.fetch("id", nil)
|
34
|
+
end
|
18
35
|
end
|
19
36
|
end
|
20
37
|
end
|
@@ -23,25 +23,31 @@ RSpec.describe JSONAPI::Realizer::Action::Show do
|
|
23
23
|
}
|
24
24
|
end
|
25
25
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
src: "http://example.com/images/productivity.png"
|
31
|
-
}
|
32
|
-
Photo::STORE["d09ae4c6-0fc3-4c42-8fe8-6029530c3bed"] = {
|
33
|
-
id: "d09ae4c6-0fc3-4c42-8fe8-6029530c3bed",
|
34
|
-
title: "Ember Fox",
|
35
|
-
src: "http://example.com/images/productivity-2.png"
|
36
|
-
}
|
37
|
-
end
|
26
|
+
shared_examples "api" do
|
27
|
+
it "returns a photo model" do
|
28
|
+
expect(subject).to be_a_kind_of(Photo)
|
29
|
+
end
|
38
30
|
|
39
|
-
|
40
|
-
|
31
|
+
it "returns the photos attributes" do
|
32
|
+
expect(subject).to have_attributes(title: "Ember Fox", src: "http://example.com/images/productivity-2.png")
|
33
|
+
end
|
41
34
|
end
|
42
35
|
|
43
|
-
|
44
|
-
|
36
|
+
context "in a memory store", memory: true do
|
37
|
+
before do
|
38
|
+
Photo::STORE["550e8400-e29b-41d4-a716-446655440000"] = {
|
39
|
+
id: "550e8400-e29b-41d4-a716-446655440000",
|
40
|
+
title: "Ember Hamster",
|
41
|
+
src: "http://example.com/images/productivity.png"
|
42
|
+
}
|
43
|
+
Photo::STORE["d09ae4c6-0fc3-4c42-8fe8-6029530c3bed"] = {
|
44
|
+
id: "d09ae4c6-0fc3-4c42-8fe8-6029530c3bed",
|
45
|
+
title: "Ember Fox",
|
46
|
+
src: "http://example.com/images/productivity-2.png"
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
include_examples "api"
|
45
51
|
end
|
46
52
|
end
|
47
53
|
end
|
@@ -7,7 +7,17 @@ module JSONAPI
|
|
7
7
|
def initialize(payload:, headers:)
|
8
8
|
@payload = payload
|
9
9
|
@headers = headers
|
10
|
+
|
11
|
+
raise Error::MissingContentTypeHeader unless headers.key?("Content-Type")
|
12
|
+
raise Error::InvalidContentTypeHeader unless headers.fetch("Content-Type") == "application/vnd.api+json"
|
13
|
+
|
14
|
+
super(payload: payload, headers: headers)
|
15
|
+
|
10
16
|
@resource = resource_class.new(adapter.find_via_call(relation, id))
|
17
|
+
|
18
|
+
raise Error::MissingRootProperty unless payload.key?("data") || payload.key?("errors") || payload.key?("meta")
|
19
|
+
raise Error::MissingTypeResourceProperty if payload.key?("data") && data.kind_of?(Hash) && !data.key?("type")
|
20
|
+
raise Error::MissingTypeResourceProperty if payload.key?("data") && data.kind_of?(Array) && !data.all? {|resource| resource.key?("type")}
|
11
21
|
end
|
12
22
|
|
13
23
|
def call
|
@@ -19,6 +29,12 @@ module JSONAPI
|
|
19
29
|
def model
|
20
30
|
resource.model
|
21
31
|
end
|
32
|
+
|
33
|
+
private def id
|
34
|
+
return data.fetch("id", nil) if data
|
35
|
+
|
36
|
+
payload.fetch("id", nil)
|
37
|
+
end
|
22
38
|
end
|
23
39
|
end
|
24
40
|
end
|
@@ -6,9 +6,82 @@ RSpec.describe JSONAPI::Realizer::Action::Update do
|
|
6
6
|
describe "#call" do
|
7
7
|
subject { action.call }
|
8
8
|
|
9
|
-
context "with no top-level data and
|
10
|
-
|
11
|
-
|
9
|
+
context "with no top-level data and no content-type header no accept headers" do
|
10
|
+
let(:payload) do
|
11
|
+
{}
|
12
|
+
end
|
13
|
+
let(:headers) do
|
14
|
+
{}
|
15
|
+
end
|
16
|
+
|
17
|
+
it "raises an exception" do
|
18
|
+
expect {subject}.to raise_exception(JSONAPI::Realizer::Error::MissingContentTypeHeader)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context "with no top-level data and good content-type header no accept headers" do
|
23
|
+
let(:payload) do
|
24
|
+
{}
|
25
|
+
end
|
26
|
+
let(:headers) do
|
27
|
+
{
|
28
|
+
"Content-Type" => "application/vnd.api+json",
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
it "raises an exception" do
|
33
|
+
expect {subject}.to raise_exception(JSONAPI::Realizer::Error::MissingAcceptHeader)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "with no top-level data and wrong content-type header" do
|
38
|
+
let(:payload) do
|
39
|
+
{}
|
40
|
+
end
|
41
|
+
let(:headers) do
|
42
|
+
{
|
43
|
+
"Content-Type" => "application/json"
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
it "raises an exception" do
|
48
|
+
expect {subject}.to raise_exception(JSONAPI::Realizer::Error::InvalidContentTypeHeader)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context "with no top-level data and good content-type header and wrong accept header" do
|
53
|
+
let(:payload) do
|
54
|
+
{}
|
55
|
+
end
|
56
|
+
let(:headers) do
|
57
|
+
{
|
58
|
+
"Content-Type" => "application/vnd.api+json",
|
59
|
+
"Accept" => "application/json"
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
it "raises an exception" do
|
64
|
+
expect {subject}.to raise_exception(JSONAPI::Realizer::Error::InvalidAcceptHeader)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context "with wrong top-level data and good headers" do
|
69
|
+
let(:payload) do
|
70
|
+
{
|
71
|
+
"data" => ""
|
72
|
+
}
|
73
|
+
end
|
74
|
+
let(:headers) do
|
75
|
+
{
|
76
|
+
"Content-Type" => "application/vnd.api+json",
|
77
|
+
"Accept" => "application/vnd.api+json"
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
it "raises an exception" do
|
82
|
+
expect {subject}.to raise_exception(JSONAPI::Realizer::Error::MalformedDataRootProperty)
|
83
|
+
end
|
84
|
+
end
|
12
85
|
|
13
86
|
context "with a good payload and good headers" do
|
14
87
|
let(:payload) do
|
@@ -23,7 +96,7 @@ RSpec.describe JSONAPI::Realizer::Action::Update do
|
|
23
96
|
},
|
24
97
|
"relationships" => {
|
25
98
|
"active-photographer" => {
|
26
|
-
"data" => { "type" => "photographer-
|
99
|
+
"data" => { "type" => "photographer-accounts", "id" => "4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9" }
|
27
100
|
}
|
28
101
|
}
|
29
102
|
}
|
@@ -36,77 +109,83 @@ RSpec.describe JSONAPI::Realizer::Action::Update do
|
|
36
109
|
}
|
37
110
|
end
|
38
111
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
name: "Kurtis Rainbolt-Greene"
|
43
|
-
}
|
44
|
-
Photo::STORE["550e8400-e29b-41d4-a716-446655440000"] = {
|
45
|
-
id: "550e8400-e29b-41d4-a716-446655440000",
|
46
|
-
title: "Ember Hamster",
|
47
|
-
src: "http://example.com/images/productivity.png"
|
48
|
-
}
|
49
|
-
end
|
50
|
-
|
51
|
-
it "is the right model" do
|
52
|
-
subject
|
112
|
+
shared_examples "api" do
|
113
|
+
it "is the right model" do
|
114
|
+
subject
|
53
115
|
|
54
|
-
|
55
|
-
|
116
|
+
expect(action.model).to be_a_kind_of(Photo)
|
117
|
+
end
|
56
118
|
|
57
|
-
|
58
|
-
|
119
|
+
it "assigns the title attribute" do
|
120
|
+
subject
|
59
121
|
|
60
|
-
|
61
|
-
|
122
|
+
expect(action.model).to have_attributes(title: "Ember Hamster 2")
|
123
|
+
end
|
62
124
|
|
63
|
-
|
64
|
-
|
125
|
+
it "assigns the alt_text attribute" do
|
126
|
+
subject
|
65
127
|
|
66
|
-
|
67
|
-
|
128
|
+
expect(action.model).to have_attributes(alt_text: "A hamster logo.")
|
129
|
+
end
|
68
130
|
|
69
|
-
|
70
|
-
|
131
|
+
it "assigns the src attribute" do
|
132
|
+
subject
|
71
133
|
|
72
|
-
|
73
|
-
|
134
|
+
expect(action.model).to have_attributes(src: "http://example.com/images/productivity-2.png")
|
135
|
+
end
|
74
136
|
|
75
|
-
|
76
|
-
|
137
|
+
it "assigns the updated_at attribute" do
|
138
|
+
subject
|
77
139
|
|
78
|
-
|
79
|
-
|
140
|
+
expect(action.model).to have_attributes(updated_at: a_kind_of(Time))
|
141
|
+
end
|
80
142
|
|
81
|
-
|
82
|
-
|
143
|
+
it "assigns the active_photographer attribute" do
|
144
|
+
subject
|
83
145
|
|
84
|
-
|
146
|
+
expect(action.model).to have_attributes(active_photographer: a_kind_of(Account))
|
147
|
+
end
|
85
148
|
end
|
86
149
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
}.from(
|
93
|
-
{
|
94
|
-
"550e8400-e29b-41d4-a716-446655440000" => hash_including(
|
95
|
-
id: "550e8400-e29b-41d4-a716-446655440000",
|
96
|
-
title: "Ember Hamster",
|
97
|
-
src: "http://example.com/images/productivity.png"
|
98
|
-
)
|
150
|
+
context "in a memory store", memory: true do
|
151
|
+
before do
|
152
|
+
Account::STORE["4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9"] = {
|
153
|
+
id: "4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9",
|
154
|
+
name: "Kurtis Rainbolt-Greene"
|
99
155
|
}
|
100
|
-
|
101
|
-
|
102
|
-
"
|
103
|
-
|
104
|
-
title: "Ember Hamster 2",
|
105
|
-
alt_text: "A hamster logo.",
|
106
|
-
src: "http://example.com/images/productivity-2.png"
|
107
|
-
)
|
156
|
+
Photo::STORE["550e8400-e29b-41d4-a716-446655440000"] = {
|
157
|
+
id: "550e8400-e29b-41d4-a716-446655440000",
|
158
|
+
title: "Ember Hamster",
|
159
|
+
src: "http://example.com/images/productivity.png"
|
108
160
|
}
|
109
|
-
|
161
|
+
end
|
162
|
+
|
163
|
+
include_examples "api"
|
164
|
+
|
165
|
+
it "updates the record" do
|
166
|
+
expect {
|
167
|
+
subject
|
168
|
+
}.to change {
|
169
|
+
Photo::STORE
|
170
|
+
}.from(
|
171
|
+
{
|
172
|
+
"550e8400-e29b-41d4-a716-446655440000" => hash_including(
|
173
|
+
id: "550e8400-e29b-41d4-a716-446655440000",
|
174
|
+
title: "Ember Hamster",
|
175
|
+
src: "http://example.com/images/productivity.png"
|
176
|
+
)
|
177
|
+
}
|
178
|
+
).to(
|
179
|
+
{
|
180
|
+
"550e8400-e29b-41d4-a716-446655440000" => hash_including(
|
181
|
+
id: "550e8400-e29b-41d4-a716-446655440000",
|
182
|
+
title: "Ember Hamster 2",
|
183
|
+
alt_text: "A hamster logo.",
|
184
|
+
src: "http://example.com/images/productivity-2.png"
|
185
|
+
)
|
186
|
+
}
|
187
|
+
)
|
188
|
+
end
|
110
189
|
end
|
111
190
|
end
|
112
191
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
RSpec.describe JSONAPI::Realizer::Action do
|
4
|
+
let(:action) do
|
5
|
+
Class.new(described_class) do
|
6
|
+
def initialize(payload:, type:)
|
7
|
+
@payload = payload
|
8
|
+
@type = type
|
9
|
+
end
|
10
|
+
end.new(payload: payload, type: :photos)
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "#includes" do
|
14
|
+
subject { action.includes }
|
15
|
+
|
16
|
+
context "with a two good and one bad" do
|
17
|
+
let(:payload) do
|
18
|
+
{
|
19
|
+
"include" => "active_photographer,active_photographer.posts.comments,active_photographer.posts"
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
it "contains only the two good" do
|
24
|
+
expect(subject).to eq([["active_photographer"], ["active_photographer", "posts"]])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#fields" do
|
30
|
+
subject { action.fields }
|
31
|
+
|
32
|
+
context "with a two good and one bad" do
|
33
|
+
let(:payload) do
|
34
|
+
{
|
35
|
+
"fields" => "title,active_photographer.posts.comments.body,active_photographer.name"
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
it "contains only the two good" do
|
40
|
+
expect(subject).to eq([["title"], ["active_photographer", "name"]])
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
module Realizer
|
3
|
+
class Error < StandardError
|
4
|
+
require_relative "error/include_without_data_property"
|
5
|
+
require_relative "error/invalid_accept_header"
|
6
|
+
require_relative "error/invalid_content_type_header"
|
7
|
+
require_relative "error/malformed_data_root_property"
|
8
|
+
require_relative "error/missing_accept_header"
|
9
|
+
require_relative "error/missing_content_type_header"
|
10
|
+
require_relative "error/missing_root_property"
|
11
|
+
require_relative "error/missing_type_resource_property"
|
12
|
+
require_relative "error/too_many_root_properties"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -51,11 +51,11 @@ module JSONAPI
|
|
51
51
|
end
|
52
52
|
|
53
53
|
def valid_sparse_field?(name)
|
54
|
-
attribute(name).
|
54
|
+
attribute(name).selectable if attribute(name)
|
55
55
|
end
|
56
56
|
|
57
57
|
def valid_includes?(name)
|
58
|
-
relationship(name).
|
58
|
+
relationship(name).includable if relationship(name)
|
59
59
|
end
|
60
60
|
|
61
61
|
def has(name, selectable: true)
|
@@ -66,12 +66,12 @@ module JSONAPI
|
|
66
66
|
relationships.public_send("#{name}=", OpenStruct.new({name: name, as: as, includable: includable}))
|
67
67
|
end
|
68
68
|
|
69
|
-
def has_one(name, as: name, includable: true)
|
70
|
-
has_related(name, as:
|
69
|
+
def has_one(name, as: name.to_s.pluralize.dasherize, includable: true)
|
70
|
+
has_related(name, as: as.to_s.dasherize, includable: includable)
|
71
71
|
end
|
72
72
|
|
73
|
-
def has_many(name, as: name, includable: true)
|
74
|
-
has_related(name, as:
|
73
|
+
def has_many(name, as: name.to_s.dasherize, includable: true)
|
74
|
+
has_related(name, as: as.to_s.dasherize, includable: includable)
|
75
75
|
end
|
76
76
|
|
77
77
|
def adapter
|
@@ -98,7 +98,7 @@ RSpec.describe JSONAPI::Realizer do
|
|
98
98
|
},
|
99
99
|
"relationships" => {
|
100
100
|
"active-photographer" => {
|
101
|
-
"data" => { "type" => "photographer-
|
101
|
+
"data" => { "type" => "photographer-accounts", "id" => "4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9" }
|
102
102
|
}
|
103
103
|
}
|
104
104
|
}
|
@@ -112,7 +112,7 @@ RSpec.describe JSONAPI::Realizer do
|
|
112
112
|
end
|
113
113
|
|
114
114
|
before do
|
115
|
-
|
115
|
+
Account::STORE["4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9"] = {
|
116
116
|
id: "4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9",
|
117
117
|
name: "Kurtis Rainbolt-Greene"
|
118
118
|
}
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jsonapi-realizer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kurtis Rainbolt-Greene
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-03-
|
11
|
+
date: 2018-03-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '5.1'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: activerecord
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '5.1'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '5.1'
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
98
|
name: pry-doc
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -128,10 +142,21 @@ files:
|
|
128
142
|
- lib/jsonapi/realizer/action/show_spec.rb
|
129
143
|
- lib/jsonapi/realizer/action/update.rb
|
130
144
|
- lib/jsonapi/realizer/action/update_spec.rb
|
145
|
+
- lib/jsonapi/realizer/action_spec.rb
|
131
146
|
- lib/jsonapi/realizer/adapter.rb
|
132
147
|
- lib/jsonapi/realizer/adapter/active_record.rb
|
133
148
|
- lib/jsonapi/realizer/adapter/memory.rb
|
134
149
|
- lib/jsonapi/realizer/adapter_spec.rb
|
150
|
+
- lib/jsonapi/realizer/error.rb
|
151
|
+
- lib/jsonapi/realizer/error/include_without_data_property.rb
|
152
|
+
- lib/jsonapi/realizer/error/invalid_accept_header.rb
|
153
|
+
- lib/jsonapi/realizer/error/invalid_content_type_header.rb
|
154
|
+
- lib/jsonapi/realizer/error/malformed_data_root_property.rb
|
155
|
+
- lib/jsonapi/realizer/error/missing_accept_header.rb
|
156
|
+
- lib/jsonapi/realizer/error/missing_content_type_header.rb
|
157
|
+
- lib/jsonapi/realizer/error/missing_root_property.rb
|
158
|
+
- lib/jsonapi/realizer/error/missing_type_resource_property.rb
|
159
|
+
- lib/jsonapi/realizer/error/too_many_root_properties.rb
|
135
160
|
- lib/jsonapi/realizer/resource.rb
|
136
161
|
- lib/jsonapi/realizer/version.rb
|
137
162
|
- lib/jsonapi/realizer/version_spec.rb
|