jsonapi-realizer 1.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 +7 -0
- data/README.md +169 -0
- data/Rakefile +12 -0
- data/lib/jsonapi-realizer.rb +3 -0
- data/lib/jsonapi/realizer.rb +30 -0
- data/lib/jsonapi/realizer/action.rb +40 -0
- data/lib/jsonapi/realizer/action/create.rb +21 -0
- data/lib/jsonapi/realizer/action/create_spec.rb +58 -0
- data/lib/jsonapi/realizer/action/update.rb +25 -0
- data/lib/jsonapi/realizer/action/update_spec.rb +63 -0
- data/lib/jsonapi/realizer/adapter.rb +33 -0
- data/lib/jsonapi/realizer/adapter/active_record.rb +23 -0
- data/lib/jsonapi/realizer/adapter/memory.rb +27 -0
- data/lib/jsonapi/realizer/adapter_spec.rb +34 -0
- data/lib/jsonapi/realizer/resource.rb +122 -0
- data/lib/jsonapi/realizer/version.rb +5 -0
- data/lib/jsonapi/realizer/version_spec.rb +7 -0
- data/lib/jsonapi/realizer_spec.rb +6 -0
- metadata +159 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f3e9f32f5798910d492d81a0564ff99337b65577d7f0fff93cc1b627c53e0bd2
|
|
4
|
+
data.tar.gz: 754cf56ffe68f405a1b68ee3d2ebb2da4d8b89146f6048e6e5c4d9755b72ff93
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: '076844edcdb11676eb825d8707c331fc00736cf5c31e2b345fb134cb28363315ee317d046830aa4d654741560a66ad5a2caef71cd224b12c372b427d9517cf26'
|
|
7
|
+
data.tar.gz: 6216f9f0c53cf4b27f25f4c90e768092525e477de0156d9fbbdd09d4e952513a3f73b8247b803d9325a3883f6f2d07a1a8b846a2986c6c9de839210c6de37cab
|
data/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# jsonapi-realizer
|
|
2
|
+
|
|
3
|
+
- [](https://travis-ci.org/krainboltgreene/jsonapi-realizer)
|
|
4
|
+
- [](https://rubygems.org/gems/jsonapi-realizer)
|
|
5
|
+
- [](https://rubygems.org/gems/jsonapi-realizer)
|
|
6
|
+
|
|
7
|
+
|
|
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
|
+
|
|
10
|
+
A successful JSON:API request can be annotated as:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
JSONAPIRequest -> (PersistanceAdapter -> 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
|
+
PersistanceAdapter -> JSONAPIRequest -> (Record | Array<Record>)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Using
|
|
30
|
+
|
|
31
|
+
``` ruby
|
|
32
|
+
class Photo < ApplicationRecord
|
|
33
|
+
belongs_to :photographer, class_name: "Profile"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class Profile < ApplicationRecord
|
|
37
|
+
has_many :photos
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class PhotoRealizer
|
|
41
|
+
include JSONAPI::Realizer::Resource
|
|
42
|
+
|
|
43
|
+
adapter :active_record
|
|
44
|
+
|
|
45
|
+
represents :photos, class_name: "Photo"
|
|
46
|
+
|
|
47
|
+
has_one :photographer, as: :profiles
|
|
48
|
+
|
|
49
|
+
has :title
|
|
50
|
+
has :src
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class ProfileRealizer
|
|
54
|
+
include JSONAPI::Realizer::Resource
|
|
55
|
+
|
|
56
|
+
adapter :active_record
|
|
57
|
+
|
|
58
|
+
represents :profiles, class_name: "Profile"
|
|
59
|
+
|
|
60
|
+
has_many :photos, as: :photos
|
|
61
|
+
|
|
62
|
+
has :name
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Once you've designed your resources, we just need to use them! In this example, we'll use controllers from Rails:
|
|
67
|
+
|
|
68
|
+
``` ruby
|
|
69
|
+
class PhotosController < ApplicationController
|
|
70
|
+
def create
|
|
71
|
+
validate_parameters!
|
|
72
|
+
authenticate_session!
|
|
73
|
+
|
|
74
|
+
record = JSONAPI::Realizer.create(params, headers: request.headers)
|
|
75
|
+
|
|
76
|
+
ProcessPhotosService.new(record)
|
|
77
|
+
|
|
78
|
+
JSONAPI::Serializer.serialize(record)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Adapters
|
|
84
|
+
|
|
85
|
+
There are two core adapters:
|
|
86
|
+
|
|
87
|
+
0. `:active_record`, which assumes an ActiveRecord-like interface.
|
|
88
|
+
0. `:memory`, which assumes a `STORE` Hash-like on the model class.
|
|
89
|
+
|
|
90
|
+
An adapter must provide the following interfaces:
|
|
91
|
+
|
|
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
|
|
95
|
+
|
|
96
|
+
You can also provide custom adapter interfaces:
|
|
97
|
+
|
|
98
|
+
``` ruby
|
|
99
|
+
class PhotoRealizer
|
|
100
|
+
include JSONAPI::Realizer::Resource
|
|
101
|
+
|
|
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
|
|
110
|
+
|
|
111
|
+
save_via do |model|
|
|
112
|
+
model.save!
|
|
113
|
+
Rails.cache.write(model.cache_key, model)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
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
|
+
end
|
|
137
|
+
|
|
138
|
+
represents :photos, class_name: "Photo"
|
|
139
|
+
|
|
140
|
+
has_one :photographer, as: :profiles
|
|
141
|
+
|
|
142
|
+
has :title
|
|
143
|
+
has :src
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Installing
|
|
148
|
+
|
|
149
|
+
Add this line to your application's Gemfile:
|
|
150
|
+
|
|
151
|
+
gem "jsonapi-realizer", "1.0.0"
|
|
152
|
+
|
|
153
|
+
And then execute:
|
|
154
|
+
|
|
155
|
+
$ bundle
|
|
156
|
+
|
|
157
|
+
Or install it yourself with:
|
|
158
|
+
|
|
159
|
+
$ gem install jsonapi-realizer
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
## Contributing
|
|
163
|
+
|
|
164
|
+
1. Read the [Code of Conduct](/CONDUCT.md)
|
|
165
|
+
2. Fork it
|
|
166
|
+
3. Create your feature branch (`git checkout -b my-new-feature`)
|
|
167
|
+
4. Commit your changes (`git commit -am 'Add some feature'`)
|
|
168
|
+
5. Push to the branch (`git push origin my-new-feature`)
|
|
169
|
+
6. Create new Pull Request
|
data/Rakefile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env rake
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
|
|
6
|
+
desc "Run all the tests in spec"
|
|
7
|
+
RSpec::Core::RakeTask.new(:spec) do |let|
|
|
8
|
+
let.pattern = "lib/**{,/*/**}/*_spec.rb"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
desc "Default: run tests"
|
|
12
|
+
task default: :spec
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require "ostruct"
|
|
2
|
+
require "active_support/concern"
|
|
3
|
+
|
|
4
|
+
module JSONAPI
|
|
5
|
+
module Realizer
|
|
6
|
+
require_relative "realizer/version"
|
|
7
|
+
require_relative "realizer/action"
|
|
8
|
+
require_relative "realizer/adapter"
|
|
9
|
+
require_relative "realizer/resource"
|
|
10
|
+
|
|
11
|
+
def self.register(resource_class, model_class, type)
|
|
12
|
+
@mapping ||= {}
|
|
13
|
+
@mapping[type] = OpenStruct.new({
|
|
14
|
+
model_class: model_class,
|
|
15
|
+
type: type,
|
|
16
|
+
resource_class: resource_class,
|
|
17
|
+
attributes: OpenStruct.new({}),
|
|
18
|
+
relationships: OpenStruct.new({})
|
|
19
|
+
})
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.mapping
|
|
23
|
+
@mapping
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.create(payload, headers:)
|
|
27
|
+
Create.new(payload: payload, headers: headers).call
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module JSONAPI
|
|
2
|
+
module Realizer
|
|
3
|
+
class Action
|
|
4
|
+
require_relative "action/create"
|
|
5
|
+
require_relative "action/update"
|
|
6
|
+
|
|
7
|
+
attr_reader :type
|
|
8
|
+
attr_reader :data
|
|
9
|
+
attr_reader :resource
|
|
10
|
+
|
|
11
|
+
def initialize(payload:, headers:)
|
|
12
|
+
raise NoMethodError, "must implement this function"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
raise NoMethodError, "must implement this function"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private def resource_class
|
|
20
|
+
JSONAPI::Realizer.mapping.fetch(type).resource_class
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private def relationships
|
|
24
|
+
data.fetch("relationships", {})
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private def id
|
|
28
|
+
data.fetch("id", nil)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private def type
|
|
32
|
+
data.fetch("type")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private def attributes
|
|
36
|
+
data.fetch("attributes", {})
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module JSONAPI
|
|
2
|
+
module Realizer
|
|
3
|
+
class Action
|
|
4
|
+
class Create < Action
|
|
5
|
+
def initialize(payload:, headers:)
|
|
6
|
+
@data = payload.fetch("data")
|
|
7
|
+
@resource = resource_class.new(resource_class.model_class.new)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
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
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe JSONAPI::Realizer::Action::Create do
|
|
4
|
+
let(:action) { described_class.new(payload: payload, headers: headers) }
|
|
5
|
+
|
|
6
|
+
describe "#call" do
|
|
7
|
+
subject { action.call }
|
|
8
|
+
|
|
9
|
+
context "with a good payload and good headers" do
|
|
10
|
+
let(:payload) do
|
|
11
|
+
{
|
|
12
|
+
"data" => {
|
|
13
|
+
"id" => "550e8400-e29b-41d4-a716-446655440000",
|
|
14
|
+
"type" => "photos",
|
|
15
|
+
"attributes" => {
|
|
16
|
+
"title" => "Ember Hamster",
|
|
17
|
+
"src" => "http://example.com/images/productivity.png"
|
|
18
|
+
},
|
|
19
|
+
"relationships" => {
|
|
20
|
+
"photographer" => {
|
|
21
|
+
"data" => { "type" => "people", "id" => "4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9" }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
let(:headers) do
|
|
28
|
+
{
|
|
29
|
+
"Content-Type" => "application/vnd.api+json",
|
|
30
|
+
"Accept" => "application/vnd.api+json"
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
before do
|
|
35
|
+
People::STORE["4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9"] = {
|
|
36
|
+
id: "4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9",
|
|
37
|
+
name: "Kurtis Rainbolt-Greene"
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "creates a model" do
|
|
42
|
+
expect(subject).to be_a_kind_of(Photo)
|
|
43
|
+
end
|
|
44
|
+
|
|
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))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "relates the relationships" do
|
|
50
|
+
expect(subject).to have_attributes(photographer: a_kind_of(People))
|
|
51
|
+
end
|
|
52
|
+
|
|
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")})
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module JSONAPI
|
|
2
|
+
module Realizer
|
|
3
|
+
class Action
|
|
4
|
+
class Update < Action
|
|
5
|
+
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
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
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
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe JSONAPI::Realizer::Action::Update do
|
|
4
|
+
let(:action) { described_class.new(payload: payload, headers: headers) }
|
|
5
|
+
|
|
6
|
+
describe "#call" do
|
|
7
|
+
subject { action.call }
|
|
8
|
+
|
|
9
|
+
context "with a good payload and good headers" do
|
|
10
|
+
let(:payload) do
|
|
11
|
+
{
|
|
12
|
+
"data" => {
|
|
13
|
+
"id" => "550e8400-e29b-41d4-a716-446655440000",
|
|
14
|
+
"type" => "photos",
|
|
15
|
+
"attributes" => {
|
|
16
|
+
"title" => "Ember Hamster 2",
|
|
17
|
+
"src" => "http://example.com/images/productivity-2.png"
|
|
18
|
+
},
|
|
19
|
+
"relationships" => {
|
|
20
|
+
"photographer" => {
|
|
21
|
+
"data" => { "type" => "people", "id" => "4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9" }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
let(:headers) do
|
|
28
|
+
{
|
|
29
|
+
"Content-Type" => "application/vnd.api+json",
|
|
30
|
+
"Accept" => "application/vnd.api+json"
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
before do
|
|
35
|
+
People::STORE["4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9"] = {
|
|
36
|
+
id: "4b8a0af6-953d-4729-8b9a-1fa4eb18f3c9",
|
|
37
|
+
name: "Kurtis Rainbolt-Greene"
|
|
38
|
+
}
|
|
39
|
+
Photo::STORE["550e8400-e29b-41d4-a716-446655440000"] = {
|
|
40
|
+
id: "550e8400-e29b-41d4-a716-446655440000",
|
|
41
|
+
title: "Ember Hamster",
|
|
42
|
+
src: "http://example.com/images/productivity.png"
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "creates a model" do
|
|
47
|
+
expect(subject).to be_a_kind_of(Photo)
|
|
48
|
+
end
|
|
49
|
+
|
|
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))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "relates the relationships" do
|
|
55
|
+
expect(subject).to have_attributes(photographer: a_kind_of(People))
|
|
56
|
+
end
|
|
57
|
+
|
|
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")})
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module JSONAPI
|
|
2
|
+
module Realizer
|
|
3
|
+
module Adapter
|
|
4
|
+
require_relative "adapter/active_record"
|
|
5
|
+
require_relative "adapter/memory"
|
|
6
|
+
|
|
7
|
+
MAPPINGS = {
|
|
8
|
+
memory: JSONAPI::Realizer::Adapter::Memory,
|
|
9
|
+
active_record: JSONAPI::Realizer::Adapter::ActiveRecord,
|
|
10
|
+
}
|
|
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
|
|
19
|
+
else
|
|
20
|
+
resource.include(interface)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
if block_given?
|
|
24
|
+
resource.instance_eval(&block)
|
|
25
|
+
end
|
|
26
|
+
|
|
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)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module JSONAPI
|
|
2
|
+
module Realizer
|
|
3
|
+
module Adapter
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
find_via do |model_class, id|
|
|
9
|
+
model_class.find(id)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
write_attributes_via do |model, attributes|
|
|
13
|
+
model.assign_attributes(attributes)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
save_via do |model|
|
|
17
|
+
model.save!
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module JSONAPI
|
|
2
|
+
module Realizer
|
|
3
|
+
module Adapter
|
|
4
|
+
module Memory
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
find_via do |model_class, id|
|
|
9
|
+
model_class.fetch(id)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
write_attributes_via do |model, attributes|
|
|
13
|
+
model.assign_attributes(attributes)
|
|
14
|
+
end
|
|
15
|
+
|
|
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
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe JSONAPI::Realizer::Adapter do
|
|
4
|
+
describe ".adapt" do
|
|
5
|
+
context "for the active_record mapped alias" do
|
|
6
|
+
it "defines the find_via_call instance variable as a anonymous function"
|
|
7
|
+
it "defines the find_via_call instance variable as a anonymous function"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
context "for a mapped alias that doesn't exist" do
|
|
11
|
+
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
context "for a block defintion" do
|
|
15
|
+
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
context "for an alias and block defintion" do
|
|
19
|
+
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
context "when the find_via interface isn't defined" do
|
|
23
|
+
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
context "when the write_attributes_via interface isn't defined" do
|
|
27
|
+
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
context "when the save_via interface isn't defined" do
|
|
31
|
+
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
module JSONAPI
|
|
2
|
+
module Realizer
|
|
3
|
+
class Resource
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
attr_reader :model
|
|
7
|
+
|
|
8
|
+
def initialize(model)
|
|
9
|
+
@model = model
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def relationship(name)
|
|
13
|
+
relationships.public_send(name.to_sym)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def attribute(name)
|
|
17
|
+
relationships.public_send(name.to_sym)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private def attributes
|
|
21
|
+
configuration.attributes
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private def relationships
|
|
25
|
+
configuration.relationships
|
|
26
|
+
end
|
|
27
|
+
|
|
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
|
+
private def model_class
|
|
38
|
+
configuration.model_class
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private def configuration
|
|
42
|
+
self.class.configuration
|
|
43
|
+
end
|
|
44
|
+
|
|
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)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.find_via(&finder)
|
|
62
|
+
@find_via_call = finder
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.find_via_call(model_class, id)
|
|
66
|
+
@find_via_call.call(model_class, id)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.save_via(&saver)
|
|
70
|
+
@save_via_call = saver
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.save_via_call(model)
|
|
74
|
+
@save_via_call.call(model)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.write_attributes_via(&writer)
|
|
78
|
+
@write_attributes_via_call = writer
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.write_attributes_via_call(model, attributes)
|
|
82
|
+
@write_attributes_via_call.call(model, attributes)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.has_one(name, as: name)
|
|
86
|
+
relationships.public_send("#{name}=", OpenStruct.new({name: name, as: as}))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.has(name)
|
|
90
|
+
attributes.public_send("#{name}=", OpenStruct.new({name: name}))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.relationship(name)
|
|
94
|
+
relationships.public_send(name.to_sym)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def self.attribute(name)
|
|
98
|
+
relationships.public_send(name.to_sym)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def self.attributes
|
|
102
|
+
configuration.attributes
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.relationships
|
|
106
|
+
configuration.relationships
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def self.model_class
|
|
110
|
+
configuration.model_class
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def self.configuration
|
|
114
|
+
if @configuration
|
|
115
|
+
@configuration
|
|
116
|
+
else
|
|
117
|
+
raise ArgumentError, "you need to have the resource configured"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: jsonapi-realizer
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Kurtis Rainbolt-Greene
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2018-03-04 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: bundler
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.16'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.16'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rspec
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.7'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.7'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rake
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '12.2'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '12.2'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: pry
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0.11'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0.11'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: activemodel
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '5.1'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '5.1'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: pry-doc
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0.11'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0.11'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: activesupport
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '5.1'
|
|
104
|
+
type: :runtime
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '5.1'
|
|
111
|
+
description: A way to take json:api requests and turn them into models
|
|
112
|
+
email:
|
|
113
|
+
- kurtis@rainbolt-greene.online
|
|
114
|
+
executables: []
|
|
115
|
+
extensions: []
|
|
116
|
+
extra_rdoc_files: []
|
|
117
|
+
files:
|
|
118
|
+
- README.md
|
|
119
|
+
- Rakefile
|
|
120
|
+
- lib/jsonapi-realizer.rb
|
|
121
|
+
- lib/jsonapi/realizer.rb
|
|
122
|
+
- lib/jsonapi/realizer/action.rb
|
|
123
|
+
- lib/jsonapi/realizer/action/create.rb
|
|
124
|
+
- lib/jsonapi/realizer/action/create_spec.rb
|
|
125
|
+
- lib/jsonapi/realizer/action/update.rb
|
|
126
|
+
- lib/jsonapi/realizer/action/update_spec.rb
|
|
127
|
+
- lib/jsonapi/realizer/adapter.rb
|
|
128
|
+
- lib/jsonapi/realizer/adapter/active_record.rb
|
|
129
|
+
- lib/jsonapi/realizer/adapter/memory.rb
|
|
130
|
+
- lib/jsonapi/realizer/adapter_spec.rb
|
|
131
|
+
- lib/jsonapi/realizer/resource.rb
|
|
132
|
+
- lib/jsonapi/realizer/version.rb
|
|
133
|
+
- lib/jsonapi/realizer/version_spec.rb
|
|
134
|
+
- lib/jsonapi/realizer_spec.rb
|
|
135
|
+
homepage: http://krainboltgreene.github.io/jsonapi-realizer
|
|
136
|
+
licenses:
|
|
137
|
+
- ISC
|
|
138
|
+
metadata: {}
|
|
139
|
+
post_install_message:
|
|
140
|
+
rdoc_options: []
|
|
141
|
+
require_paths:
|
|
142
|
+
- lib
|
|
143
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
144
|
+
requirements:
|
|
145
|
+
- - ">="
|
|
146
|
+
- !ruby/object:Gem::Version
|
|
147
|
+
version: '0'
|
|
148
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
149
|
+
requirements:
|
|
150
|
+
- - ">="
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: '0'
|
|
153
|
+
requirements: []
|
|
154
|
+
rubyforge_project:
|
|
155
|
+
rubygems_version: 2.7.4
|
|
156
|
+
signing_key:
|
|
157
|
+
specification_version: 4
|
|
158
|
+
summary: A way to take json:api requests and turn them into models
|
|
159
|
+
test_files: []
|