restorm 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.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +31 -0
  5. data/.rubocop_todo.yml +232 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +55 -0
  8. data/.yardopts +2 -0
  9. data/CONTRIBUTING.md +26 -0
  10. data/Gemfile +10 -0
  11. data/HER_README.md +1065 -0
  12. data/LICENSE +7 -0
  13. data/README.md +7 -0
  14. data/Rakefile +11 -0
  15. data/UPGRADE.md +101 -0
  16. data/gemfiles/Gemfile.activemodel-4.2 +6 -0
  17. data/gemfiles/Gemfile.activemodel-5.0 +6 -0
  18. data/gemfiles/Gemfile.activemodel-5.1 +6 -0
  19. data/gemfiles/Gemfile.activemodel-5.2 +6 -0
  20. data/gemfiles/Gemfile.faraday-1.0 +6 -0
  21. data/lib/restorm/api.rb +121 -0
  22. data/lib/restorm/collection.rb +13 -0
  23. data/lib/restorm/errors.rb +29 -0
  24. data/lib/restorm/json_api/model.rb +42 -0
  25. data/lib/restorm/middleware/accept_json.rb +18 -0
  26. data/lib/restorm/middleware/first_level_parse_json.rb +37 -0
  27. data/lib/restorm/middleware/json_api_parser.rb +37 -0
  28. data/lib/restorm/middleware/parse_json.rb +22 -0
  29. data/lib/restorm/middleware/second_level_parse_json.rb +37 -0
  30. data/lib/restorm/middleware.rb +12 -0
  31. data/lib/restorm/model/associations/association.rb +128 -0
  32. data/lib/restorm/model/associations/association_proxy.rb +44 -0
  33. data/lib/restorm/model/associations/belongs_to_association.rb +95 -0
  34. data/lib/restorm/model/associations/has_many_association.rb +100 -0
  35. data/lib/restorm/model/associations/has_one_association.rb +79 -0
  36. data/lib/restorm/model/associations.rb +141 -0
  37. data/lib/restorm/model/attributes.rb +322 -0
  38. data/lib/restorm/model/base.rb +33 -0
  39. data/lib/restorm/model/deprecated_methods.rb +61 -0
  40. data/lib/restorm/model/http.rb +119 -0
  41. data/lib/restorm/model/introspection.rb +67 -0
  42. data/lib/restorm/model/nested_attributes.rb +45 -0
  43. data/lib/restorm/model/orm.rb +299 -0
  44. data/lib/restorm/model/parse.rb +223 -0
  45. data/lib/restorm/model/paths.rb +125 -0
  46. data/lib/restorm/model/relation.rb +209 -0
  47. data/lib/restorm/model.rb +75 -0
  48. data/lib/restorm/version.rb +3 -0
  49. data/lib/restorm.rb +19 -0
  50. data/restorm.gemspec +29 -0
  51. data/spec/api_spec.rb +120 -0
  52. data/spec/collection_spec.rb +41 -0
  53. data/spec/json_api/model_spec.rb +169 -0
  54. data/spec/middleware/accept_json_spec.rb +11 -0
  55. data/spec/middleware/first_level_parse_json_spec.rb +63 -0
  56. data/spec/middleware/json_api_parser_spec.rb +52 -0
  57. data/spec/middleware/second_level_parse_json_spec.rb +35 -0
  58. data/spec/model/associations/association_proxy_spec.rb +29 -0
  59. data/spec/model/associations_spec.rb +911 -0
  60. data/spec/model/attributes_spec.rb +354 -0
  61. data/spec/model/callbacks_spec.rb +176 -0
  62. data/spec/model/dirty_spec.rb +133 -0
  63. data/spec/model/http_spec.rb +201 -0
  64. data/spec/model/introspection_spec.rb +81 -0
  65. data/spec/model/nested_attributes_spec.rb +135 -0
  66. data/spec/model/orm_spec.rb +704 -0
  67. data/spec/model/parse_spec.rb +520 -0
  68. data/spec/model/paths_spec.rb +348 -0
  69. data/spec/model/relation_spec.rb +247 -0
  70. data/spec/model/validations_spec.rb +43 -0
  71. data/spec/model_spec.rb +45 -0
  72. data/spec/spec_helper.rb +25 -0
  73. data/spec/support/macros/her_macros.rb +17 -0
  74. data/spec/support/macros/model_macros.rb +36 -0
  75. data/spec/support/macros/request_macros.rb +27 -0
  76. metadata +203 -0
