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 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