jsonapi-realizer 2.0.3 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02f40c79b4a4ecf49431c220d161c065737e744f420250dec013011c11647af5
4
- data.tar.gz: 923505a0c13a519bf0db6aa7b2ce260884a9bfd309476987e32e802f0affc0bc
3
+ metadata.gz: 161aa73d85429aefb08d235f632784acb2275bd05516394b77053b224acbfbd0
4
+ data.tar.gz: 33731346eaf5fbd9e8bdba67100208762b3848702d9a761c005bd68548384379
5
5
  SHA512:
6
- metadata.gz: d6c3c50ddb0321258311a199b4e2c4eb7946ec35981865696662cad16563d40bbdb8d745f9e215cfb5fdc894fa54897410cf3bf347ac7eeb14015271a74f38e5
7
- data.tar.gz: 710585e9ed18604598d2eb52ac48e935ae3cfb85f462b84284fee06a2d660f2df3b6896a33ce3ba86a063843de28b5ea07c18e132666cda3e511e841fef9fcac
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, as: :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", "2.0.0"
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
 
@@ -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 NoMethodError, "must implement this function"
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 includes.any?
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
- (@type || data.fetch("type")).to_s.dasherize
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
- private def includes
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
- map { |path| path.split(/\s*,\s*/) }.
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(&resource_class.method(:valid_includes?))
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
- private def fields
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
- select(&resource_class.method(:valid_sparse_field?))
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-people", "id" => "4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9" }
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
- before do
41
- People::STORE["4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9"] = {
42
- id: "4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9",
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
- expect(action.model).to be_a_kind_of(Photo)
51
- end
44
+ expect(action.model).to be_a_kind_of(Photo)
45
+ end
52
46
 
53
- it "assigns the id attribute" do
54
- subject
47
+ it "assigns the id attribute" do
48
+ subject
55
49
 
56
- expect(action.model).to have_attributes(id: "550e8400-e29b-41d4-a716-446655440000")
57
- end
50
+ expect(action.model).to have_attributes(id: "550e8400-e29b-41d4-a716-446655440000")
51
+ end
58
52
 
59
- it "assigns the title attribute" do
60
- subject
53
+ it "assigns the title attribute" do
54
+ subject
61
55
 
62
- expect(action.model).to have_attributes(title: "Ember Hamster")
63
- end
56
+ expect(action.model).to have_attributes(title: "Ember Hamster")
57
+ end
64
58
 
65
- it "assigns the alt_text attribute" do
66
- subject
59
+ it "assigns the alt_text attribute" do
60
+ subject
67
61
 
68
- expect(action.model).to have_attributes(alt_text: "A hamster logo.")
69
- end
62
+ expect(action.model).to have_attributes(alt_text: "A hamster logo.")
63
+ end
70
64
 
71
- it "assigns the src attribute" do
72
- subject
65
+ it "assigns the src attribute" do
66
+ subject
73
67
 
74
- expect(action.model).to have_attributes(src: "http://example.com/images/productivity.png")
75
- end
68
+ expect(action.model).to have_attributes(src: "http://example.com/images/productivity.png")
69
+ end
76
70
 
77
- it "assigns the updated_at attribute" do
78
- subject
71
+ it "assigns the updated_at attribute" do
72
+ subject
79
73
 
80
- expect(action.model).to have_attributes(updated_at: a_kind_of(Time))
81
- end
74
+ expect(action.model).to have_attributes(updated_at: a_kind_of(Time))
75
+ end
82
76
 
83
- it "assigns the active_photographer attribute" do
84
- subject
77
+ it "assigns the active_photographer attribute" do
78
+ subject
85
79
 
86
- expect(action.model).to have_attributes(active_photographer: a_kind_of(People))
80
+ expect(action.model).to have_attributes(active_photographer: a_kind_of(Account))
81
+ end
87
82
  end
88
83
 
89
- it "creates the new record" do
90
- expect {
91
- subject
92
- }.to change {
93
- Photo::STORE
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
- before do
25
- Photo::STORE["550e8400-e29b-41d4-a716-446655440000"] = {
26
- id: "550e8400-e29b-41d4-a716-446655440000",
27
- title: "Ember Hamster",
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
- it "returns a list of photos" do
38
- expect(subject).to include(a_kind_of(Photo), a_kind_of(Photo))
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
- before do
27
- Photo::STORE["550e8400-e29b-41d4-a716-446655440000"] = {
28
- id: "550e8400-e29b-41d4-a716-446655440000",
29
- title: "Ember Hamster",
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
- it "returns a photo model" do
40
- expect(subject).to be_a_kind_of(Photo)
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
- it "returns the photos attributes" do
44
- expect(subject).to have_attributes(title: "Ember Fox", src: "http://example.com/images/productivity-2.png")
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 good headers"
10
- context "with no top-level data and bad headers"
11
- context "with a good payload and bad headers"
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-people", "id" => "4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9" }
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
- before do
40
- People::STORE["4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9"] = {
41
- id: "4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9",
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
- expect(action.model).to be_a_kind_of(Photo)
55
- end
116
+ expect(action.model).to be_a_kind_of(Photo)
117
+ end
56
118
 
57
- it "assigns the title attribute" do
58
- subject
119
+ it "assigns the title attribute" do
120
+ subject
59
121
 
60
- expect(action.model).to have_attributes(title: "Ember Hamster 2")
61
- end
122
+ expect(action.model).to have_attributes(title: "Ember Hamster 2")
123
+ end
62
124
 
63
- it "assigns the alt_text attribute" do
64
- subject
125
+ it "assigns the alt_text attribute" do
126
+ subject
65
127
 
66
- expect(action.model).to have_attributes(alt_text: "A hamster logo.")
67
- end
128
+ expect(action.model).to have_attributes(alt_text: "A hamster logo.")
129
+ end
68
130
 
69
- it "assigns the src attribute" do
70
- subject
131
+ it "assigns the src attribute" do
132
+ subject
71
133
 
72
- expect(action.model).to have_attributes(src: "http://example.com/images/productivity-2.png")
73
- end
134
+ expect(action.model).to have_attributes(src: "http://example.com/images/productivity-2.png")
135
+ end
74
136
 
75
- it "assigns the updated_at attribute" do
76
- subject
137
+ it "assigns the updated_at attribute" do
138
+ subject
77
139
 
78
- expect(action.model).to have_attributes(updated_at: a_kind_of(Time))
79
- end
140
+ expect(action.model).to have_attributes(updated_at: a_kind_of(Time))
141
+ end
80
142
 
81
- it "assigns the active_photographer attribute" do
82
- subject
143
+ it "assigns the active_photographer attribute" do
144
+ subject
83
145
 
84
- expect(action.model).to have_attributes(active_photographer: a_kind_of(People))
146
+ expect(action.model).to have_attributes(active_photographer: a_kind_of(Account))
147
+ end
85
148
  end
86
149
 
87
- it "updates the record" do
88
- expect {
89
- subject
90
- }.to change {
91
- Photo::STORE
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
- ).to(
101
- {
102
- "550e8400-e29b-41d4-a716-446655440000" => hash_including(
103
- id: "550e8400-e29b-41d4-a716-446655440000",
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
@@ -0,0 +1,9 @@
1
+ module JSONAPI
2
+ module Realizer
3
+ class Error
4
+ class IncludeWithoutDataProperty < Error
5
+
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module JSONAPI
2
+ module Realizer
3
+ class Error
4
+ class InvalidAcceptHeader < Error
5
+
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module JSONAPI
2
+ module Realizer
3
+ class Error
4
+ class InvalidContentTypeHeader < Error
5
+
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module JSONAPI
2
+ module Realizer
3
+ class Error
4
+ class MalformedDataRootProperty < Error
5
+
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module JSONAPI
2
+ module Realizer
3
+ class Error
4
+ class MissingAcceptHeader < Error
5
+
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module JSONAPI
2
+ module Realizer
3
+ class Error
4
+ class MissingContentTypeHeader < Error
5
+
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module JSONAPI
2
+ module Realizer
3
+ class Error
4
+ class MissingRootProperty < Error
5
+
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module JSONAPI
2
+ module Realizer
3
+ class Error
4
+ class MissingTypeResourceProperty < Error
5
+
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module JSONAPI
2
+ module Realizer
3
+ class Error
4
+ class TooManyRootProperties < Error
5
+
6
+ end
7
+ end
8
+ end
9
+ end
@@ -51,11 +51,11 @@ module JSONAPI
51
51
  end
52
52
 
53
53
  def valid_sparse_field?(name)
54
- attribute(name).fetch(:selectable)
54
+ attribute(name).selectable if attribute(name)
55
55
  end
56
56
 
57
57
  def valid_includes?(name)
58
- relationship(name).fetch(:includable)
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: name, includable: includable)
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: name, includable: includable)
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
@@ -1,5 +1,5 @@
1
1
  module JSONAPI
2
2
  module Realizer
3
- VERSION = "2.0.3"
3
+ VERSION = "3.0.0"
4
4
  end
5
5
  end
@@ -98,7 +98,7 @@ RSpec.describe JSONAPI::Realizer do
98
98
  },
99
99
  "relationships" => {
100
100
  "active-photographer" => {
101
- "data" => { "type" => "photographer-people", "id" => "4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9" }
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
- People::STORE["4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9"] = {
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: 2.0.3
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-12 00:00:00.000000000 Z
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