her5 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -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 +101 -0
  12. data/gemfiles/Gemfile.activemodel-3.2.x +7 -0
  13. data/gemfiles/Gemfile.activemodel-4.0 +7 -0
  14. data/gemfiles/Gemfile.activemodel-4.1 +7 -0
  15. data/gemfiles/Gemfile.activemodel-4.2 +7 -0
  16. data/gemfiles/Gemfile.activemodel-5.0.x +7 -0
  17. data/her5.gemspec +30 -0
  18. data/lib/her.rb +19 -0
  19. data/lib/her/api.rb +120 -0
  20. data/lib/her/collection.rb +12 -0
  21. data/lib/her/errors.rb +104 -0
  22. data/lib/her/json_api/model.rb +57 -0
  23. data/lib/her/middleware.rb +12 -0
  24. data/lib/her/middleware/accept_json.rb +17 -0
  25. data/lib/her/middleware/first_level_parse_json.rb +36 -0
  26. data/lib/her/middleware/json_api_parser.rb +68 -0
  27. data/lib/her/middleware/parse_json.rb +28 -0
  28. data/lib/her/middleware/second_level_parse_json.rb +36 -0
  29. data/lib/her/model.rb +75 -0
  30. data/lib/her/model/associations.rb +141 -0
  31. data/lib/her/model/associations/association.rb +107 -0
  32. data/lib/her/model/associations/association_proxy.rb +45 -0
  33. data/lib/her/model/associations/belongs_to_association.rb +101 -0
  34. data/lib/her/model/associations/has_many_association.rb +101 -0
  35. data/lib/her/model/associations/has_one_association.rb +80 -0
  36. data/lib/her/model/attributes.rb +297 -0
  37. data/lib/her/model/base.rb +33 -0
  38. data/lib/her/model/deprecated_methods.rb +61 -0
  39. data/lib/her/model/http.rb +113 -0
  40. data/lib/her/model/introspection.rb +65 -0
  41. data/lib/her/model/nested_attributes.rb +84 -0
  42. data/lib/her/model/orm.rb +207 -0
  43. data/lib/her/model/parse.rb +221 -0
  44. data/lib/her/model/paths.rb +126 -0
  45. data/lib/her/model/relation.rb +164 -0
  46. data/lib/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 +305 -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 +289 -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
+ user.changes.should be_empty
32
+ user.should_not be_changed
33
+ end
34
+ context "with successful save" do
35
+ it "tracks dirty attributes" do
36
+ user.fullname = "Tobias Fünke"
37
+ user.fullname_changed?.should be_truthy
38
+ user.email_changed?.should be_falsey
39
+ user.should be_changed
40
+ user.save
41
+ user.should_not be_changed
42
+ end
43
+
44
+ it "tracks only changed dirty attributes" do
45
+ user.fullname = user.fullname
46
+ user.fullname_changed?.should be_falsey
47
+ end
48
+
49
+ it "tracks previous changes" do
50
+ user.fullname = "Tobias Fünke"
51
+ user.save
52
+ user.previous_changes.should 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
+ user.fullname_changed?.should be_truthy
59
+ user.should be_changed
60
+ user.changes.length.should 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
+ user.fullname_changed?.should be_truthy
69
+ user.email_changed?.should be_falsey
70
+ user.should be_changed
71
+ user.save
72
+ user.should 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
+ user.should be_changed
81
+ end
82
+ it "tracks dirty attributes" do
83
+ user.fullname = "Tobias Fünke"
84
+ user.fullname_changed?.should be_truthy
85
+ user.should be_changed
86
+ user.save
87
+ user.should_not 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 { Foo::Subclass.her_api.should == 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 { Foo::Subclass.her_api.should_not == 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 { subject.first.id.should == 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
+ parsed_data[:data].should == [{ :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 { subject[:parsed_data][:data].should == [{ :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 { subject.first.id.should == 1 }
84
+ end
85
+
86
+ context "with a Symbol" do
87
+ subject { Foo::User.get_collection(:popular) }
88
+ its(:length) { should == 2 }
89
+ specify { subject.first.id.should == 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 { subject.first.id.should == 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
+ parsed_data[:data].should == [{ :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
+ ["#<Foo::User(users/1) name=\"Tobias Funke\" id=1>", "#<Foo::User(users/1) id=1 name=\"Tobias Funke\">"].should 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
+ @user.inspect.should == "#<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
+ @user.inspect.should include("name=\"Tobias Funke\"")
40
+ @user.inspect.should include("password=\"filtered\"")
41
+ @user.inspect.should_not include("password=\"Funke\"")
42
+ end
43
+
44
+ it "support dash on attribute" do
45
+ @user = Foo::User.new(:'life-span' => "3 years")
46
+ @user.inspect.should 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
+ ["#<Foo::Comment(#{path}) body=\"Hello!\" id=1>", "#<Foo::Comment(#{path}) id=1 body=\"Hello!\">"].should 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
+ Foo::User.her_nearby_class("AccessRecord").should == Foo::AccessRecord
70
+ AccessRecord.her_nearby_class("Log").should == Log
71
+ Foo::User.her_nearby_class("Log").should == Log
72
+ Foo::User.her_nearby_class("X").should be_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
+ @user_with_data_through_nested_attributes.company.should be_a(Foo::Company)
25
+ @user_with_data_through_nested_attributes.company.name.should == "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
+ @user_with_data_through_nested_attributes.company.should be_a(Foo::Company)
33
+ @user_with_data_through_nested_attributes.company.name.should == "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
+ @user_with_data_through_nested_attributes.pet.should be_a(Foo::Pet)
58
+ @user_with_data_through_nested_attributes.pet.name.should == "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
+ @user_with_data_through_nested_attributes.pet.should be_a(Foo::Pet)
66
+ @user_with_data_through_nested_attributes.pet.name.should == "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
+ @user_with_data_through_nested_attributes.pets.length.should == 2
91
+ @user_with_data_through_nested_attributes.pets[0].should be_a(Foo::Pet)
92
+ @user_with_data_through_nested_attributes.pets[1].should be_a(Foo::Pet)
93
+ @user_with_data_through_nested_attributes.pets[0].name.should == "Hasi"
94
+ @user_with_data_through_nested_attributes.pets[1].name.should == "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
+ @user_with_data_through_nested_attributes_as_hash.pets.length.should == 2
119
+ @user_with_data_through_nested_attributes_as_hash.pets[0].should be_a(Foo::Pet)
120
+ @user_with_data_through_nested_attributes_as_hash.pets[1].should be_a(Foo::Pet)
121
+ @user_with_data_through_nested_attributes_as_hash.pets[0].name.should == "Hasi"
122
+ @user_with_data_through_nested_attributes_as_hash.pets[1].name.should == "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