jsonapi-realizer 1.0.0 → 2.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: 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