him 0.1.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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +40 -0
  3. data/.gitignore +6 -0
  4. data/.qlty/qlty.toml +57 -0
  5. data/.rspec +1 -0
  6. data/.ruby-version +1 -0
  7. data/.yardopts +2 -0
  8. data/CONTRIBUTING.md +26 -0
  9. data/Gemfile +2 -0
  10. data/LICENSE +8 -0
  11. data/README.md +1007 -0
  12. data/Rakefile +11 -0
  13. data/UPGRADE.md +101 -0
  14. data/gemfiles/Gemfile.activemodel-6.1 +6 -0
  15. data/gemfiles/Gemfile.activemodel-7.0 +6 -0
  16. data/gemfiles/Gemfile.activemodel-7.1 +6 -0
  17. data/gemfiles/Gemfile.activemodel-7.2 +6 -0
  18. data/gemfiles/Gemfile.activemodel-8.0 +6 -0
  19. data/him.gemspec +28 -0
  20. data/lib/him/api.rb +121 -0
  21. data/lib/him/collection.rb +21 -0
  22. data/lib/him/errors.rb +29 -0
  23. data/lib/him/json_api/model.rb +42 -0
  24. data/lib/him/middleware/accept_json.rb +18 -0
  25. data/lib/him/middleware/first_level_parse_json.rb +37 -0
  26. data/lib/him/middleware/json_api_parser.rb +65 -0
  27. data/lib/him/middleware/parse_json.rb +22 -0
  28. data/lib/him/middleware/second_level_parse_json.rb +37 -0
  29. data/lib/him/middleware.rb +12 -0
  30. data/lib/him/model/associations/association.rb +147 -0
  31. data/lib/him/model/associations/association_proxy.rb +47 -0
  32. data/lib/him/model/associations/belongs_to_association.rb +95 -0
  33. data/lib/him/model/associations/has_many_association.rb +113 -0
  34. data/lib/him/model/associations/has_one_association.rb +79 -0
  35. data/lib/him/model/associations.rb +141 -0
  36. data/lib/him/model/attributes.rb +337 -0
  37. data/lib/him/model/base.rb +33 -0
  38. data/lib/him/model/http.rb +113 -0
  39. data/lib/him/model/introspection.rb +77 -0
  40. data/lib/him/model/nested_attributes.rb +45 -0
  41. data/lib/him/model/orm.rb +306 -0
  42. data/lib/him/model/parse.rb +224 -0
  43. data/lib/him/model/paths.rb +125 -0
  44. data/lib/him/model/relation.rb +212 -0
  45. data/lib/him/model.rb +79 -0
  46. data/lib/him/version.rb +3 -0
  47. data/lib/him.rb +22 -0
  48. data/spec/api_spec.rb +120 -0
  49. data/spec/collection_spec.rb +70 -0
  50. data/spec/json_api/model_spec.rb +260 -0
  51. data/spec/middleware/accept_json_spec.rb +11 -0
  52. data/spec/middleware/first_level_parse_json_spec.rb +63 -0
  53. data/spec/middleware/json_api_parser_spec.rb +52 -0
  54. data/spec/middleware/second_level_parse_json_spec.rb +35 -0
  55. data/spec/model/associations/association_proxy_spec.rb +29 -0
  56. data/spec/model/associations_spec.rb +1010 -0
  57. data/spec/model/attributes_spec.rb +384 -0
  58. data/spec/model/callbacks_spec.rb +194 -0
  59. data/spec/model/dirty_spec.rb +133 -0
  60. data/spec/model/http_spec.rb +187 -0
  61. data/spec/model/introspection_spec.rb +110 -0
  62. data/spec/model/nested_attributes_spec.rb +135 -0
  63. data/spec/model/orm_spec.rb +717 -0
  64. data/spec/model/parse_spec.rb +619 -0
  65. data/spec/model/paths_spec.rb +348 -0
  66. data/spec/model/relation_spec.rb +255 -0
  67. data/spec/model/validations_spec.rb +45 -0
  68. data/spec/model_spec.rb +55 -0
  69. data/spec/spec_helper.rb +25 -0
  70. data/spec/support/extensions/array.rb +6 -0
  71. data/spec/support/extensions/hash.rb +6 -0
  72. data/spec/support/macros/her_macros.rb +17 -0
  73. data/spec/support/macros/model_macros.rb +36 -0
  74. data/spec/support/macros/request_macros.rb +27 -0
  75. metadata +201 -0
