castle-her 1.0.1
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/.gitignore +6 -0
- data/.rspec +1 -0
- data/.travis.yml +17 -0
- data/.yardopts +2 -0
- data/CONTRIBUTING.md +26 -0
- data/Gemfile +10 -0
- data/LICENSE +7 -0
- data/README.md +1017 -0
- data/Rakefile +11 -0
- data/UPGRADE.md +110 -0
- data/castle-her.gemspec +30 -0
- data/gemfiles/Gemfile.activemodel-3.2.x +7 -0
- data/gemfiles/Gemfile.activemodel-4.0 +7 -0
- data/gemfiles/Gemfile.activemodel-4.1 +7 -0
- data/gemfiles/Gemfile.activemodel-4.2 +7 -0
- data/gemfiles/Gemfile.activemodel-5.0.x +7 -0
- data/lib/castle-her.rb +20 -0
- data/lib/castle-her/api.rb +113 -0
- data/lib/castle-her/collection.rb +12 -0
- data/lib/castle-her/errors.rb +27 -0
- data/lib/castle-her/json_api/model.rb +46 -0
- data/lib/castle-her/middleware.rb +12 -0
- data/lib/castle-her/middleware/accept_json.rb +17 -0
- data/lib/castle-her/middleware/first_level_parse_json.rb +36 -0
- data/lib/castle-her/middleware/json_api_parser.rb +36 -0
- data/lib/castle-her/middleware/parse_json.rb +21 -0
- data/lib/castle-her/middleware/second_level_parse_json.rb +36 -0
- data/lib/castle-her/model.rb +75 -0
- data/lib/castle-her/model/associations.rb +141 -0
- data/lib/castle-her/model/associations/association.rb +103 -0
- data/lib/castle-her/model/associations/association_proxy.rb +45 -0
- data/lib/castle-her/model/associations/belongs_to_association.rb +96 -0
- data/lib/castle-her/model/associations/has_many_association.rb +100 -0
- data/lib/castle-her/model/associations/has_one_association.rb +79 -0
- data/lib/castle-her/model/attributes.rb +284 -0
- data/lib/castle-her/model/base.rb +33 -0
- data/lib/castle-her/model/deprecated_methods.rb +61 -0
- data/lib/castle-her/model/http.rb +114 -0
- data/lib/castle-her/model/introspection.rb +65 -0
- data/lib/castle-her/model/nested_attributes.rb +45 -0
- data/lib/castle-her/model/orm.rb +207 -0
- data/lib/castle-her/model/parse.rb +216 -0
- data/lib/castle-her/model/paths.rb +126 -0
- data/lib/castle-her/model/relation.rb +164 -0
- data/lib/castle-her/version.rb +3 -0
- data/spec/api_spec.rb +114 -0
- data/spec/collection_spec.rb +26 -0
- data/spec/json_api/model_spec.rb +166 -0
- data/spec/middleware/accept_json_spec.rb +10 -0
- data/spec/middleware/first_level_parse_json_spec.rb +62 -0
- data/spec/middleware/json_api_parser_spec.rb +32 -0
- data/spec/middleware/second_level_parse_json_spec.rb +35 -0
- data/spec/model/associations/association_proxy_spec.rb +31 -0
- data/spec/model/associations_spec.rb +504 -0
- data/spec/model/attributes_spec.rb +389 -0
- data/spec/model/callbacks_spec.rb +145 -0
- data/spec/model/dirty_spec.rb +91 -0
- data/spec/model/http_spec.rb +158 -0
- data/spec/model/introspection_spec.rb +76 -0
- data/spec/model/nested_attributes_spec.rb +134 -0
- data/spec/model/orm_spec.rb +506 -0
- data/spec/model/parse_spec.rb +345 -0
- data/spec/model/paths_spec.rb +347 -0
- data/spec/model/relation_spec.rb +226 -0
- data/spec/model/validations_spec.rb +42 -0
- data/spec/model_spec.rb +44 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/extensions/array.rb +5 -0
- data/spec/support/extensions/hash.rb +5 -0
- data/spec/support/macros/her_macros.rb +17 -0
- data/spec/support/macros/model_macros.rb +36 -0
- data/spec/support/macros/request_macros.rb +27 -0
- metadata +290 -0
data/spec/api_spec.rb
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require File.join(File.dirname(__FILE__), "spec_helper.rb")
|
3
|
+
|
4
|
+
describe Her::API do
|
5
|
+
subject { Her::API.new }
|
6
|
+
|
7
|
+
context "initialization" do
|
8
|
+
describe "#setup" do
|
9
|
+
context "when setting custom middleware" do
|
10
|
+
before do
|
11
|
+
class Foo; end;
|
12
|
+
class Bar; end;
|
13
|
+
|
14
|
+
subject.setup :url => "https://api.example.com" do |connection|
|
15
|
+
connection.use Foo
|
16
|
+
connection.use Bar
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
specify { expect(subject.connection.builder.handlers).to eq([Foo, Bar]) }
|
21
|
+
end
|
22
|
+
|
23
|
+
context "when setting custom options" do
|
24
|
+
before { subject.setup :foo => { :bar => "baz" }, :url => "https://api.example.com" }
|
25
|
+
its(:options) { should == { :foo => { :bar => "baz" }, :url => "https://api.example.com" } }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#request" do
|
30
|
+
before do
|
31
|
+
class SimpleParser < Faraday::Response::Middleware
|
32
|
+
def on_complete(env)
|
33
|
+
env[:body] = { :data => env[:body] }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context "making HTTP requests" do
|
39
|
+
let(:parsed_data) { subject.request(:_method => :get, :_path => "/foo")[:parsed_data] }
|
40
|
+
before do
|
41
|
+
subject.setup :url => "https://api.example.com" do |builder|
|
42
|
+
builder.use SimpleParser
|
43
|
+
builder.adapter(:test) { |stub| stub.get("/foo") { |env| [200, {}, "Foo, it is."] } }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
specify { expect(parsed_data[:data]).to eq("Foo, it is.") }
|
48
|
+
end
|
49
|
+
|
50
|
+
context "making HTTP requests while specifying custom HTTP headers" do
|
51
|
+
let(:parsed_data) { subject.request(:_method => :get, :_path => "/foo", :_headers => { "X-Page" => 2 })[:parsed_data] }
|
52
|
+
|
53
|
+
before do
|
54
|
+
subject.setup :url => "https://api.example.com" do |builder|
|
55
|
+
builder.use SimpleParser
|
56
|
+
builder.adapter(:test) { |stub| stub.get("/foo") { |env| [200, {}, "Foo, it is page #{env[:request_headers]["X-Page"]}."] } }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
specify { expect(parsed_data[:data]).to eq("Foo, it is page 2.") }
|
61
|
+
end
|
62
|
+
|
63
|
+
context "parsing a request with the default parser" do
|
64
|
+
let(:parsed_data) { subject.request(:_method => :get, :_path => "users/1")[:parsed_data] }
|
65
|
+
before do
|
66
|
+
subject.setup :url => "https://api.example.com" do |builder|
|
67
|
+
builder.use Her::Middleware::FirstLevelParseJSON
|
68
|
+
builder.adapter :test do |stub|
|
69
|
+
stub.get("/users/1") { |env| [200, {}, MultiJson.dump({ :id => 1, :name => "George Michael Bluth", :errors => ["This is a single error"], :metadata => { :page => 1, :per_page => 10 } })] }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
specify do
|
75
|
+
expect(parsed_data[:data]).to eq({ :id => 1, :name => "George Michael Bluth" })
|
76
|
+
expect(parsed_data[:errors]).to eq(["This is a single error"])
|
77
|
+
expect(parsed_data[:metadata]).to eq({ :page => 1, :per_page => 10 })
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context "parsing a request with a custom parser" do
|
82
|
+
let(:parsed_data) { subject.request(:_method => :get, :_path => "users/1")[:parsed_data] }
|
83
|
+
before do
|
84
|
+
class CustomParser < Faraday::Response::Middleware
|
85
|
+
def on_complete(env)
|
86
|
+
json = MultiJson.load(env[:body], :symbolize_keys => true)
|
87
|
+
errors = json.delete(:errors) || []
|
88
|
+
metadata = json.delete(:metadata) || {}
|
89
|
+
env[:body] = {
|
90
|
+
:data => json,
|
91
|
+
:errors => errors,
|
92
|
+
:metadata => metadata,
|
93
|
+
}
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
subject.setup :url => "https://api.example.com" do |builder|
|
98
|
+
builder.use CustomParser
|
99
|
+
builder.use Faraday::Request::UrlEncoded
|
100
|
+
builder.adapter :test do |stub|
|
101
|
+
stub.get("/users/1") { |env| [200, {}, MultiJson.dump(:id => 1, :name => "George Michael Bluth")] }
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
specify do
|
107
|
+
expect(parsed_data[:data]).to eq({ :id => 1, :name => "George Michael Bluth" })
|
108
|
+
expect(parsed_data[:errors]).to eq([])
|
109
|
+
expect(parsed_data[:metadata]).to eq({})
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Her::Collection do
|
4
|
+
|
5
|
+
let(:items) { [1, 2, 3, 4] }
|
6
|
+
let(:metadata) { { :name => 'Testname' } }
|
7
|
+
let(:errors) { { :name => ['not_present'] } }
|
8
|
+
|
9
|
+
describe "#new" do
|
10
|
+
context "without parameters" do
|
11
|
+
subject { Her::Collection.new }
|
12
|
+
|
13
|
+
it { should eq([]) }
|
14
|
+
its(:metadata) { should eq({}) }
|
15
|
+
its(:errors) { should eq({}) }
|
16
|
+
end
|
17
|
+
|
18
|
+
context "with parameters" do
|
19
|
+
subject { Her::Collection.new(items, metadata, errors) }
|
20
|
+
|
21
|
+
it { should eq([1,2,3,4]) }
|
22
|
+
its(:metadata) { should eq({ :name => 'Testname' }) }
|
23
|
+
its(:errors) { should eq({ :name => ['not_present'] }) }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Her::JsonApi::Model do
|
4
|
+
before do
|
5
|
+
Her::API.setup :url => "https://api.example.com" do |connection|
|
6
|
+
connection.use Her::Middleware::JsonApiParser
|
7
|
+
connection.adapter :test do |stub|
|
8
|
+
stub.get("/users/1") do |env|
|
9
|
+
[
|
10
|
+
200,
|
11
|
+
{},
|
12
|
+
{
|
13
|
+
data: {
|
14
|
+
id: 1,
|
15
|
+
type: 'users',
|
16
|
+
attributes: {
|
17
|
+
name: "Roger Federer",
|
18
|
+
},
|
19
|
+
}
|
20
|
+
|
21
|
+
}.to_json
|
22
|
+
]
|
23
|
+
end
|
24
|
+
|
25
|
+
stub.get("/users") do |env|
|
26
|
+
[
|
27
|
+
200,
|
28
|
+
{},
|
29
|
+
{
|
30
|
+
data: [
|
31
|
+
{
|
32
|
+
id: 1,
|
33
|
+
type: 'users',
|
34
|
+
attributes: {
|
35
|
+
name: "Roger Federer",
|
36
|
+
},
|
37
|
+
},
|
38
|
+
{
|
39
|
+
id: 2,
|
40
|
+
type: 'users',
|
41
|
+
attributes: {
|
42
|
+
name: "Kei Nishikori",
|
43
|
+
},
|
44
|
+
}
|
45
|
+
]
|
46
|
+
}.to_json
|
47
|
+
]
|
48
|
+
end
|
49
|
+
|
50
|
+
stub.post("/users", data: {
|
51
|
+
type: 'users',
|
52
|
+
attributes: {
|
53
|
+
name: "Jeremy Lin",
|
54
|
+
},
|
55
|
+
}) do |env|
|
56
|
+
[
|
57
|
+
201,
|
58
|
+
{},
|
59
|
+
{
|
60
|
+
data: {
|
61
|
+
id: 3,
|
62
|
+
type: 'users',
|
63
|
+
attributes: {
|
64
|
+
name: 'Jeremy Lin',
|
65
|
+
},
|
66
|
+
}
|
67
|
+
|
68
|
+
}.to_json
|
69
|
+
]
|
70
|
+
end
|
71
|
+
|
72
|
+
stub.patch("/users/1", data: {
|
73
|
+
type: 'users',
|
74
|
+
id: 1,
|
75
|
+
attributes: {
|
76
|
+
name: "Fed GOAT",
|
77
|
+
},
|
78
|
+
}) do |env|
|
79
|
+
[
|
80
|
+
200,
|
81
|
+
{},
|
82
|
+
{
|
83
|
+
data: {
|
84
|
+
id: 1,
|
85
|
+
type: 'users',
|
86
|
+
attributes: {
|
87
|
+
name: 'Fed GOAT',
|
88
|
+
},
|
89
|
+
}
|
90
|
+
|
91
|
+
}.to_json
|
92
|
+
]
|
93
|
+
end
|
94
|
+
|
95
|
+
stub.delete("/users/1") { |env|
|
96
|
+
[ 204, {}, {}, ]
|
97
|
+
}
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
spawn_model("Foo::User", type: Her::JsonApi::Model)
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'allows configuration of type' do
|
106
|
+
spawn_model("Foo::Bar", type: Her::JsonApi::Model) do
|
107
|
+
type :foobars
|
108
|
+
end
|
109
|
+
|
110
|
+
expect(Foo::Bar.instance_variable_get('@type')).to eql('foobars')
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'finds models by id' do
|
114
|
+
user = Foo::User.find(1)
|
115
|
+
expect(user.attributes).to eql(
|
116
|
+
'id' => 1,
|
117
|
+
'name' => 'Roger Federer',
|
118
|
+
)
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'finds a collection of models' do
|
122
|
+
users = Foo::User.all
|
123
|
+
expect(users.map(&:attributes)).to match_array([
|
124
|
+
{
|
125
|
+
'id' => 1,
|
126
|
+
'name' => 'Roger Federer',
|
127
|
+
},
|
128
|
+
{
|
129
|
+
'id' => 2,
|
130
|
+
'name' => 'Kei Nishikori',
|
131
|
+
}
|
132
|
+
])
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'creates a Foo::User' do
|
136
|
+
user = Foo::User.new(name: 'Jeremy Lin')
|
137
|
+
user.save
|
138
|
+
expect(user.attributes).to eql(
|
139
|
+
'id' => 3,
|
140
|
+
'name' => 'Jeremy Lin',
|
141
|
+
)
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'updates a Foo::User' do
|
145
|
+
user = Foo::User.find(1)
|
146
|
+
user.name = 'Fed GOAT'
|
147
|
+
user.save
|
148
|
+
expect(user.attributes).to eql(
|
149
|
+
'id' => 1,
|
150
|
+
'name' => 'Fed GOAT',
|
151
|
+
)
|
152
|
+
end
|
153
|
+
|
154
|
+
it 'destroys a Foo::User' do
|
155
|
+
user = Foo::User.find(1)
|
156
|
+
expect(user.destroy).to be_destroyed
|
157
|
+
end
|
158
|
+
|
159
|
+
context 'undefined methods' do
|
160
|
+
it 'removes methods that are not compatible with json api' do
|
161
|
+
[:parse_root_in_json, :include_root_in_json, :root_element, :primary_key].each do |method|
|
162
|
+
expect { Foo::User.new.send(method, :foo) }.to raise_error NoMethodError, "Her::JsonApi::Model does not support the #{method} configuration option"
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "spec_helper"
|
3
|
+
|
4
|
+
describe Her::Middleware::FirstLevelParseJSON do
|
5
|
+
subject { described_class.new }
|
6
|
+
let(:body_without_errors) { "{\"id\": 1, \"name\": \"Tobias Fünke\", \"metadata\": 3}" }
|
7
|
+
let(:body_with_errors) { "{\"id\": 1, \"name\": \"Tobias Fünke\", \"errors\": { \"name\": [ \"not_valid\", \"should_be_present\" ] }, \"metadata\": 3}" }
|
8
|
+
let(:body_with_malformed_json) { "wut." }
|
9
|
+
let(:body_with_invalid_json) { "true" }
|
10
|
+
let(:empty_body) { '' }
|
11
|
+
let(:nil_body) { nil }
|
12
|
+
|
13
|
+
it "parses body as json" do
|
14
|
+
subject.parse(body_without_errors).tap do |json|
|
15
|
+
expect(json[:data]).to eq({ :id => 1, :name => "Tobias Fünke" })
|
16
|
+
expect(json[:metadata]).to eq(3)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
it "parses :body key as json in the env hash" do
|
21
|
+
env = { :body => body_without_errors }
|
22
|
+
subject.on_complete(env)
|
23
|
+
env[:body].tap do |json|
|
24
|
+
expect(json[:data]).to eq({ :id => 1, :name => "Tobias Fünke" })
|
25
|
+
expect(json[:metadata]).to eq(3)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'ensures the errors are a hash if there are no errors' do
|
30
|
+
expect(subject.parse(body_without_errors)[:errors]).to eq({})
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'ensures the errors are a hash if there are no errors' do
|
34
|
+
expect(subject.parse(body_with_errors)[:errors]).to eq({:name => [ 'not_valid', 'should_be_present']})
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'ensures that malformed JSON throws an exception' do
|
38
|
+
expect { subject.parse(body_with_malformed_json) }.to raise_error(Her::Errors::ParseError, 'Response from the API must behave like a Hash or an Array (last JSON response was "wut.")')
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'ensures that invalid JSON throws an exception' do
|
42
|
+
expect { subject.parse(body_with_invalid_json) }.to raise_error(Her::Errors::ParseError, 'Response from the API must behave like a Hash or an Array (last JSON response was "true")')
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'ensures that a nil response returns an empty hash' do
|
46
|
+
expect(subject.parse(nil_body)[:data]).to eq({})
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'ensures that an empty response returns an empty hash' do
|
50
|
+
expect(subject.parse(empty_body)[:data]).to eq({})
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'with status code 204' do
|
54
|
+
it 'returns an empty body' do
|
55
|
+
env = { :status => 204 }
|
56
|
+
subject.on_complete(env)
|
57
|
+
env[:body].tap do |json|
|
58
|
+
expect(json[:data]).to eq({})
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "spec_helper"
|
3
|
+
|
4
|
+
describe Her::Middleware::JsonApiParser do
|
5
|
+
subject { described_class.new }
|
6
|
+
|
7
|
+
context "with valid JSON body" do
|
8
|
+
let(:body) { '{"data": {"type": "foo", "id": "bar", "attributes": {"baz": "qux"} }, "meta": {"api": "json api"} }' }
|
9
|
+
let(:env) { { body: body } }
|
10
|
+
|
11
|
+
it "parses body as json" do
|
12
|
+
subject.on_complete(env)
|
13
|
+
env.fetch(:body).tap do |json|
|
14
|
+
expect(json[:data]).to eql(
|
15
|
+
:type => "foo",
|
16
|
+
:id => "bar",
|
17
|
+
:attributes => { :baz => "qux" }
|
18
|
+
)
|
19
|
+
expect(json[:errors]).to eql([])
|
20
|
+
expect(json[:metadata]).to eql(:api => "json api")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
#context "with invalid JSON body" do
|
26
|
+
# let(:body) { '"foo"' }
|
27
|
+
# it 'ensures that invalid JSON throws an exception' do
|
28
|
+
# expect { subject.parse(body) }.to raise_error(Her::Errors::ParseError, 'Response from the API must behave like a Hash or an Array (last JSON response was "\"foo\"")')
|
29
|
+
# end
|
30
|
+
#end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "spec_helper"
|
3
|
+
|
4
|
+
describe Her::Middleware::SecondLevelParseJSON do
|
5
|
+
subject { described_class.new }
|
6
|
+
|
7
|
+
context "with valid JSON body" do
|
8
|
+
let(:body) { "{\"data\": 1, \"errors\": 2, \"metadata\": 3}" }
|
9
|
+
it "parses body as json" do
|
10
|
+
subject.parse(body).tap do |json|
|
11
|
+
expect(json[:data]).to eq(1)
|
12
|
+
expect(json[:errors]).to eq(2)
|
13
|
+
expect(json[:metadata]).to eq(3)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
it "parses :body key as json in the env hash" do
|
18
|
+
env = { :body => body }
|
19
|
+
subject.on_complete(env)
|
20
|
+
env[:body].tap do |json|
|
21
|
+
expect(json[:data]).to eq(1)
|
22
|
+
expect(json[:errors]).to eq(2)
|
23
|
+
expect(json[:metadata]).to eq(3)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context "with invalid JSON body" do
|
29
|
+
let(:body) { '"foo"' }
|
30
|
+
it 'ensures that invalid JSON throws an exception' do
|
31
|
+
expect { subject.parse(body) }.to raise_error(Her::Errors::ParseError, 'Response from the API must behave like a Hash or an Array (last JSON response was "\"foo\"")')
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|