@@ -0,0 +1,201 @@
1
+ # encoding: utf-8
2
+
3
+ require File.join(File.dirname(__FILE__), "../spec_helper.rb")
4
+
5
+ describe Restorm::Model::HTTP do
6
+ context "binding a model with an API" do
7
+ let(:api1) { Restorm::API.new url: "https://api1.example.com" }
8
+ let(:api2) { Restorm::API.new url: "https://api2.example.com" }
9
+
10
+ before do
11
+ spawn_model("Foo::User")
12
+ spawn_model("Foo::Comment")
13
+ Restorm::API.setup url: "https://api.example.com"
14
+ end
15
+
16
+ context "when binding a model to its superclass' her_api" do
17
+ before do
18
+ spawn_model "Foo::Superclass"
19
+ Foo::Superclass.uses_api api1
20
+ Foo::Subclass = Class.new(Foo::Superclass)
21
+ end
22
+
23
+ specify { expect(Foo::Subclass.her_api).to eq(Foo::Superclass.her_api) }
24
+ end
25
+
26
+ context "when changing her_api without changing the parent class' restorm_api" do
27
+ before do
28
+ spawn_model "Foo::Superclass"
29
+ Foo::Subclass = Class.new(Foo::Superclass)
30
+ Foo::Superclass.uses_api api1
31
+ Foo::Subclass.uses_api api2
32
+ end
33
+
34
+ specify { expect(Foo::Subclass.her_api).not_to eq(Foo::Superclass.restorm_api) }
35
+ end
36
+ end
37
+
38
+ context "making HTTP requests" do
39
+ before do
40
+ Restorm::API.setup url: "https://api.example.com" do |builder|
41
+ builder.use Restorm::Middleware::FirstLevelParseJSON
42
+ builder.use Faraday::Request::UrlEncoded
43
+ builder.adapter :test do |stub|
44
+ stub.get("/users") { [200, {}, [{ id: 1 }].to_json] }
45
+ stub.get("/users/1") { [200, {}, { id: 1 }.to_json] }
46
+ stub.get("/users/popular") do |env|
47
+ if env[:params]["page"] == "2"
48
+ [200, {}, [{ id: 3 }, { id: 4 }].to_json]
49
+ else
50
+ [200, {}, [{ id: 1 }, { id: 2 }].to_json]
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ spawn_model "Foo::User"
57
+ end
58
+
59
+ describe :get do
60
+ subject { Foo::User.get(:popular) }
61
+
62
+ describe "#length" do
63
+ subject { super().length }
64
+ it { is_expected.to eq(2) }
65
+ end
66
+ specify { expect(subject.first.id).to eq(1) }
67
+ end
68
+
69
+ describe :get_raw do
70
+ context "with a block" do
71
+ specify do
72
+ Foo::User.get_raw("/users") do |parsed_data, _response|
73
+ expect(parsed_data[:data]).to eq([{ id: 1 }])
74
+ end
75
+ end
76
+ end
77
+
78
+ context "with a return value" do
79
+ subject { Foo::User.get_raw("/users") }
80
+ specify { expect(subject[:parsed_data][:data]).to eq([{ id: 1 }]) }
81
+ end
82
+ end
83
+
84
+ describe :get_collection do
85
+ context "with a String path" do
86
+ subject { Foo::User.get_collection("/users/popular") }
87
+
88
+ describe "#length" do
89
+ subject { super().length }
90
+ it { is_expected.to eq(2) }
91
+ end
92
+ specify { expect(subject.first.id).to eq(1) }
93
+ end
94
+
95
+ context "with a Symbol" do
96
+ subject { Foo::User.get_collection(:popular) }
97
+
98
+ describe "#length" do
99
+ subject { super().length }
100
+ it { is_expected.to eq(2) }
101
+ end
102
+ specify { expect(subject.first.id).to eq(1) }
103
+ end
104
+
105
+ context "with extra parameters" do
106
+ subject { Foo::User.get_collection(:popular, page: 2) }
107
+
108
+ describe "#length" do
109
+ subject { super().length }
110
+ it { is_expected.to eq(2) }
111
+ end
112
+ specify { expect(subject.first.id).to eq(3) }
113
+ end
114
+ end
115
+
116
+ describe :get_resource do
117
+ context "with a String path" do
118
+ subject { Foo::User.get_resource("/users/1") }
119
+
120
+ describe "#id" do
121
+ subject { super().id }
122
+ it { is_expected.to eq(1) }
123
+ end
124
+ end
125
+
126
+ context "with a Symbol" do
127
+ subject { Foo::User.get_resource(:"1") }
128
+
129
+ describe "#id" do
130
+ subject { super().id }
131
+ it { is_expected.to eq(1) }
132
+ end
133
+ end
134
+ end
135
+
136
+ describe :get_raw do
137
+ specify do
138
+ Foo::User.get_raw(:popular) do |parsed_data, _response|
139
+ expect(parsed_data[:data]).to eq([{ id: 1 }, { id: 2 }])
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+ context "setting custom HTTP requests" do
146
+ before do
147
+ Restorm::API.setup url: "https://api.example.com" do |connection|
148
+ connection.use Restorm::Middleware::FirstLevelParseJSON
149
+ connection.adapter :test do |stub|
150
+ stub.get("/users/popular") { [200, {}, [{ id: 1 }, { id: 2 }].to_json] }
151
+ stub.post("/users/from_default") { [200, {}, { id: 4 }.to_json] }
152
+ end
153
+ end
154
+
155
+ spawn_model "Foo::User"
156
+ end
157
+
158
+ subject { Foo::User }
159
+
160
+ describe :custom_get do
161
+ before do
162
+ Foo::User.custom_get :popular, :recent
163
+ end
164
+
165
+ it { is_expected.to respond_to(:popular) }
166
+ it { is_expected.to respond_to(:recent) }
167
+
168
+ it "makes HTTP request" do
169
+ expect(Foo::User.popular.length).to be 2
170
+ end
171
+ end
172
+
173
+ describe :custom_post do
174
+ before do
175
+ Foo::User.custom_post :from_default
176
+ end
177
+
178
+ it { is_expected.to respond_to(:from_default) }
179
+
180
+ it "makes HTTP request" do
181
+ user = Foo::User.from_default(name: "Tobias Fünke")
182
+ expect(user.id).to be 4
183
+ end
184
+ end
185
+
186
+ context "with options" do
187
+ before do
188
+ allow(Foo::User).to receive(:warn)
189
+ Foo::User.custom_get :popular, foo: "bar"
190
+ end
191
+
192
+ it "issues DEPRECATION warning" do
193
+ expect(Foo::User).to have_received(:warn).with("[DEPRECATION] options for custom request methods are deprecated and will be removed on or after January 2020.")
194
+ end
195
+
196
+ it "makes HTTP request" do
197
+ expect(Foo::User.popular.length).to be 2
198
+ end
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,81 @@
1
+ # encoding: utf-8
2
+
3
+ require File.join(File.dirname(__FILE__), "../spec_helper.rb")
4
+
5
+ describe Restorm::Model::Introspection do
6
+ context "introspecting a resource" do
7
+ before do
8
+ Restorm::API.setup url: "https://api.example.com" do |builder|
9
+ builder.use Restorm::Middleware::FirstLevelParseJSON
10
+ builder.use Faraday::Request::UrlEncoded
11
+ builder.adapter :test do |stub|
12
+ stub.post("/users") { [200, {}, { id: 1, name: "Tobias Funke" }.to_json] }
13
+ stub.get("/users/1") { [200, {}, { id: 1, name: "Tobias Funke" }.to_json] }
14
+ stub.put("/users/1") { [200, {}, { id: 1, name: "Tobias Funke" }.to_json] }
15
+ stub.delete("/users/1") { [200, {}, { id: 1, name: "Tobias Funke" }.to_json] }
16
+ stub.get("/projects/1/comments") { [200, {}, [{ id: 1, body: "Hello!" }].to_json] }
17
+ end
18
+ end
19
+
20
+ spawn_model "Foo::User"
21
+ spawn_model "Foo::Comment" do
22
+ collection_path "projects/:project_id/comments"
23
+ end
24
+ end
25
+
26
+ describe "#inspect" do
27
+ it "outputs resource attributes for an existing resource" do
28
+ @user = Foo::User.find(1)
29
+ expect(["#<Foo::User(users/1) name=\"Tobias Funke\" id=1>", "#<Foo::User(users/1) id=1 name=\"Tobias Funke\">"]).to include(@user.inspect)
30
+ end
31
+
32
+ it "outputs resource attributes for an not-saved-yet resource" do
33
+ @user = Foo::User.new(name: "Tobias Funke")
34
+ expect(@user.inspect).to eq("#<Foo::User(users) name=\"Tobias Funke\">")
35
+ end
36
+
37
+ it "outputs resource attributes using getters" do
38
+ @user = Foo::User.new(name: "Tobias Funke", password: "Funke")
39
+ @user.instance_eval do
40
+ def password
41
+ "filtered"
42
+ end
43
+ end
44
+ expect(@user.inspect).to include("name=\"Tobias Funke\"")
45
+ expect(@user.inspect).to include("password=\"filtered\"")
46
+ expect(@user.inspect).not_to include("password=\"Funke\"")
47
+ end
48
+
49
+ it "support dash on attribute" do
50
+ @user = Foo::User.new(:'life-span' => "3 years")
51
+ expect(@user.inspect).to include("life-span=\"3 years\"")
52
+ end
53
+ end
54
+
55
+ describe "#inspect with errors in resource path" do
56
+ it "prints the resource path as “unknown”" do
57
+ @comment = Foo::Comment.where(project_id: 1).first
58
+ path = "<unknown path, missing `project_id`>"
59
+ expect(["#<Foo::Comment(#{path}) body=\"Hello!\" id=1>", "#<Foo::Comment(#{path}) id=1 body=\"Hello!\">"]).to include(@comment.inspect)
60
+ end
61
+ end
62
+ end
63
+
64
+ describe "#restorm_nearby_class" do
65
+ context "for a class inside of a module" do
66
+ before do
67
+ spawn_model "Foo::User"
68
+ spawn_model "Foo::AccessRecord"
69
+ spawn_model "AccessRecord"
70
+ spawn_model "Log"
71
+ end
72
+
73
+ it "returns a sibling class, if found" do
74
+ expect(Foo::User.restorm_nearby_class("AccessRecord")).to eq(Foo::AccessRecord)
75
+ expect(AccessRecord.restorm_nearby_class("Log")).to eq(Log)
76
+ expect(Foo::User.restorm_nearby_class("Log")).to eq(Log)
77
+ expect { Foo::User.restorm_nearby_class("X") }.to raise_error(NameError)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,135 @@
1
+ # encoding: utf-8
2
+
3
+ require File.join(File.dirname(__FILE__), "../spec_helper.rb")
4
+
5
+ describe Restorm::Model::NestedAttributes do
6
+ context "with a belongs_to association" do
7
+ before do
8
+ Restorm::API.setup url: "https://api.example.com" do |builder|
9
+ builder.use Restorm::Middleware::FirstLevelParseJSON
10
+ builder.use Faraday::Request::UrlEncoded
11
+ end
12
+
13
+ spawn_model "Foo::User" do
14
+ belongs_to :company, path: "/organizations/:id", foreign_key: :organization_id
15
+ accepts_nested_attributes_for :company
16
+ end
17
+
18
+ spawn_model "Foo::Company"
19
+
20
+ @user_with_data_through_nested_attributes = Foo::User.new name: "Test", company_attributes: { name: "Example Company" }
21
+ end
22
+
23
+ context "when child does not yet exist" do
24
+ it "creates an instance of the associated class" do
25
+ expect(@user_with_data_through_nested_attributes.company).to be_a(Foo::Company)
26
+ expect(@user_with_data_through_nested_attributes.company.name).to eq("Example Company")
27
+ end
28
+ end
29
+
30
+ context "when child does exist" do
31
+ it "updates the attributes of the associated object" do
32
+ @user_with_data_through_nested_attributes.company_attributes = { name: "Fünke's Company" }
33
+ expect(@user_with_data_through_nested_attributes.company).to be_a(Foo::Company)
34
+ expect(@user_with_data_through_nested_attributes.company.name).to eq("Fünke's Company")
35
+ end
36
+ end
37
+ end
38
+
39
+ context "with a has_one association" do
40
+ before do
41
+ Restorm::API.setup url: "https://api.example.com" do |builder|
42
+ builder.use Restorm::Middleware::FirstLevelParseJSON
43
+ builder.use Faraday::Request::UrlEncoded
44
+ end
45
+
46
+ spawn_model "Foo::User" do
47
+ has_one :pet
48
+ accepts_nested_attributes_for :pet
49
+ end
50
+
51
+ spawn_model "Foo::Pet"
52
+
53
+ @user_with_data_through_nested_attributes = Foo::User.new name: "Test", pet_attributes: { name: "Hasi" }
54
+ end
55
+
56
+ context "when child does not yet exist" do
57
+ it "creates an instance of the associated class" do
58
+ expect(@user_with_data_through_nested_attributes.pet).to be_a(Foo::Pet)
59
+ expect(@user_with_data_through_nested_attributes.pet.name).to eq("Hasi")
60
+ end
61
+ end
62
+
63
+ context "when child does exist" do
64
+ it "updates the attributes of the associated object" do
65
+ @user_with_data_through_nested_attributes.pet_attributes = { name: "Rodriguez" }
66
+ expect(@user_with_data_through_nested_attributes.pet).to be_a(Foo::Pet)
67
+ expect(@user_with_data_through_nested_attributes.pet.name).to eq("Rodriguez")
68
+ end
69
+ end
70
+ end
71
+
72
+ context "with a has_many association" do
73
+ before do
74
+ Restorm::API.setup url: "https://api.example.com" do |builder|
75
+ builder.use Restorm::Middleware::FirstLevelParseJSON
76
+ builder.use Faraday::Request::UrlEncoded
77
+ end
78
+
79
+ spawn_model "Foo::User" do
80
+ has_many :pets
81
+ accepts_nested_attributes_for :pets
82
+ end
83
+
84
+ spawn_model "Foo::Pet"
85
+
86
+ @user_with_data_through_nested_attributes = Foo::User.new name: "Test", pets_attributes: [{ name: "Hasi" }, { name: "Rodriguez" }]
87
+ end
88
+
89
+ context "when children do not yet exist" do
90
+ it "creates an instance of the associated class" do
91
+ expect(@user_with_data_through_nested_attributes.pets.length).to eq(2)
92
+ expect(@user_with_data_through_nested_attributes.pets[0]).to be_a(Foo::Pet)
93
+ expect(@user_with_data_through_nested_attributes.pets[1]).to be_a(Foo::Pet)
94
+ expect(@user_with_data_through_nested_attributes.pets[0].name).to eq("Hasi")
95
+ expect(@user_with_data_through_nested_attributes.pets[1].name).to eq("Rodriguez")
96
+ end
97
+ end
98
+ end
99
+
100
+ context "with a has_many association as a Hash" do
101
+ before do
102
+ Restorm::API.setup url: "https://api.example.com" do |builder|
103
+ builder.use Restorm::Middleware::FirstLevelParseJSON
104
+ builder.use Faraday::Request::UrlEncoded
105
+ end
106
+
107
+ spawn_model "Foo::User" do
108
+ has_many :pets
109
+ accepts_nested_attributes_for :pets
110
+ end
111
+
112
+ spawn_model "Foo::Pet"
113
+
114
+ @user_with_data_through_nested_attributes_as_hash = Foo::User.new name: "Test", pets_attributes: { "0" => { name: "Hasi" }, "1" => { name: "Rodriguez" } }
115
+ end
116
+
117
+ context "when children do not yet exist" do
118
+ it "creates an instance of the associated class" do
119
+ expect(@user_with_data_through_nested_attributes_as_hash.pets.length).to eq(2)
120
+ expect(@user_with_data_through_nested_attributes_as_hash.pets[0]).to be_a(Foo::Pet)
121
+ expect(@user_with_data_through_nested_attributes_as_hash.pets[1]).to be_a(Foo::Pet)
122
+ expect(@user_with_data_through_nested_attributes_as_hash.pets[0].name).to eq("Hasi")
123
+ expect(@user_with_data_through_nested_attributes_as_hash.pets[1].name).to eq("Rodriguez")
124
+ end
125
+ end
126
+ end
127
+
128
+ context "with an unknown association" do
129
+ it "raises an error" do
130
+ expect do
131
+ spawn_model("Foo::User") { accepts_nested_attributes_for :company }
132
+ end.to raise_error(Restorm::Errors::AssociationUnknownError, "Unknown association name :company")
133
+ end
134
+ end
135
+ end