@@ -0,0 +1,260 @@
1
+ require "spec_helper"
2
+
3
+ describe Him::JsonApi::Model do
4
+ before do
5
+ Him::API.setup url: "https://api.example.com" do |connection|
6
+ connection.use Him::Middleware::JsonApiParser
7
+ connection.adapter :test do |stub|
8
+ stub.get("/users/1") do
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
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
+ {
52
+ type: "users",
53
+ attributes: {
54
+ name: "Jeremy Lin"
55
+ }
56
+ }) do
57
+ [
58
+ 201,
59
+ {},
60
+ {
61
+ data: {
62
+ id: 3,
63
+ type: "users",
64
+ attributes: {
65
+ name: "Jeremy Lin"
66
+ }
67
+ }
68
+
69
+ }.to_json
70
+ ]
71
+ end
72
+
73
+ stub.patch("/users/1", data:
74
+ {
75
+ type: "users",
76
+ id: 1,
77
+ attributes: {
78
+ name: "Fed GOAT"
79
+ }
80
+ }) do
81
+ [
82
+ 200,
83
+ {},
84
+ {
85
+ data: {
86
+ id: 1,
87
+ type: "users",
88
+ attributes: {
89
+ name: "Fed GOAT"
90
+ }
91
+ }
92
+
93
+ }.to_json
94
+ ]
95
+ end
96
+
97
+ stub.delete("/users/1") do
98
+ [204, {}, {}]
99
+ end
100
+ end
101
+ end
102
+
103
+ spawn_model("Foo::User", type: Him::JsonApi::Model)
104
+ end
105
+
106
+ it "allows configuration of type" do
107
+ spawn_model("Foo::Bar", type: Him::JsonApi::Model) do
108
+ type :foobars
109
+ end
110
+
111
+ expect(Foo::Bar.instance_variable_get("@type")).to eql("foobars")
112
+ end
113
+
114
+ it "finds models by id" do
115
+ user = Foo::User.find(1)
116
+ expect(user.attributes).to eql(
117
+ "id" => 1,
118
+ "name" => "Roger Federer"
119
+ )
120
+ end
121
+
122
+ it "finds a collection of models" do
123
+ users = Foo::User.all
124
+ expect(users.map(&:attributes)).to match_array(
125
+ [
126
+ {
127
+ "id" => 1,
128
+ "name" => "Roger Federer"
129
+ },
130
+ {
131
+ "id" => 2,
132
+ "name" => "Kei Nishikori"
133
+ }
134
+ ]
135
+ )
136
+ end
137
+
138
+ it "creates a Foo::User" do
139
+ user = Foo::User.new(name: "Jeremy Lin")
140
+ user.save
141
+ expect(user.attributes).to eql(
142
+ "id" => 3,
143
+ "name" => "Jeremy Lin"
144
+ )
145
+ end
146
+
147
+ it "updates a Foo::User" do
148
+ user = Foo::User.find(1)
149
+ user.name = "Fed GOAT"
150
+ user.save
151
+ expect(user.attributes).to eql(
152
+ "id" => 1,
153
+ "name" => "Fed GOAT"
154
+ )
155
+ end
156
+
157
+ it "destroys a Foo::User" do
158
+ user = Foo::User.find(1)
159
+ expect(user.destroy).to be_destroyed
160
+ end
161
+
162
+ context "undefined methods" do
163
+ it "removes methods that are not compatible with json api" do
164
+ [:parse_root_in_json, :include_root_in_json, :root_element, :primary_key].each do |method|
165
+ expect { Foo::User.new.send(method, :foo) }.to raise_error NoMethodError, "Him::JsonApi::Model does not support the #{method} configuration option"
166
+ end
167
+ end
168
+ end
169
+
170
+ context "compound document" do
171
+ before do
172
+ Him::API.setup url: "https://api.example.com" do |connection|
173
+ connection.use Him::Middleware::JsonApiParser
174
+ connection.adapter :test do |stub|
175
+ stub.get("/players") do
176
+ [
177
+ 200,
178
+ {},
179
+ {
180
+ data: [
181
+ {
182
+ id: 1,
183
+ type: "players",
184
+ attributes: { name: "Roger Federer" },
185
+ relationships: {
186
+ sponsors: {
187
+ data: [
188
+ { type: "sponsors", id: 1 },
189
+ { type: "sponsors", id: 2 }
190
+ ]
191
+ },
192
+ racquet: {
193
+ data: { type: "racquets", id: 1 }
194
+ }
195
+ }
196
+ },
197
+ {
198
+ id: 2,
199
+ type: "players",
200
+ attributes: { name: "Kei Nishikori" },
201
+ relationships: {
202
+ sponsors: {
203
+ data: [
204
+ { type: "sponsors", id: 2 },
205
+ { type: "sponsors", id: 3 }
206
+ ]
207
+ },
208
+ racquet: {
209
+ data: { type: "racquets", id: 2 }
210
+ }
211
+ }
212
+ },
213
+ {
214
+ id: 3,
215
+ type: "players",
216
+ attributes: { name: "Hubert Huang", racquet_id: nil },
217
+ relationships: {}
218
+ }
219
+ ],
220
+ included: [
221
+ { type: "sponsors", id: 1, attributes: { company: "Nike" } },
222
+ { type: "sponsors", id: 2, attributes: { company: "Wilson" } },
223
+ { type: "sponsors", id: 3, attributes: { company: "Uniqlo" } },
224
+ { type: "racquets", id: 1, attributes: { name: "Wilson Pro Staff" } },
225
+ { type: "racquets", id: 2, attributes: { name: "Wilson Steam" } }
226
+ ]
227
+ }.to_json
228
+ ]
229
+ end
230
+
231
+ stub.get("/players/3/sponsors") do
232
+ [200, {}, { data: [] }.to_json]
233
+ end
234
+ end
235
+ end
236
+
237
+ spawn_model("Foo::Sponsor", type: Him::JsonApi::Model)
238
+ spawn_model("Foo::Racquet", type: Him::JsonApi::Model)
239
+ spawn_model("Foo::Player", type: Him::JsonApi::Model) do
240
+ has_many :sponsors
241
+ belongs_to :racquet
242
+ end
243
+ end
244
+
245
+ it "parses included resources into associations from compound documents" do
246
+ players = Foo::Player.all.to_a
247
+ fed = players.detect { |p| p.name == "Roger Federer" }
248
+ expect(fed.sponsors.map(&:company)).to match_array ["Nike", "Wilson"]
249
+ expect(fed.racquet.name).to eq "Wilson Pro Staff"
250
+
251
+ kei = players.detect { |p| p.name == "Kei Nishikori" }
252
+ expect(kei.sponsors.map(&:company)).to match_array ["Uniqlo", "Wilson"]
253
+ expect(kei.racquet.name).to eq "Wilson Steam"
254
+
255
+ hubert = players.detect { |p| p.name == "Hubert Huang" }
256
+ expect(hubert.sponsors).to eq []
257
+ expect(hubert.racquet).to be_nil
258
+ end
259
+ end
260
+ end
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+
3
+ require "spec_helper"
4
+
5
+ describe Him::Middleware::AcceptJSON do
6
+ it "adds an Accept header" do
7
+ described_class.new.add_header({}).tap do |headers|
8
+ expect(headers["Accept"]).to eq("application/json")
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,63 @@
1
+ # encoding: utf-8
2
+
3
+ require "spec_helper"
4
+
5
+ describe Him::Middleware::FirstLevelParseJSON do
6
+ subject { described_class.new }
7
+ let(:body_without_errors) { "{\"id\": 1, \"name\": \"Tobias Fünke\", \"metadata\": 3}" }
8
+ let(:body_with_errors) { "{\"id\": 1, \"name\": \"Tobias Fünke\", \"errors\": { \"name\": [ \"not_valid\", \"should_be_present\" ] }, \"metadata\": 3}" }
9
+ let(:body_with_malformed_json) { "wut." }
10
+ let(:body_with_invalid_json) { "true" }
11
+ let(:empty_body) { "" }
12
+ let(:nil_body) { nil }
13
+
14
+ it "parses body as json" do
15
+ subject.parse(body_without_errors).tap do |json|
16
+ expect(json[:data]).to eq(id: 1, name: "Tobias Fünke")
17
+ expect(json[:metadata]).to eq(3)
18
+ end
19
+ end
20
+
21
+ it "parses :body key as json in the env hash" do
22
+ env = { body: body_without_errors }
23
+ subject.on_complete(env)
24
+ env[:body].tap do |json|
25
+ expect(json[:data]).to eq(id: 1, name: "Tobias Fünke")
26
+ expect(json[:metadata]).to eq(3)
27
+ end
28
+ end
29
+
30
+ it "ensures the errors are a hash if there are no errors" do
31
+ expect(subject.parse(body_without_errors)[:errors]).to eq({})
32
+ end
33
+
34
+ it "ensures the errors are a hash if there are no errors" do
35
+ expect(subject.parse(body_with_errors)[:errors]).to eq(name: %w[not_valid should_be_present])
36
+ end
37
+
38
+ it "ensures that malformed JSON throws an exception" do
39
+ expect { subject.parse(body_with_malformed_json) }.to raise_error(Him::Errors::ParseError, 'Response from the API must behave like a Hash or an Array (last JSON response was "wut.")')
40
+ end
41
+
42
+ it "ensures that invalid JSON throws an exception" do
43
+ expect { subject.parse(body_with_invalid_json) }.to raise_error(Him::Errors::ParseError, 'Response from the API must behave like a Hash or an Array (last JSON response was "true")')
44
+ end
45
+
46
+ it "ensures that a nil response returns an empty hash" do
47
+ expect(subject.parse(nil_body)[:data]).to eq({})
48
+ end
49
+
50
+ it "ensures that an empty response returns an empty hash" do
51
+ expect(subject.parse(empty_body)[:data]).to eq({})
52
+ end
53
+
54
+ context "with status code 204" do
55
+ it "returns an empty body" do
56
+ env = { status: 204 }
57
+ subject.on_complete(env)
58
+ env[:body].tap do |json|
59
+ expect(json[:data]).to eq({})
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,52 @@
1
+ # encoding: utf-8
2
+
3
+ require "spec_helper"
4
+
5
+ describe Him::Middleware::JsonApiParser do
6
+ subject { described_class.new }
7
+
8
+ context "with valid JSON body" do
9
+ let(:body) { '{"data": {"type": "foo", "id": "bar", "attributes": {"baz": "qux"} }, "meta": {"api": "json api"} }' }
10
+ let(:env) { { body: body } }
11
+
12
+ it "parses body as json" do
13
+ subject.on_complete(env)
14
+ env.fetch(:body).tap do |json|
15
+ expect(json[:data]).to eql(
16
+ type: "foo",
17
+ id: "bar",
18
+ attributes: { baz: "qux" }
19
+ )
20
+ expect(json[:errors]).to eql([])
21
+ expect(json[:metadata]).to eql(api: "json api")
22
+ end
23
+ end
24
+ end
25
+
26
+ context "with status code 204" do
27
+ it "returns an empty body" do
28
+ env = { status: 204 }
29
+ subject.on_complete(env)
30
+ env[:body].tap do |json|
31
+ expect(json[:data]).to eq({})
32
+ end
33
+ end
34
+ end
35
+
36
+ context 'with status code 304' do
37
+ it 'returns an empty body' do
38
+ env = { :status => 304 }
39
+ subject.on_complete(env)
40
+ env[:body].tap do |json|
41
+ expect(json[:data]).to eq({})
42
+ end
43
+ end
44
+ end
45
+
46
+ # context "with invalid JSON body" do
47
+ # let(:body) { '"foo"' }
48
+ # it 'ensures that invalid JSON throws an exception' do
49
+ # expect { subject.parse(body) }.to raise_error(Him::Errors::ParseError, 'Response from the API must behave like a Hash or an Array (last JSON response was "\"foo\"")')
50
+ # end
51
+ # end
52
+ end
@@ -0,0 +1,35 @@
1
+ # encoding: utf-8
2
+
3
+ require "spec_helper"
4
+
5
+ describe Him::Middleware::SecondLevelParseJSON do
6
+ subject { described_class.new }
7
+
8
+ context "with valid JSON body" do
9
+ let(:body) { "{\"data\": 1, \"errors\": 2, \"metadata\": 3}" }
10
+ it "parses body as json" do
11
+ subject.parse(body).tap do |json|
12
+ expect(json[:data]).to eq(1)
13
+ expect(json[:errors]).to eq(2)
14
+ expect(json[:metadata]).to eq(3)
15
+ end
16
+ end
17
+
18
+ it "parses :body key as json in the env hash" do
19
+ env = { body: body }
20
+ subject.on_complete(env)
21
+ env[:body].tap do |json|
22
+ expect(json[:data]).to eq(1)
23
+ expect(json[:errors]).to eq(2)
24
+ expect(json[:metadata]).to eq(3)
25
+ end
26
+ end
27
+ end
28
+
29
+ context "with invalid JSON body" do
30
+ let(:body) { '"foo"' }
31
+ it "ensures that invalid JSON throws an exception" do
32
+ expect { subject.parse(body) }.to raise_error(Him::Errors::ParseError, 'Response from the API must behave like a Hash or an Array (last JSON response was "\"foo\"")')
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+
3
+ require "spec_helper"
4
+
5
+ describe Him::Model::Associations::AssociationProxy do
6
+ describe "proxy assignment methods" do
7
+ before do
8
+ Him::API.setup url: "https://api.example.com" do |builder|
9
+ builder.use Him::Middleware::FirstLevelParseJSON
10
+ builder.use Faraday::Request::UrlEncoded
11
+ builder.adapter :test do |stub|
12
+ stub.get("/users/1") { [200, {}, { id: 1, name: "Tobias Fünke" }.to_json] }
13
+ stub.get("/users/1/fish") { [200, {}, { id: 1, name: "Tobias's Fish" }.to_json] }
14
+ end
15
+ end
16
+ spawn_model "User" do
17
+ has_one :fish
18
+ end
19
+ spawn_model "Fish"
20
+ end
21
+
22
+ subject { User.find(1) }
23
+
24
+ it "should assign value" do
25
+ subject.fish.name = "Fishy"
26
+ expect(subject.fish.name).to eq "Fishy"
27
+ end
28
+ end
29
+ end