her5 0.8.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 +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