jsonapi-realizer 1.0.0 → 2.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: f3e9f32f5798910d492d81a0564ff99337b65577d7f0fff93cc1b627c53e0bd2
4
- data.tar.gz: 754cf56ffe68f405a1b68ee3d2ebb2da4d8b89146f6048e6e5c4d9755b72ff93
3
+ metadata.gz: 38b5321aa71d6cd99b39ddb9fb9a3e09679239a8eed202403a82fe0191d1eb06
4
+ data.tar.gz: e0ff4d86639cef636a852196af3cca83d10e335eda4bbaf322e7b79f63da56f9
5
5
  SHA512:
6
- metadata.gz: '076844edcdb11676eb825d8707c331fc00736cf5c31e2b345fb134cb28363315ee317d046830aa4d654741560a66ad5a2caef71cd224b12c372b427d9517cf26'
7
- data.tar.gz: 6216f9f0c53cf4b27f25f4c90e768092525e477de0156d9fbbdd09d4e952513a3f73b8247b803d9325a3883f6f2d07a1a8b846a2986c6c9de839210c6de37cab
6
+ metadata.gz: 6bfa065c4d7b383210a0be470f5d95e497da056e4df74beb10ced42920d6e66c09d9b9025c2397a98bad14268d330faaa021320b8fbae0d8144be5d77c3d095b
7
+ data.tar.gz: 9e2c5b864c0b3d067a88863bdc23f3bf839ecb3bb1da93b0423948f02e0b2dc8b8789cecb4bd1d677055ac0c83d5f25eb5c50b6bed7a60e8044dd2c299dfca3e
data/README.md CHANGED
@@ -10,7 +10,7 @@ This library handles incoming [json:api](https://www.jsonapi.org) payloads and t
10
10
  A successful JSON:API request can be annotated as:
11
11
 
12
12
  ```
13
- JSONAPIRequest -> (PersistanceAdapter -> JSONAPIRequest -> (Record | Array<Record>)) -> JSONAPIResponse
13
+ JSONAPIRequest -> (BusinessLayer -> JSONAPIRequest -> (Record | Array<Record>)) -> JSONAPIResponse
14
14
  ```
15
15
 
16
16
  The `jsonapi-serializers` library provides this shape:
@@ -22,12 +22,14 @@ JSONAPIRequest -> (Record | Array<Record>) -> JSONAPIResponse
22
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
23
 
24
24
  ```
25
- PersistanceAdapter -> JSONAPIRequest -> (Record | Array<Record>)
25
+ BusinessLayer -> JSONAPIRequest -> (Record | Array<Record>)
26
26
  ```
27
27
 
28
28
 
29
29
  ## Using
30
30
 
31
+ In order to use this library you'll want to have some models:
32
+
31
33
  ``` ruby
32
34
  class Photo < ApplicationRecord
33
35
  belongs_to :photographer, class_name: "Profile"
@@ -36,13 +38,15 @@ end
36
38
  class Profile < ApplicationRecord
37
39
  has_many :photos
38
40
  end
41
+ ```
42
+
43
+ *They don't have to be ActiveRecord* models, but we have built-in support for that library (adapter-based). Second you'll need some realizers:
39
44
 
45
+ ``` ruby
40
46
  class PhotoRealizer
41
47
  include JSONAPI::Realizer::Resource
42
48
 
43
- adapter :active_record
44
-
45
- represents :photos, class_name: "Photo"
49
+ register :photos, class_name: "Photo", adapter: :active_record
46
50
 
47
51
  has_one :photographer, as: :profiles
48
52
 
@@ -53,9 +57,7 @@ end
53
57
  class ProfileRealizer
54
58
  include JSONAPI::Realizer::Resource
55
59
 
56
- adapter :active_record
57
-
58
- represents :profiles, class_name: "Profile"
60
+ register :profiles, class_name: "Profile", adapter: :active_record
59
61
 
60
62
  has_many :photos, as: :photos
61
63
 
@@ -63,6 +65,14 @@ class ProfileRealizer
63
65
  end
64
66
  ```
65
67
 
68
+ You can define special properties on attributes and relationships realizers:
69
+
70
+ ``` ruby
71
+ has_many :doctors, as: :users, includable: false
72
+
73
+ has :title, selectable: false
74
+ ```
75
+
66
76
  Once you've designed your resources, we just need to use them! In this example, we'll use controllers from Rails:
67
77
 
68
78
  ``` ruby
@@ -71,11 +81,24 @@ class PhotosController < ApplicationController
71
81
  validate_parameters!
72
82
  authenticate_session!
73
83
 
74
- record = JSONAPI::Realizer.create(params, headers: request.headers)
84
+ realization = JSONAPI::Realizer.create(params, headers: request.headers)
85
+
86
+ ProcessPhotosService.new(realization.model)
87
+
88
+ render json: JSONAPI::Serializer.serialize(record)
89
+ end
90
+
91
+ def index
92
+ validate_parameters!
93
+ authenticate_session!
94
+
95
+ realization = JSONAPI::Realizer.index(params, headers: request.headers, type: :photos)
75
96
 
76
- ProcessPhotosService.new(record)
97
+ # See: pundit for `authorize()`
98
+ authorize realization.models
77
99
 
78
- JSONAPI::Serializer.serialize(record)
100
+ # See: pundit for `policy_scope()`
101
+ render json: JSONAPI::Serializer.serialize(policy_scope(record), is_collection: true)
79
102
  end
80
103
  end
81
104
  ```
@@ -89,9 +112,14 @@ There are two core adapters:
89
112
 
90
113
  An adapter must provide the following interfaces:
91
114
 
92
- 0. `find_via`, which tells the action how to find the model
93
- 0. `write_attributes_via`, which tells the action how to write an individual property
94
- 0. `save_via`, which tells the action how to save the model when it's done
115
+ 0. `find_via`, describes how to find the model
116
+ 0. `find_many_via`, describes how to find many models
117
+ 0. `assign_attributes_via`, describes how to write a set of properties
118
+ 0. `assign_relationships_via`, describes how to write a set of relationships
119
+ 0. `create_via`, describes how to create the model
120
+ 0. `update_via`, describes how to update the model
121
+ 0. `includes_via`, describes how to eager include related models
122
+ 0. `sparse_fields_via`, describes how to only return certain fields
95
123
 
96
124
  You can also provide custom adapter interfaces:
97
125
 
@@ -99,43 +127,20 @@ You can also provide custom adapter interfaces:
99
127
  class PhotoRealizer
100
128
  include JSONAPI::Realizer::Resource
101
129
 
102
- adapter do
103
- find_via do |model_class, id|
104
- model_class.where { id == id or slug == id }.first
105
- end
106
-
107
- write_attributes_via do |model, attributes|
108
- model.update_columns(attributes)
109
- end
130
+ register :photos, class_name: "Photo", adapter: :active_record
110
131
 
111
- save_via do |model|
112
- model.save!
113
- Rails.cache.write(model.cache_key, model)
114
- end
132
+ adapter.find_via do |model_class, id|
133
+ model_class.where { id == id or slug == id }.first
115
134
  end
116
135
 
117
- represents :photos, class_name: "Photo"
118
-
119
- has_one :photographer, as: :profiles
120
-
121
- has :title
122
- has :src
123
- end
124
- ```
125
-
126
- If you want, you can use both the regular adapters and some custom pieces:
127
-
128
- ``` ruby
129
- class PhotoRealizer
130
- include JSONAPI::Realizer::Resource
131
-
132
- adapter :active_record do
133
- find_via do |model_class, id|
134
- model_class.where { id == id or slug == id }.first
135
- end
136
+ adapter.assign_attributes_via do |model, attributes|
137
+ model.update_columns(attributes)
136
138
  end
137
139
 
138
- represents :photos, class_name: "Photo"
140
+ adapter.create_via do |model|
141
+ model.save!
142
+ Rails.cache.write(model.cache_key, model)
143
+ end
139
144
 
140
145
  has_one :photographer, as: :profiles
141
146
 
@@ -144,11 +149,12 @@ class PhotoRealizer
144
149
  end
145
150
  ```
146
151
 
152
+
147
153
  ## Installing
148
154
 
149
155
  Add this line to your application's Gemfile:
150
156
 
151
- gem "jsonapi-realizer", "1.0.0"
157
+ gem "jsonapi-realizer", "2.0.0"
152
158
 
153
159
  And then execute:
154
160
 
@@ -1,5 +1,6 @@
1
1
  require "ostruct"
2
2
  require "active_support/concern"
3
+ require "active_support/core_ext/enumerable"
3
4
 
4
5
  module JSONAPI
5
6
  module Realizer
@@ -8,23 +9,44 @@ module JSONAPI
8
9
  require_relative "realizer/adapter"
9
10
  require_relative "realizer/resource"
10
11
 
11
- def self.register(resource_class, model_class, type)
12
- @mapping ||= {}
13
- @mapping[type] = OpenStruct.new({
14
- model_class: model_class,
15
- type: type,
12
+ def self.register(resource_class:, model_class:, adapter:, type:)
13
+ @mapping ||= Set.new
14
+ @mapping << OpenStruct.new({
16
15
  resource_class: resource_class,
16
+ model_class: model_class,
17
+ adapter: adapter,
18
+ type: type.dasherize,
17
19
  attributes: OpenStruct.new({}),
18
20
  relationships: OpenStruct.new({})
19
21
  })
20
22
  end
21
23
 
22
- def self.mapping
23
- @mapping
24
+ def self.resource_mapping
25
+ @mapping.index_by(&:resource_class)
26
+ end
27
+
28
+ def self.type_mapping
29
+ @mapping.index_by(&:type)
24
30
  end
25
31
 
26
32
  def self.create(payload, headers:)
27
- Create.new(payload: payload, headers: headers).call
33
+ enact(Create.new(payload: payload, headers: headers))
34
+ end
35
+
36
+ def self.update(payload, headers:)
37
+ enact(Update.new(payload: payload, headers: headers))
38
+ end
39
+
40
+ def self.show(payload, headers:, type:)
41
+ enact(Show.new(payload: payload, headers: headers, type: type))
42
+ end
43
+
44
+ def self.index(payload, headers:, type:)
45
+ enact(Index.new(payload: payload, headers: headers, type: type))
46
+ end
47
+
48
+ private_class_method def self.inact(action)
49
+ action.tap(&:call)
28
50
  end
29
51
  end
30
52
  end
@@ -3,37 +3,111 @@ module JSONAPI
3
3
  class Action
4
4
  require_relative "action/create"
5
5
  require_relative "action/update"
6
+ require_relative "action/show"
7
+ require_relative "action/index"
6
8
 
7
- attr_reader :type
8
- attr_reader :data
9
- attr_reader :resource
9
+ attr_reader :payload
10
10
 
11
- def initialize(payload:, headers:)
11
+ def initialize
12
12
  raise NoMethodError, "must implement this function"
13
13
  end
14
14
 
15
- def call
16
- raise NoMethodError, "must implement this function"
15
+ def call; end
16
+
17
+ private def model_class
18
+ resource_class.model_class
17
19
  end
18
20
 
19
21
  private def resource_class
20
- JSONAPI::Realizer.mapping.fetch(type).resource_class
22
+ configuration.resource_class
21
23
  end
22
24
 
23
- private def relationships
24
- data.fetch("relationships", {})
25
+ private def adapter
26
+ configuration.adapter
27
+ end
28
+
29
+ private def relation_after_inclusion(relation)
30
+ if includes.any?
31
+ resource_class.include_via_call(relation, includes)
32
+ else
33
+ relation
34
+ end
35
+ end
36
+
37
+ private def relation_after_fields(relation)
38
+ if includes.any?
39
+ resource_class.sparse_fields_call(relation, fields)
40
+ else
41
+ relation
42
+ end
43
+ end
44
+
45
+ private def relation
46
+ relation_after_fields(
47
+ relation_after_inclusion(
48
+ model_class
49
+ )
50
+ )
51
+ end
52
+
53
+ private def data
54
+ payload.fetch("data", {})
25
55
  end
26
56
 
27
57
  private def id
28
- data.fetch("id", nil)
58
+ data.fetch("id", nil) || payload.fetch("id", nil)
29
59
  end
30
60
 
31
61
  private def type
32
- data.fetch("type")
62
+ (@type || data.fetch("type")).to_s.dasherize
33
63
  end
34
64
 
35
65
  private def attributes
36
- data.fetch("attributes", {})
66
+ data.
67
+ fetch("attributes", {}).
68
+ transform_keys(&:underscore).
69
+ select(&resource_class.method(:valid_attribute?))
70
+ end
71
+
72
+ private def relationships
73
+ data.
74
+ fetch("relationships", {}).
75
+ transform_keys(&:underscore).
76
+ select(&resource_class.method(:valid_relationship?)).
77
+ transform_values(&method(:as_relationship))
78
+ end
79
+
80
+ private def as_relationship(value)
81
+ data = value.fetch("data")
82
+ mapping = JSONAPI::Realizer.type_mapping.fetch(data.fetch("type"))
83
+ mapping.adapter.find_via_call(
84
+ mapping.model_class,
85
+ data.fetch("id")
86
+ )
87
+ end
88
+
89
+ private def includes
90
+ payload.
91
+ fetch("include", []).
92
+ # "carts.cart-items,carts.cart-items.product,carts.billing-information,payments"
93
+ map { |path| path.split(/\s*,\s*/) }.
94
+ # ["carts.cart-items", "carts.cart-items.product", "carts.billing-information", "payments"]
95
+ map { |path| path.gsub("-", "_") }.
96
+ # ["carts.cart_items", "carts.cart_items.product", "carts.billing_information", "payments"]
97
+ map { |path| path.split(".") }.
98
+ # [["carts", "cart_items"], ["carts", "cart_items", "product"], ["carts", "billing_information"], ["payments"]]
99
+ select(&resource_class.method(:valid_includes?))
100
+ end
101
+
102
+ private def fields
103
+ payload.
104
+ fetch("fields", []).
105
+ split(/\s*,\s*/).
106
+ select(&resource_class.method(:valid_sparse_field?))
107
+ end
108
+
109
+ private def configuration
110
+ JSONAPI::Realizer.type_mapping.fetch(type)
37
111
  end
38
112
  end
39
113
  end
@@ -2,18 +2,23 @@ module JSONAPI
2
2
  module Realizer
3
3
  class Action
4
4
  class Create < Action
5
+ attr_accessor :resource
6
+
5
7
  def initialize(payload:, headers:)
6
- @data = payload.fetch("data")
7
- @resource = resource_class.new(resource_class.model_class.new)
8
+ @payload = payload
9
+ @headers = headers
10
+ @resource = resource_class.new(relation.new)
8
11
  end
9
12
 
10
13
  def call
11
- resource.model.tap do |model|
12
- resource_class.write_attributes_via_call(model, {id: id}) if id
13
- resource_class.write_attributes_via_call(model, attributes.select(&resource.method(:valid_attribute?)))
14
- resource_class.write_attributes_via_call(model, relationships.select(&resource.method(:valid_relationship?)).transform_values(&resource.method(:as_relationship)))
15
- resource_class.save_via_call(model)
16
- end
14
+ adapter.assign_attributes_via_call(resource.model, {id: id}) if id
15
+ adapter.assign_attributes_via_call(resource.model, attributes)
16
+ adapter.assign_relationships_via_call(resource.model, relationships)
17
+ adapter.create_via_call(resource.model)
18
+ end
19
+
20
+ def model
21
+ resource.model
17
22
  end
18
23
  end
19
24
  end
@@ -6,6 +6,10 @@ RSpec.describe JSONAPI::Realizer::Action::Create do
6
6
  describe "#call" do
7
7
  subject { action.call }
8
8
 
9
+ context "with no top-level data" do
10
+
11
+ end
12
+
9
13
  context "with a good payload and good headers" do
10
14
  let(:payload) do
11
15
  {
@@ -14,16 +18,18 @@ RSpec.describe JSONAPI::Realizer::Action::Create do
14
18
  "type" => "photos",
15
19
  "attributes" => {
16
20
  "title" => "Ember Hamster",
21
+ "alt-text" => "A hamster logo.",
17
22
  "src" => "http://example.com/images/productivity.png"
18
23
  },
19
24
  "relationships" => {
20
- "photographer" => {
21
- "data" => { "type" => "people", "id" => "4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9" }
25
+ "active-photographer" => {
26
+ "data" => { "type" => "photographer-people", "id" => "4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9" }
22
27
  }
23
28
  }
24
29
  }
25
30
  }
26
31
  end
32
+
27
33
  let(:headers) do
28
34
  {
29
35
  "Content-Type" => "application/vnd.api+json",
@@ -38,20 +44,65 @@ RSpec.describe JSONAPI::Realizer::Action::Create do
38
44
  }
39
45
  end
40
46
 
41
- it "creates a model" do
42
- expect(subject).to be_a_kind_of(Photo)
47
+ it "is the right model" do
48
+ subject
49
+
50
+ expect(action.model).to be_a_kind_of(Photo)
51
+ end
52
+
53
+ it "assigns the id attribute" do
54
+ subject
55
+
56
+ expect(action.model).to have_attributes(id: "550e8400-e29b-41d4-a716-446655440000")
57
+ end
58
+
59
+ it "assigns the title attribute" do
60
+ subject
61
+
62
+ expect(action.model).to have_attributes(title: "Ember Hamster")
63
+ end
64
+
65
+ it "assigns the alt_text attribute" do
66
+ subject
67
+
68
+ expect(action.model).to have_attributes(alt_text: "A hamster logo.")
69
+ end
70
+
71
+ it "assigns the src attribute" do
72
+ subject
73
+
74
+ expect(action.model).to have_attributes(src: "http://example.com/images/productivity.png")
43
75
  end
44
76
 
45
- it "assigns the attributes" do
46
- expect(subject).to have_attributes(title: "Ember Hamster", src: "http://example.com/images/productivity.png", updated_at: a_kind_of(Time))
77
+ it "assigns the updated_at attribute" do
78
+ subject
79
+
80
+ expect(action.model).to have_attributes(updated_at: a_kind_of(Time))
47
81
  end
48
82
 
49
- it "relates the relationships" do
50
- expect(subject).to have_attributes(photographer: a_kind_of(People))
83
+ it "assigns the active_photographer attribute" do
84
+ subject
85
+
86
+ expect(action.model).to have_attributes(active_photographer: a_kind_of(People))
51
87
  end
52
88
 
53
- it "stores the new record" do
54
- expect {subject}.to change {Photo::STORE}.from({}).to({"550e8400-e29b-41d4-a716-446655440000" => hash_including(id: "550e8400-e29b-41d4-a716-446655440000", title: "Ember Hamster", src: "http://example.com/images/productivity.png")})
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
+ )
104
+ }
105
+ )
55
106
  end
56
107
  end
57
108
  end
@@ -0,0 +1,20 @@
1
+ module JSONAPI
2
+ module Realizer
3
+ class Action
4
+ class Index < Action
5
+ attr_accessor :resources
6
+
7
+ def initialize(payload:, headers:, type:)
8
+ @payload = payload
9
+ @headers = headers
10
+ @type = type
11
+ @resources = adapter.find_many_via_call(relation).map(&resource_class.method(:new))
12
+ end
13
+
14
+ def models
15
+ resources.map(&:model)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,42 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe JSONAPI::Realizer::Action::Index do
4
+ let(:action) { described_class.new(payload: payload, headers: headers, type: :photos) }
5
+
6
+ describe "#models" do
7
+ subject { action.models }
8
+
9
+ context "with no top-level data" do
10
+
11
+ end
12
+
13
+ context "with a good payload and good headers" do
14
+ let(:payload) do
15
+ {}
16
+ end
17
+ let(:headers) do
18
+ {
19
+ "Content-Type" => "application/vnd.api+json",
20
+ "Accept" => "application/vnd.api+json"
21
+ }
22
+ end
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
+ }
35
+ end
36
+
37
+ it "returns a list of photos" do
38
+ expect(subject).to include(a_kind_of(Photo), a_kind_of(Photo))
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,21 @@
1
+ module JSONAPI
2
+ module Realizer
3
+ class Action
4
+ class Show < Action
5
+
6
+ attr_accessor :resource
7
+
8
+ def initialize(payload:, headers:, type:)
9
+ @payload = payload
10
+ @headers = headers
11
+ @type = type
12
+ @resource = resource_class.new(adapter.find_via_call(relation, id))
13
+ end
14
+
15
+ def model
16
+ resource.model
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,48 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe JSONAPI::Realizer::Action::Show do
4
+ let(:action) { described_class.new(payload: payload, headers: headers, type: :photos) }
5
+
6
+ describe "#model" do
7
+ subject { action.model }
8
+
9
+ context "with no top-level data" do
10
+
11
+ end
12
+
13
+ context "with a good payload and good headers" do
14
+ let(:payload) do
15
+ {
16
+ "id" => "d09ae4c6-0fc3-4c42-8fe8-6029530c3bed"
17
+ }
18
+ end
19
+ let(:headers) do
20
+ {
21
+ "Content-Type" => "application/vnd.api+json",
22
+ "Accept" => "application/vnd.api+json"
23
+ }
24
+ end
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
38
+
39
+ it "returns a photo model" do
40
+ expect(subject).to be_a_kind_of(Photo)
41
+ end
42
+
43
+ it "returns the photos attributes" do
44
+ expect(subject).to have_attributes(title: "Ember Fox", src: "http://example.com/images/productivity-2.png")
45
+ end
46
+ end
47
+ end
48
+ end
@@ -2,22 +2,22 @@ module JSONAPI
2
2
  module Realizer
3
3
  class Action
4
4
  class Update < Action
5
+ attr_accessor :resource
6
+
5
7
  def initialize(payload:, headers:)
6
- @data = payload.fetch("data")
7
- @resource = resource_class.new(
8
- resource_class.find_via_call(
9
- resource_class.model_class,
10
- id
11
- )
12
- )
8
+ @payload = payload
9
+ @headers = headers
10
+ @resource = resource_class.new(adapter.find_via_call(relation, id))
13
11
  end
14
12
 
15
13
  def call
16
- @resource.model.tap do |model|
17
- resource_class.write_attributes_via_call(model, attributes.select(&resource.method(:valid_attribute?)))
18
- resource_class.write_attributes_via_call(model, relationships.select(&resource.method(:valid_relationship?)).transform_values(&resource.method(:as_relationship)))
19
- resource_class.save_via_call(model)
20
- end
14
+ adapter.assign_attributes_via_call(resource.model, attributes)
15
+ adapter.assign_relationships_via_call(resource.model, relationships)
16
+ adapter.update_via_call(resource.model)
17
+ end
18
+
19
+ def model
20
+ resource.model
21
21
  end
22
22
  end
23
23
  end
@@ -6,6 +6,10 @@ 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" do
10
+
11
+ end
12
+
9
13
  context "with a good payload and good headers" do
10
14
  let(:payload) do
11
15
  {
@@ -14,11 +18,12 @@ RSpec.describe JSONAPI::Realizer::Action::Update do
14
18
  "type" => "photos",
15
19
  "attributes" => {
16
20
  "title" => "Ember Hamster 2",
21
+ "alt-text" => "A hamster logo.",
17
22
  "src" => "http://example.com/images/productivity-2.png"
18
23
  },
19
24
  "relationships" => {
20
- "photographer" => {
21
- "data" => { "type" => "people", "id" => "4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9" }
25
+ "active-photographer" => {
26
+ "data" => { "type" => "photographer-people", "id" => "4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9" }
22
27
  }
23
28
  }
24
29
  }
@@ -43,20 +48,65 @@ RSpec.describe JSONAPI::Realizer::Action::Update do
43
48
  }
44
49
  end
45
50
 
46
- it "creates a model" do
47
- expect(subject).to be_a_kind_of(Photo)
51
+ it "is the right model" do
52
+ subject
53
+
54
+ expect(action.model).to be_a_kind_of(Photo)
48
55
  end
49
56
 
50
- it "assigns the attributes" do
51
- expect(subject).to have_attributes(title: "Ember Hamster 2", src: "http://example.com/images/productivity-2.png", updated_at: a_kind_of(Time))
57
+ it "assigns the title attribute" do
58
+ subject
59
+
60
+ expect(action.model).to have_attributes(title: "Ember Hamster 2")
52
61
  end
53
62
 
54
- it "relates the relationships" do
55
- expect(subject).to have_attributes(photographer: a_kind_of(People))
63
+ it "assigns the alt_text attribute" do
64
+ subject
65
+
66
+ expect(action.model).to have_attributes(alt_text: "A hamster logo.")
67
+ end
68
+
69
+ it "assigns the src attribute" do
70
+ subject
71
+
72
+ expect(action.model).to have_attributes(src: "http://example.com/images/productivity-2.png")
56
73
  end
57
74
 
58
- it "saves the record" do
59
- expect {subject}.to change {Photo::STORE}.from({"550e8400-e29b-41d4-a716-446655440000" => hash_including(id: "550e8400-e29b-41d4-a716-446655440000", title: "Ember Hamster", src: "http://example.com/images/productivity.png")}).to({"550e8400-e29b-41d4-a716-446655440000" => hash_including(id: "550e8400-e29b-41d4-a716-446655440000", title: "Ember Hamster 2", src: "http://example.com/images/productivity-2.png")})
75
+ it "assigns the updated_at attribute" do
76
+ subject
77
+
78
+ expect(action.model).to have_attributes(updated_at: a_kind_of(Time))
79
+ end
80
+
81
+ it "assigns the active_photographer attribute" do
82
+ subject
83
+
84
+ expect(action.model).to have_attributes(active_photographer: a_kind_of(People))
85
+ end
86
+
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
+ )
99
+ }
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
+ )
108
+ }
109
+ )
60
110
  end
61
111
  end
62
112
  end
@@ -1,32 +1,93 @@
1
1
  module JSONAPI
2
2
  module Realizer
3
- module Adapter
3
+ class Adapter
4
4
  require_relative "adapter/active_record"
5
5
  require_relative "adapter/memory"
6
6
 
7
7
  MAPPINGS = {
8
- memory: JSONAPI::Realizer::Adapter::Memory,
9
- active_record: JSONAPI::Realizer::Adapter::ActiveRecord,
8
+ memory: JSONAPI::Realizer::Adapter::MEMORY,
9
+ active_record: JSONAPI::Realizer::Adapter::ACTIVE_RECORD,
10
10
  }
11
11
 
12
- def self.adapt(resource, interface, &block)
13
- if interface.kind_of?(Symbol)
14
- if JSONAPI::Realizer::Adapter::MAPPINGS.key?(interface)
15
- resource.include(JSONAPI::Realizer::Adapter::MAPPINGS.fetch(interface))
16
- else
17
- raise ArgumentError, "you've given an invalid adapter alias: #{interface}, we support #{JSONAPI::Realizer::Adapter::MAPPINGS.keys}"
18
- end
12
+ def initialize(interface)
13
+ if JSONAPI::Realizer::Adapter::MAPPINGS.key?(interface.to_sym)
14
+ instance_eval(&JSONAPI::Realizer::Adapter::MAPPINGS.fetch(interface.to_sym))
19
15
  else
20
- resource.include(interface)
16
+ raise ArgumentError, "you've given an invalid adapter alias: #{interface}, we support #{JSONAPI::Realizer::Adapter::MAPPINGS.keys}"
21
17
  end
22
18
 
23
- if block_given?
24
- resource.instance_eval(&block)
25
- end
19
+ raise ArgumentError, "need to provide a Adapter.find_via interface" unless instance_variable_defined?(:@find_via_call)
20
+ raise ArgumentError, "need to provide a Adapter.find_many_via_call interface" unless instance_variable_defined?(:@find_many_via_call)
21
+ raise ArgumentError, "need to provide a Adapter.assign_attributes_via interface" unless instance_variable_defined?(:@assign_attributes_via_call)
22
+ raise ArgumentError, "need to provide a Adapter.assign_relationships_via interface" unless instance_variable_defined?(:@assign_relationships_via_call)
23
+ raise ArgumentError, "need to provide a Adapter.create_via interface" unless instance_variable_defined?(:@create_via_call)
24
+ raise ArgumentError, "need to provide a Adapter.update_via interface" unless instance_variable_defined?(:@update_via_call)
25
+ raise ArgumentError, "need to provide a Adapter.sparse_fields interface" unless instance_variable_defined?(:@sparse_fields_call)
26
+ raise ArgumentError, "need to provide a Adapter.include_via interface" unless instance_variable_defined?(:@include_via_call)
27
+ end
28
+
29
+ def find_via(&callback)
30
+ @find_via_call = callback
31
+ end
32
+
33
+ def find_many_via(&callback)
34
+ @find_many_via_call = callback
35
+ end
36
+
37
+ def create_via(&callback)
38
+ @create_via_call = callback
39
+ end
40
+
41
+ def update_via(&callback)
42
+ @update_via_call = callback
43
+ end
44
+
45
+ def assign_attributes_via(&callback)
46
+ @assign_attributes_via_call = callback
47
+ end
48
+
49
+ def assign_relationships_via(&callback)
50
+ @assign_relationships_via_call = callback
51
+ end
52
+
53
+ def sparse_fields(&callback)
54
+ @sparse_fields_call = callback
55
+ end
56
+
57
+ def include_via(&callback)
58
+ @include_via_call = callback
59
+ end
60
+
61
+ def find_via_call(model_class, id)
62
+ @find_via_call.call(model_class, id)
63
+ end
64
+
65
+ def find_many_via_call(model_class)
66
+ @find_many_via_call.call(model_class)
67
+ end
68
+
69
+ def create_via_call(model)
70
+ @create_via_call.call(model)
71
+ end
72
+
73
+ def update_via_call(model)
74
+ @update_via_call.call(model)
75
+ end
76
+
77
+ def assign_attributes_via_call(model, attributes)
78
+ @assign_attributes_via_call.call(model, attributes)
79
+ end
80
+
81
+ def assign_relationships_via_call(model, relationships)
82
+ @assign_relationships_via_call.call(model, relationships)
83
+ end
84
+
85
+ def sparse_fields_call(model_class, fields)
86
+ @sparse_fields_call.call(model_class, fields)
87
+ end
26
88
 
27
- raise ArgumentError, "need to provide a Adapter.find_via interface" unless resource.instance_variable_defined?(:@find_via_call)
28
- raise ArgumentError, "need to provide a Adapter.write_attributes_via interface" unless resource.instance_variable_defined?(:@write_attributes_via_call)
29
- raise ArgumentError, "need to provide a Adapter.save_via interface" unless resource.instance_variable_defined?(:@save_via_call)
89
+ def include_via_call(model_class, includes)
90
+ @include_via_call.call(model_class, includes)
30
91
  end
31
92
  end
32
93
  end
@@ -1,21 +1,43 @@
1
1
  module JSONAPI
2
2
  module Realizer
3
- module Adapter
4
- module ActiveRecord
5
- extend ActiveSupport::Concern
3
+ class Adapter
4
+ ACTIVE_RECORD = Proc.new do
5
+ find_many_via do |model_class|
6
+ model_class.all
7
+ end
8
+
9
+ find_via do |model_class, id|
10
+ model_class.find(id)
11
+ end
6
12
 
7
- included do
8
- find_via do |model_class, id|
9
- model_class.find(id)
10
- end
13
+ assign_attributes_via do |model, attributes|
14
+ model.assign_attributes(attributes)
15
+ end
16
+
17
+ assign_relationships_via do |model, relationships|
18
+ model.assign_attributes(relationships)
19
+ end
11
20
 
12
- write_attributes_via do |model, attributes|
13
- model.assign_attributes(attributes)
14
- end
21
+ sparse_fields do |model_class, fields|
22
+ model_class.select(fields)
23
+ end
24
+
25
+ include_via do |model_class, includes|
26
+ model_class.includes(includes.map(&(recursively_nest = -> (chains) do
27
+ if chains.size == 1
28
+ chains.first
29
+ else
30
+ {chains.first => recursively_nest.call(chains.drop(1))}
31
+ end
32
+ end)))
33
+ end
34
+
35
+ create_via do |model|
36
+ model.create!
37
+ end
15
38
 
16
- save_via do |model|
17
- model.save!
18
- end
39
+ update_via do |model|
40
+ model.update!
19
41
  end
20
42
  end
21
43
  end
@@ -1,24 +1,43 @@
1
1
  module JSONAPI
2
2
  module Realizer
3
- module Adapter
4
- module Memory
5
- extend ActiveSupport::Concern
3
+ class Adapter
4
+ MEMORY = Proc.new do
5
+ find_many_via do |model_class|
6
+ model_class.all
7
+ end
6
8
 
7
- included do
8
- find_via do |model_class, id|
9
- model_class.fetch(id)
10
- end
9
+ find_via do |model_class, id|
10
+ model_class.fetch(id)
11
+ end
12
+
13
+ assign_attributes_via do |model, attributes|
14
+ model.assign_attributes(attributes)
15
+ end
16
+
17
+ assign_relationships_via do |model, relationships|
18
+ model.assign_attributes(relationships)
19
+ end
20
+
21
+ sparse_fields do |model_class, fields|
22
+ model_class
23
+ end
11
24
 
12
- write_attributes_via do |model, attributes|
13
- model.assign_attributes(attributes)
25
+ include_via do |model_class, includes|
26
+ model_class
27
+ end
28
+
29
+ create_via do |model|
30
+ model.assign_attributes(id: model.id || SecureRandom.uuid)
31
+ model.assign_attributes(updated_at: Time.now)
32
+ model.class.const_get("STORE")[model.id] = model.class.const_get("ATTRIBUTES").inject({}) do |hash, key|
33
+ hash.merge({ key => model.public_send(key) })
14
34
  end
35
+ end
15
36
 
16
- save_via do |model|
17
- model.assign_attributes(id: model.id || SecureRandom.uuid)
18
- model.assign_attributes(updated_at: Time.now)
19
- model_class.const_get("STORE")[model.id] = model_class.const_get("ATTRIBUTES").inject({}) do |hash, key|
20
- hash.merge({ key => model.public_send(key) })
21
- end
37
+ update_via do |model|
38
+ model.assign_attributes(updated_at: Time.now)
39
+ model.class.const_get("STORE")[model.id] = model.class.const_get("ATTRIBUTES").inject({}) do |hash, key|
40
+ hash.merge({ key => model.public_send(key) })
22
41
  end
23
42
  end
24
43
  end
@@ -23,11 +23,15 @@ RSpec.describe JSONAPI::Realizer::Adapter do
23
23
 
24
24
  end
25
25
 
26
- context "when the write_attributes_via interface isn't defined" do
26
+ context "when the assign_attributes_via interface isn't defined" do
27
27
 
28
28
  end
29
29
 
30
- context "when the save_via interface isn't defined" do
30
+ context "when the create_via interface isn't defined" do
31
+
32
+ end
33
+
34
+ context "when the update_via interface isn't defined" do
31
35
 
32
36
  end
33
37
  end
@@ -1,19 +1,17 @@
1
1
  module JSONAPI
2
2
  module Realizer
3
3
  class Resource
4
- extend ActiveSupport::Concern
5
-
6
4
  attr_reader :model
7
5
 
8
6
  def initialize(model)
9
7
  @model = model
10
8
  end
11
9
 
12
- def relationship(name)
13
- relationships.public_send(name.to_sym)
10
+ private def attribute(name)
11
+ attributes.public_send(name.to_sym)
14
12
  end
15
13
 
16
- def attribute(name)
14
+ private def relationship(name)
17
15
  relationships.public_send(name.to_sym)
18
16
  end
19
17
 
@@ -25,15 +23,6 @@ module JSONAPI
25
23
  configuration.relationships
26
24
  end
27
25
 
28
- private def as_relationship(value)
29
- data = value.fetch("data")
30
- mapping = JSONAPI::Realizer.mapping.fetch(data.fetch("type"))
31
- mapping.resource_class.find_via_call(
32
- mapping.model_class,
33
- data.fetch("id")
34
- )
35
- end
36
-
37
26
  private def model_class
38
27
  configuration.model_class
39
28
  end
@@ -42,60 +31,48 @@ module JSONAPI
42
31
  self.class.configuration
43
32
  end
44
33
 
45
- def valid_attribute?(name, value)
46
- attributes.respond_to?(name.to_sym)
47
- end
48
-
49
- def valid_relationship?(name, value)
50
- relationships.respond_to?(name.to_sym)
51
- end
52
-
53
- def self.represents(type, class_name:)
54
- @configuration = JSONAPI::Realizer.register(self, class_name.constantize, type.to_s)
55
- end
56
-
57
- def self.adapter(interface, &block)
58
- JSONAPI::Realizer::Adapter.adapt(self, interface, &block)
34
+ def self.attribute(name)
35
+ attributes.public_send(name.to_sym)
59
36
  end
60
37
 
61
- def self.find_via(&finder)
62
- @find_via_call = finder
38
+ def self.relationship(name)
39
+ relationships.public_send(name.to_sym)
63
40
  end
64
41
 
65
- def self.find_via_call(model_class, id)
66
- @find_via_call.call(model_class, id)
42
+ def self.valid_attribute?(name, value)
43
+ attributes.respond_to?(name.to_sym)
67
44
  end
68
45
 
69
- def self.save_via(&saver)
70
- @save_via_call = saver
46
+ def self.valid_relationship?(name, value)
47
+ relationships.respond_to?(name.to_sym)
71
48
  end
72
49
 
73
- def self.save_via_call(model)
74
- @save_via_call.call(model)
50
+ def self.valid_sparse_field?(name)
51
+ attribute(name).fetch(:selectable)
75
52
  end
76
53
 
77
- def self.write_attributes_via(&writer)
78
- @write_attributes_via_call = writer
54
+ def self.valid_includes?(name)
55
+ relationship(name).fetch(:includable)
79
56
  end
80
57
 
81
- def self.write_attributes_via_call(model, attributes)
82
- @write_attributes_via_call.call(model, attributes)
58
+ def self.has(name, selectable: true)
59
+ attributes.public_send("#{name}=", OpenStruct.new({name: name, selectable: selectable}))
83
60
  end
84
61
 
85
- def self.has_one(name, as: name)
86
- relationships.public_send("#{name}=", OpenStruct.new({name: name, as: as}))
62
+ def self.has_related(name, as: name, includable: true)
63
+ relationships.public_send("#{name}=", OpenStruct.new({name: name, as: as, includable: includable}))
87
64
  end
88
65
 
89
- def self.has(name)
90
- attributes.public_send("#{name}=", OpenStruct.new({name: name}))
66
+ def self.has_one(name, as: name, includable: true)
67
+ has_related(name, as: name, includable: includable)
91
68
  end
92
69
 
93
- def self.relationship(name)
94
- relationships.public_send(name.to_sym)
70
+ def self.has_many(name, as: name, includable: true)
71
+ has_related(name, as: name, includable: includable)
95
72
  end
96
73
 
97
- def self.attribute(name)
98
- relationships.public_send(name.to_sym)
74
+ def self.adapter
75
+ configuration.adapter
99
76
  end
100
77
 
101
78
  def self.attributes
@@ -110,12 +87,17 @@ module JSONAPI
110
87
  configuration.model_class
111
88
  end
112
89
 
90
+ def self.register(type, class_name:, adapter:)
91
+ JSONAPI::Realizer.register(
92
+ resource_class: self,
93
+ model_class: class_name.constantize,
94
+ adapter: JSONAPI::Realizer::Adapter.new(adapter),
95
+ type: type.to_s
96
+ )
97
+ end
98
+
113
99
  def self.configuration
114
- if @configuration
115
- @configuration
116
- else
117
- raise ArgumentError, "you need to have the resource configured"
118
- end
100
+ JSONAPI::Realizer.resource_mapping.fetch(self)
119
101
  end
120
102
  end
121
103
  end
@@ -1,5 +1,5 @@
1
1
  module JSONAPI
2
2
  module Realizer
3
- VERSION = "1.0.0"
3
+ VERSION = "2.0.0"
4
4
  end
5
5
  end
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: 1.0.0
4
+ version: 2.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-04 00:00:00.000000000 Z
11
+ date: 2018-03-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -122,6 +122,10 @@ files:
122
122
  - lib/jsonapi/realizer/action.rb
123
123
  - lib/jsonapi/realizer/action/create.rb
124
124
  - lib/jsonapi/realizer/action/create_spec.rb
125
+ - lib/jsonapi/realizer/action/index.rb
126
+ - lib/jsonapi/realizer/action/index_spec.rb
127
+ - lib/jsonapi/realizer/action/show.rb
128
+ - lib/jsonapi/realizer/action/show_spec.rb
125
129
  - lib/jsonapi/realizer/action/update.rb
126
130
  - lib/jsonapi/realizer/action/update_spec.rb
127
131
  - lib/jsonapi/realizer/adapter.rb