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.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +17 -0
  5. data/.yardopts +2 -0
  6. data/CONTRIBUTING.md +26 -0
  7. data/Gemfile +10 -0
  8. data/LICENSE +7 -0
  9. data/README.md +1017 -0
  10. data/Rakefile +11 -0
  11. data/UPGRADE.md +110 -0
  12. data/castle-her.gemspec +30 -0
  13. data/gemfiles/Gemfile.activemodel-3.2.x +7 -0
  14. data/gemfiles/Gemfile.activemodel-4.0 +7 -0
  15. data/gemfiles/Gemfile.activemodel-4.1 +7 -0
  16. data/gemfiles/Gemfile.activemodel-4.2 +7 -0
  17. data/gemfiles/Gemfile.activemodel-5.0.x +7 -0
  18. data/lib/castle-her.rb +20 -0
  19. data/lib/castle-her/api.rb +113 -0
  20. data/lib/castle-her/collection.rb +12 -0
  21. data/lib/castle-her/errors.rb +27 -0
  22. data/lib/castle-her/json_api/model.rb +46 -0
  23. data/lib/castle-her/middleware.rb +12 -0
  24. data/lib/castle-her/middleware/accept_json.rb +17 -0
  25. data/lib/castle-her/middleware/first_level_parse_json.rb +36 -0
  26. data/lib/castle-her/middleware/json_api_parser.rb +36 -0
  27. data/lib/castle-her/middleware/parse_json.rb +21 -0
  28. data/lib/castle-her/middleware/second_level_parse_json.rb +36 -0
  29. data/lib/castle-her/model.rb +75 -0
  30. data/lib/castle-her/model/associations.rb +141 -0
  31. data/lib/castle-her/model/associations/association.rb +103 -0
  32. data/lib/castle-her/model/associations/association_proxy.rb +45 -0
  33. data/lib/castle-her/model/associations/belongs_to_association.rb +96 -0
  34. data/lib/castle-her/model/associations/has_many_association.rb +100 -0
  35. data/lib/castle-her/model/associations/has_one_association.rb +79 -0
  36. data/lib/castle-her/model/attributes.rb +284 -0
  37. data/lib/castle-her/model/base.rb +33 -0
  38. data/lib/castle-her/model/deprecated_methods.rb +61 -0
  39. data/lib/castle-her/model/http.rb +114 -0
  40. data/lib/castle-her/model/introspection.rb +65 -0
  41. data/lib/castle-her/model/nested_attributes.rb +45 -0
  42. data/lib/castle-her/model/orm.rb +207 -0
  43. data/lib/castle-her/model/parse.rb +216 -0
  44. data/lib/castle-her/model/paths.rb +126 -0
  45. data/lib/castle-her/model/relation.rb +164 -0
  46. data/lib/castle-her/version.rb +3 -0
  47. data/spec/api_spec.rb +114 -0
  48. data/spec/collection_spec.rb +26 -0
  49. data/spec/json_api/model_spec.rb +166 -0
  50. data/spec/middleware/accept_json_spec.rb +10 -0
  51. data/spec/middleware/first_level_parse_json_spec.rb +62 -0
  52. data/spec/middleware/json_api_parser_spec.rb +32 -0
  53. data/spec/middleware/second_level_parse_json_spec.rb +35 -0
  54. data/spec/model/associations/association_proxy_spec.rb +31 -0
  55. data/spec/model/associations_spec.rb +504 -0
  56. data/spec/model/attributes_spec.rb +389 -0
  57. data/spec/model/callbacks_spec.rb +145 -0
  58. data/spec/model/dirty_spec.rb +91 -0
  59. data/spec/model/http_spec.rb +158 -0
  60. data/spec/model/introspection_spec.rb +76 -0
  61. data/spec/model/nested_attributes_spec.rb +134 -0
  62. data/spec/model/orm_spec.rb +506 -0
  63. data/spec/model/parse_spec.rb +345 -0
  64. data/spec/model/paths_spec.rb +347 -0
  65. data/spec/model/relation_spec.rb +226 -0
  66. data/spec/model/validations_spec.rb +42 -0
  67. data/spec/model_spec.rb +44 -0
  68. data/spec/spec_helper.rb +26 -0
  69. data/spec/support/extensions/array.rb +5 -0
  70. data/spec/support/extensions/hash.rb +5 -0
  71. data/spec/support/macros/her_macros.rb +17 -0
  72. data/spec/support/macros/model_macros.rb +36 -0
  73. data/spec/support/macros/request_macros.rb +27 -0
  74. metadata +290 -0
@@ -0,0 +1,3 @@
1
+ module Her
2
+ VERSION = "1.0.1"
3
+ end
@@ -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,10 @@
1
+ # encoding: utf-8
2
+ require "spec_helper"
3
+
4
+ describe Her::Middleware::AcceptJSON do
5
+ it "adds an Accept header" do
6
+ described_class.new.add_header({}).tap do |headers|
7
+ expect(headers["Accept"]).to eq("application/json")
8
+ end
9
+ end
10
+ 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