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,91 @@
1
+ # encoding: utf-8
2
+ require File.join(File.dirname(__FILE__), "../spec_helper.rb")
3
+
4
+ describe "Her::Model and ActiveModel::Dirty" do
5
+ context "checking dirty attributes" do
6
+ before do
7
+ Her::API.setup :url => "https://api.example.com" do |builder|
8
+ builder.use Her::Middleware::FirstLevelParseJSON
9
+ builder.use Faraday::Request::UrlEncoded
10
+ builder.adapter :test do |stub|
11
+ stub.get("/users/1") { |env| [200, {}, { :id => 1, :fullname => "Lindsay Fünke" }.to_json] }
12
+ stub.get("/users/2") { |env| [200, {}, { :id => 2, :fullname => "Maeby Fünke" }.to_json] }
13
+ stub.get("/users/3") { |env| [200, {}, { :user_id => 3, :fullname => "Maeby Fünke" }.to_json] }
14
+ stub.put("/users/1") { |env| [200, {}, { :id => 1, :fullname => "Tobias Fünke" }.to_json] }
15
+ stub.put("/users/2") { |env| [400, {}, { :errors => ["Email cannot be blank"] }.to_json] }
16
+ stub.post("/users") { |env| [200, {}, { :id => 1, :fullname => "Tobias Fünke" }.to_json] }
17
+ end
18
+ end
19
+
20
+ spawn_model "Foo::User" do
21
+ attributes :fullname, :email
22
+ end
23
+ spawn_model "Dynamic::User" do
24
+ primary_key :user_id
25
+ end
26
+ end
27
+
28
+ context "for existing resource" do
29
+ let(:user) { Foo::User.find(1) }
30
+ it "has no changes" do
31
+ expect(user.changes).to be_empty
32
+ expect(user).not_to be_changed
33
+ end
34
+ context "with successful save" do
35
+ it "tracks dirty attributes" do
36
+ user.fullname = "Tobias Fünke"
37
+ expect(user.fullname_changed?).to be_truthy
38
+ expect(user.email_changed?).to be_falsey
39
+ expect(user).to be_changed
40
+ user.save
41
+ expect(user).not_to be_changed
42
+ end
43
+
44
+ it "tracks only changed dirty attributes" do
45
+ user.fullname = user.fullname
46
+ expect(user.fullname_changed?).to be_falsey
47
+ end
48
+
49
+ it "tracks previous changes" do
50
+ user.fullname = "Tobias Fünke"
51
+ user.save
52
+ expect(user.previous_changes).to eq({"fullname"=>"Lindsay Fünke"})
53
+ end
54
+
55
+ it 'tracks dirty attribute for mass assign for dynamic created attributes' do
56
+ user = Dynamic::User.find(3)
57
+ user.assign_attributes(:fullname => 'New Fullname')
58
+ expect(user.fullname_changed?).to be_truthy
59
+ expect(user).to be_changed
60
+ expect(user.changes.length).to eq(1)
61
+ end
62
+ end
63
+
64
+ context "with erroneous save" do
65
+ it "tracks dirty attributes" do
66
+ user = Foo::User.find(2)
67
+ user.fullname = "Tobias Fünke"
68
+ expect(user.fullname_changed?).to be_truthy
69
+ expect(user.email_changed?).to be_falsey
70
+ expect(user).to be_changed
71
+ user.save
72
+ expect(user).to be_changed
73
+ end
74
+ end
75
+ end
76
+
77
+ context "for new resource" do
78
+ let(:user) { Foo::User.new(:fullname => "Lindsay Fünke") }
79
+ it "has changes" do
80
+ expect(user).to be_changed
81
+ end
82
+ it "tracks dirty attributes" do
83
+ user.fullname = "Tobias Fünke"
84
+ expect(user.fullname_changed?).to be_truthy
85
+ expect(user).to be_changed
86
+ user.save
87
+ expect(user).not_to be_changed
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,158 @@
1
+ # encoding: utf-8
2
+ require File.join(File.dirname(__FILE__), "../spec_helper.rb")
3
+
4
+ describe Her::Model::HTTP do
5
+ context "binding a model with an API" do
6
+ let(:api1) { Her::API.new :url => "https://api1.example.com" }
7
+ let(:api2) { Her::API.new :url => "https://api2.example.com" }
8
+
9
+ before do
10
+ spawn_model("Foo::User")
11
+ spawn_model("Foo::Comment")
12
+ Her::API.setup :url => "https://api.example.com"
13
+ end
14
+
15
+ context "when binding a model to its superclass' her_api" do
16
+ before do
17
+ spawn_model "Foo::Superclass"
18
+ Foo::Superclass.uses_api api1
19
+ Foo::Subclass = Class.new(Foo::Superclass)
20
+ end
21
+
22
+ specify { expect(Foo::Subclass.her_api).to eq(Foo::Superclass.her_api) }
23
+ end
24
+
25
+ context "when changing her_api without changing the parent class' her_api" do
26
+ before do
27
+ spawn_model "Foo::Superclass"
28
+ Foo::Subclass = Class.new(Foo::Superclass)
29
+ Foo::Superclass.uses_api api1
30
+ Foo::Subclass.uses_api api2
31
+ end
32
+
33
+ specify { expect(Foo::Subclass.her_api).not_to eq(Foo::Superclass.her_api) }
34
+ end
35
+ end
36
+
37
+ context "making HTTP requests" do
38
+ before do
39
+ Her::API.setup :url => "https://api.example.com" do |builder|
40
+ builder.use Her::Middleware::FirstLevelParseJSON
41
+ builder.use Faraday::Request::UrlEncoded
42
+ builder.adapter :test do |stub|
43
+ stub.get("/users") { |env| [200, {}, [{ :id => 1 }].to_json] }
44
+ stub.get("/users/1") { |env| [200, {}, { :id => 1 }.to_json] }
45
+ stub.get("/users/popular") do |env|
46
+ if env[:params]["page"] == "2"
47
+ [200, {}, [{ :id => 3 }, { :id => 4 }].to_json]
48
+ else
49
+ [200, {}, [{ :id => 1 }, { :id => 2 }].to_json]
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ spawn_model "Foo::User"
56
+ end
57
+
58
+ describe :get do
59
+ subject { Foo::User.get(:popular) }
60
+ its(:length) { should == 2 }
61
+ specify { expect(subject.first.id).to eq(1) }
62
+ end
63
+
64
+ describe :get_raw do
65
+ context "with a block" do
66
+ specify do
67
+ Foo::User.get_raw("/users") do |parsed_data, response|
68
+ expect(parsed_data[:data]).to eq([{ :id => 1 }])
69
+ end
70
+ end
71
+ end
72
+
73
+ context "with a return value" do
74
+ subject { Foo::User.get_raw("/users") }
75
+ specify { expect(subject[:parsed_data][:data]).to eq([{ :id => 1 }]) }
76
+ end
77
+ end
78
+
79
+ describe :get_collection do
80
+ context "with a String path" do
81
+ subject { Foo::User.get_collection("/users/popular") }
82
+ its(:length) { should == 2 }
83
+ specify { expect(subject.first.id).to eq(1) }
84
+ end
85
+
86
+ context "with a Symbol" do
87
+ subject { Foo::User.get_collection(:popular) }
88
+ its(:length) { should == 2 }
89
+ specify { expect(subject.first.id).to eq(1) }
90
+ end
91
+
92
+ context "with extra parameters" do
93
+ subject { Foo::User.get_collection(:popular, :page => 2) }
94
+ its(:length) { should == 2 }
95
+ specify { expect(subject.first.id).to eq(3) }
96
+ end
97
+ end
98
+
99
+ describe :get_resource do
100
+ context "with a String path" do
101
+ subject { Foo::User.get_resource("/users/1") }
102
+ its(:id) { should == 1 }
103
+ end
104
+
105
+ context "with a Symbol" do
106
+ subject { Foo::User.get_resource(:"1") }
107
+ its(:id) { should == 1 }
108
+ end
109
+ end
110
+
111
+ describe :get_raw do
112
+ specify do
113
+ Foo::User.get_raw(:popular) do |parsed_data, response|
114
+ expect(parsed_data[:data]).to eq([{ :id => 1 }, { :id => 2 }])
115
+ end
116
+ end
117
+ end
118
+ end
119
+
120
+ context "setting custom HTTP requests" do
121
+ before do
122
+ Her::API.setup :url => "https://api.example.com" do |connection|
123
+ connection.use Her::Middleware::FirstLevelParseJSON
124
+ connection.adapter :test do |stub|
125
+ stub.get("/users/popular") { |env| [200, {}, [{ :id => 1 }, { :id => 2 }].to_json] }
126
+ stub.post("/users/from_default") { |env| [200, {}, { :id => 4 }.to_json] }
127
+ end
128
+ end
129
+
130
+ spawn_model "Foo::User"
131
+ end
132
+
133
+ subject { Foo::User }
134
+
135
+ describe :custom_get do
136
+ context "without cache" do
137
+ before { Foo::User.custom_get :popular, :recent }
138
+ it { should respond_to(:popular) }
139
+ it { should respond_to(:recent) }
140
+
141
+ context "making the HTTP request" do
142
+ subject { Foo::User.popular }
143
+ its(:length) { should == 2 }
144
+ end
145
+ end
146
+ end
147
+
148
+ describe :custom_post do
149
+ before { Foo::User.custom_post :from_default }
150
+ it { should respond_to(:from_default) }
151
+
152
+ context "making the HTTP request" do
153
+ subject { Foo::User.from_default(:name => "Tobias Fünke") }
154
+ its(:id) { should == 4 }
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,76 @@
1
+ # encoding: utf-8
2
+ require File.join(File.dirname(__FILE__), "../spec_helper.rb")
3
+
4
+ describe Her::Model::Introspection do
5
+ context "introspecting a resource" do
6
+ before do
7
+ Her::API.setup :url => "https://api.example.com" do |builder|
8
+ builder.use Her::Middleware::FirstLevelParseJSON
9
+ builder.use Faraday::Request::UrlEncoded
10
+ builder.adapter :test do |stub|
11
+ stub.post("/users") { |env| [200, {}, { :id => 1, :name => "Tobias Funke" }.to_json] }
12
+ stub.get("/users/1") { |env| [200, {}, { :id => 1, :name => "Tobias Funke" }.to_json] }
13
+ stub.put("/users/1") { |env| [200, {}, { :id => 1, :name => "Tobias Funke" }.to_json] }
14
+ stub.delete("/users/1") { |env| [200, {}, { :id => 1, :name => "Tobias Funke" }.to_json] }
15
+ stub.get("/projects/1/comments") { |env| [200, {}, [{ :id => 1, :body => "Hello!" }].to_json] }
16
+ end
17
+ end
18
+
19
+ spawn_model "Foo::User"
20
+ spawn_model "Foo::Comment" do
21
+ collection_path "projects/:project_id/comments"
22
+ end
23
+ end
24
+
25
+ describe "#inspect" do
26
+ it "outputs resource attributes for an existing resource" do
27
+ @user = Foo::User.find(1)
28
+ expect(["#<Foo::User(users/1) name=\"Tobias Funke\" id=1>", "#<Foo::User(users/1) id=1 name=\"Tobias Funke\">"]).to include(@user.inspect)
29
+ end
30
+
31
+ it "outputs resource attributes for an not-saved-yet resource" do
32
+ @user = Foo::User.new(:name => "Tobias Funke")
33
+ expect(@user.inspect).to eq("#<Foo::User(users) name=\"Tobias Funke\">")
34
+ end
35
+
36
+ it "outputs resource attributes using getters" do
37
+ @user = Foo::User.new(:name => "Tobias Funke", :password => "Funke")
38
+ @user.instance_eval {def password; 'filtered'; end}
39
+ expect(@user.inspect).to include("name=\"Tobias Funke\"")
40
+ expect(@user.inspect).to include("password=\"filtered\"")
41
+ expect(@user.inspect).not_to include("password=\"Funke\"")
42
+ end
43
+
44
+ it "support dash on attribute" do
45
+ @user = Foo::User.new(:'life-span' => "3 years")
46
+ expect(@user.inspect).to include("life-span=\"3 years\"")
47
+ end
48
+ end
49
+
50
+ describe "#inspect with errors in resource path" do
51
+ it "prints the resource path as “unknown”" do
52
+ @comment = Foo::Comment.where(:project_id => 1).first
53
+ path = '<unknown path, missing `project_id`>'
54
+ expect(["#<Foo::Comment(#{path}) body=\"Hello!\" id=1>", "#<Foo::Comment(#{path}) id=1 body=\"Hello!\">"]).to include(@comment.inspect)
55
+ end
56
+ end
57
+ end
58
+
59
+ describe "#her_nearby_class" do
60
+ context "for a class inside of a module" do
61
+ before do
62
+ spawn_model "Foo::User"
63
+ spawn_model "Foo::AccessRecord"
64
+ spawn_model "AccessRecord"
65
+ spawn_model "Log"
66
+ end
67
+
68
+ it "returns a sibling class, if found" do
69
+ expect(Foo::User.her_nearby_class("AccessRecord")).to eq(Foo::AccessRecord)
70
+ expect(AccessRecord.her_nearby_class("Log")).to eq(Log)
71
+ expect(Foo::User.her_nearby_class("Log")).to eq(Log)
72
+ expect(Foo::User.her_nearby_class("X")).to eq(nil)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,134 @@
1
+ # encoding: utf-8
2
+ require File.join(File.dirname(__FILE__), "../spec_helper.rb")
3
+
4
+ describe Her::Model::NestedAttributes do
5
+ context "with a belongs_to association" do
6
+ before do
7
+ Her::API.setup :url => "https://api.example.com" do |builder|
8
+ builder.use Her::Middleware::FirstLevelParseJSON
9
+ builder.use Faraday::Request::UrlEncoded
10
+ end
11
+
12
+ spawn_model "Foo::User" do
13
+ belongs_to :company, :path => "/organizations/:id", :foreign_key => :organization_id
14
+ accepts_nested_attributes_for :company
15
+ end
16
+
17
+ spawn_model "Foo::Company"
18
+
19
+ @user_with_data_through_nested_attributes = Foo::User.new :name => "Test", :company_attributes => { :name => "Example Company" }
20
+ end
21
+
22
+ context "when child does not yet exist" do
23
+ it "creates an instance of the associated class" do
24
+ expect(@user_with_data_through_nested_attributes.company).to be_a(Foo::Company)
25
+ expect(@user_with_data_through_nested_attributes.company.name).to eq("Example Company")
26
+ end
27
+ end
28
+
29
+ context "when child does exist" do
30
+ it "updates the attributes of the associated object" do
31
+ @user_with_data_through_nested_attributes.company_attributes = { :name => "Fünke's Company" }
32
+ expect(@user_with_data_through_nested_attributes.company).to be_a(Foo::Company)
33
+ expect(@user_with_data_through_nested_attributes.company.name).to eq("Fünke's Company")
34
+ end
35
+ end
36
+ end
37
+
38
+ context "with a has_one association" do
39
+ before do
40
+ Her::API.setup :url => "https://api.example.com" do |builder|
41
+ builder.use Her::Middleware::FirstLevelParseJSON
42
+ builder.use Faraday::Request::UrlEncoded
43
+ end
44
+
45
+ spawn_model "Foo::User" do
46
+ has_one :pet
47
+ accepts_nested_attributes_for :pet
48
+ end
49
+
50
+ spawn_model "Foo::Pet"
51
+
52
+ @user_with_data_through_nested_attributes = Foo::User.new :name => "Test", :pet_attributes => { :name => "Hasi" }
53
+ end
54
+
55
+ context "when child does not yet exist" do
56
+ it "creates an instance of the associated class" do
57
+ expect(@user_with_data_through_nested_attributes.pet).to be_a(Foo::Pet)
58
+ expect(@user_with_data_through_nested_attributes.pet.name).to eq("Hasi")
59
+ end
60
+ end
61
+
62
+ context "when child does exist" do
63
+ it "updates the attributes of the associated object" do
64
+ @user_with_data_through_nested_attributes.pet_attributes = { :name => "Rodriguez" }
65
+ expect(@user_with_data_through_nested_attributes.pet).to be_a(Foo::Pet)
66
+ expect(@user_with_data_through_nested_attributes.pet.name).to eq("Rodriguez")
67
+ end
68
+ end
69
+ end
70
+
71
+ context "with a has_many association" do
72
+ before do
73
+ Her::API.setup :url => "https://api.example.com" do |builder|
74
+ builder.use Her::Middleware::FirstLevelParseJSON
75
+ builder.use Faraday::Request::UrlEncoded
76
+ end
77
+
78
+ spawn_model "Foo::User" do
79
+ has_many :pets
80
+ accepts_nested_attributes_for :pets
81
+ end
82
+
83
+ spawn_model "Foo::Pet"
84
+
85
+ @user_with_data_through_nested_attributes = Foo::User.new :name => "Test", :pets_attributes => [{ :name => "Hasi" }, { :name => "Rodriguez" }]
86
+ end
87
+
88
+ context "when children do not yet exist" do
89
+ it "creates an instance of the associated class" do
90
+ expect(@user_with_data_through_nested_attributes.pets.length).to eq(2)
91
+ expect(@user_with_data_through_nested_attributes.pets[0]).to be_a(Foo::Pet)
92
+ expect(@user_with_data_through_nested_attributes.pets[1]).to be_a(Foo::Pet)
93
+ expect(@user_with_data_through_nested_attributes.pets[0].name).to eq("Hasi")
94
+ expect(@user_with_data_through_nested_attributes.pets[1].name).to eq("Rodriguez")
95
+ end
96
+ end
97
+ end
98
+
99
+ context "with a has_many association as a Hash" do
100
+ before do
101
+ Her::API.setup :url => "https://api.example.com" do |builder|
102
+ builder.use Her::Middleware::FirstLevelParseJSON
103
+ builder.use Faraday::Request::UrlEncoded
104
+ end
105
+
106
+ spawn_model "Foo::User" do
107
+ has_many :pets
108
+ accepts_nested_attributes_for :pets
109
+ end
110
+
111
+ spawn_model "Foo::Pet"
112
+
113
+ @user_with_data_through_nested_attributes_as_hash = Foo::User.new :name => "Test", :pets_attributes => { '0' => { :name => "Hasi" }, '1' => { :name => "Rodriguez" }}
114
+ end
115
+
116
+ context "when children do not yet exist" do
117
+ it "creates an instance of the associated class" do
118
+ expect(@user_with_data_through_nested_attributes_as_hash.pets.length).to eq(2)
119
+ expect(@user_with_data_through_nested_attributes_as_hash.pets[0]).to be_a(Foo::Pet)
120
+ expect(@user_with_data_through_nested_attributes_as_hash.pets[1]).to be_a(Foo::Pet)
121
+ expect(@user_with_data_through_nested_attributes_as_hash.pets[0].name).to eq("Hasi")
122
+ expect(@user_with_data_through_nested_attributes_as_hash.pets[1].name).to eq("Rodriguez")
123
+ end
124
+ end
125
+ end
126
+
127
+ context "with an unknown association" do
128
+ it "raises an error" do
129
+ expect {
130
+ spawn_model("Foo::User") { accepts_nested_attributes_for :company }
131
+ }.to raise_error(Her::Errors::AssociationUnknownError, 'Unknown association name :company')
132
+ end
133
+ end
134
+ end