frenchy 0.0.9 → 0.2.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.
@@ -0,0 +1,38 @@
1
+ require "spec_helper"
2
+
3
+ class MyModel
4
+ include Frenchy::Model
5
+ end
6
+
7
+ class MyModelDecorator
8
+ def self.decorate_collection(collection, options={})
9
+ return "DECORATED"
10
+ end
11
+ end
12
+
13
+ describe Frenchy::Collection do
14
+ describe "#decorate" do
15
+ describe "when there are no items" do
16
+ it "returns an empty array" do
17
+ coll = Frenchy::Collection.new
18
+ expect(coll.decorate).to eql([])
19
+ end
20
+ end
21
+
22
+ describe "when there are model items" do
23
+ it "decorates using the named convention" do
24
+ m1 = MyModel.new
25
+ m2 = MyModel.new
26
+ coll = Frenchy::Collection.new([m1, m2])
27
+ expect(coll.decorate).to eql("DECORATED")
28
+ end
29
+
30
+ it "supports a hash of options" do
31
+ m1 = MyModel.new
32
+ m2 = MyModel.new
33
+ coll = Frenchy::Collection.new([m1, m2])
34
+ expect(coll.decorate({"a" => 1})).to eq("DECORATED") # test arity
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,42 @@
1
+ require "spec_helper"
2
+
3
+ class MyOtherClass
4
+ include Frenchy::Model
5
+ end
6
+
7
+ class MyClass
8
+ include Frenchy::Model
9
+
10
+ field :other, type: "my_other_class"
11
+ end
12
+
13
+ describe Hash do
14
+ describe "#stringify_keys" do
15
+ it "converts symbol keyed has to string keyed" do
16
+ expect({a: 1, b: 2}.stringify_keys!).to eql({"a" => 1, "b" => 2})
17
+ end
18
+ end
19
+ end
20
+
21
+ describe String do
22
+ describe "#constantize" do
23
+ it "properly constantizes a string" do
24
+ expect("MyClass".constantize).to eql(MyClass)
25
+ end
26
+ end
27
+
28
+ describe "#camelize" do
29
+ it "converts under_score to CamelCase" do
30
+ expect("my_class".camelize).to eql("MyClass")
31
+ expect("just_a_model".camelize).to eql("JustAModel")
32
+ end
33
+ end
34
+
35
+ describe "#underscore" do
36
+ it "converts CamelCase to under_score" do
37
+ expect("MyClass".underscore).to eql("my_class")
38
+ expect("JustAModel".underscore).to eql("just_a_model")
39
+ end
40
+ end
41
+ end
42
+
@@ -0,0 +1,69 @@
1
+ require "spec_helper"
2
+
3
+ describe Frenchy::RequestError do
4
+ describe "#message" do
5
+ describe "with an exception" do
6
+ it "uses the message from the exception" do
7
+ ex = EOFError.new("reached eof")
8
+ error = Frenchy::RequestError.new(ex.message)
9
+
10
+ message = begin
11
+ raise(error, ex)
12
+ rescue => e
13
+ e.message
14
+ end
15
+
16
+ expect(message).to eql("reached eof")
17
+ end
18
+
19
+ it "can be raised" do
20
+ ex = EOFError.new("reached eof")
21
+ error = Frenchy::RequestError
22
+ expect do
23
+ raise(error, ex)
24
+ end.to raise_error(Frenchy::RequestError, "reached eof")
25
+ end
26
+ end
27
+
28
+ describe "with a response" do
29
+ it "uses the status of the response" do
30
+ response = instance_double("Net::HTTPResponse", code: "500", body: "internal server error")
31
+ error = Frenchy::RequestError.new(nil, nil, response)
32
+
33
+ message = begin
34
+ raise(error)
35
+ rescue => e
36
+ e.message
37
+ end
38
+
39
+ expect(message).to include("500")
40
+ expect(message).to include("internal server error")
41
+ end
42
+
43
+ it "can be raised" do
44
+ response = instance_double("Net::HTTPResponse", code: "500", body: "internal server error")
45
+ error = Frenchy::RequestError.new(nil, response)
46
+ expect do
47
+ raise error, "something"
48
+ end.to raise_error(Frenchy::RequestError)
49
+ end
50
+ end
51
+ end
52
+
53
+ describe "#request" do
54
+ it "contains the original request" do
55
+ request = "GET https://api.github.com"
56
+ error = Frenchy::RequestError.new(nil, request, nil)
57
+ expect(error.request).to eql(request)
58
+ end
59
+ end
60
+
61
+ describe "#response" do
62
+ it "contains the original response" do
63
+ response = instance_double("Net::HTTPResponse", code: "500", body: "internal server error")
64
+ error = Frenchy::RequestError.new(nil, nil, response)
65
+ expect(error.response).to eql(response)
66
+ end
67
+ end
68
+
69
+ end
@@ -0,0 +1,213 @@
1
+ require "spec_helper"
2
+
3
+ class SimpleModel
4
+ include Frenchy::Model
5
+
6
+ field :id, type: "integer"
7
+ field :name, type: "string", default: "unknown"
8
+ field :occupation, type: "string"
9
+ end
10
+
11
+ class SpecialItem
12
+ include Frenchy::Model
13
+
14
+ field :id, type: "integer"
15
+ end
16
+
17
+ class SuperSpecialItem
18
+ include Frenchy::Model
19
+
20
+ field :id, type: "integer"
21
+ end
22
+
23
+ class Box
24
+ include Frenchy::Model
25
+
26
+ key :name
27
+
28
+ field :id, type: "integer"
29
+ field :name, type: "string"
30
+ field :gpa, type: "float", aliases: [:grade, :grade_point_average]
31
+ field :happy, type: "bool"
32
+ field :birth, type: "time"
33
+ field :aliases, type: "array"
34
+ field :extras, type: "hash"
35
+
36
+ field :item, type: "special_item"
37
+ field :items, type: "special_item", many: true
38
+ field :special, type: "special_item", class_name: "SuperSpecialItem"
39
+ end
40
+
41
+ class SimpleModelDecorator
42
+ def self.decorate(object, options={})
43
+ "DECORATED"
44
+ end
45
+ end
46
+
47
+ describe Frenchy::Model do
48
+ describe "#initialize" do
49
+ it "assigns given attributes" do
50
+ model = SimpleModel.new(id: 1, name: "bob")
51
+ expect(model.id).to eq(1)
52
+ expect(model.name).to eq("bob")
53
+ end
54
+
55
+ it "assigns defaults when attributes are missing" do
56
+ model = SimpleModel.new(id: 1)
57
+ expect(model.name).to eq("unknown")
58
+ end
59
+ end
60
+
61
+ describe "#attributes" do
62
+ it "includes all attributes, present or not" do
63
+ model = SimpleModel.new(id: 1)
64
+ expect(model.attributes).to eq({"id" => 1, "name" => "unknown", "occupation" => nil})
65
+ end
66
+ end
67
+
68
+ describe "#decorate" do
69
+ it "decorates the model using the named convention" do
70
+ model = SimpleModel.new
71
+ expect(model.decorate).to eq("DECORATED")
72
+ end
73
+
74
+ it "supports a hash of options" do
75
+ model = SimpleModel.new
76
+ expect(model.decorate({"a" => 1})).to eq("DECORATED") # test arity
77
+ end
78
+ end
79
+
80
+ describe ".key" do
81
+ it "provides to_param method" do
82
+ m = Box.new(id: 5, name: "john")
83
+ expect(m.to_param).to eql("john")
84
+ end
85
+ end
86
+
87
+ describe ".field" do
88
+ describe "aliases" do
89
+ it "aliases fields" do
90
+ m = Box.new(gpa: 5.0)
91
+ expect(m.gpa).to eql(5.0)
92
+ expect(m.grade).to eql(5.0)
93
+ expect(m.grade_point_average).to eql(5.0)
94
+ end
95
+ end
96
+
97
+ describe "string" do
98
+ it "converts values to a String" do
99
+ expect(Box.new(name: 1234).name).to eql("1234")
100
+ end
101
+ end
102
+
103
+ describe "integer" do
104
+ it "converts values to an Integer" do
105
+ expect(Box.new(id: "1234").id).to eql(1234)
106
+ end
107
+
108
+ it "raises an error with invalid values" do
109
+ expect{Box.new(id: "a")}.to raise_error(ArgumentError)
110
+ end
111
+ end
112
+
113
+ describe "float" do
114
+ it "converts values to a Float" do
115
+ expect(Box.new(gpa: "1").gpa).to eql(1.0)
116
+ end
117
+
118
+ it "raises an error with invalid values" do
119
+ expect{Box.new(gpa: "a")}.to raise_error(ArgumentError)
120
+ end
121
+ end
122
+
123
+ describe "bool" do
124
+ it "converts truthy values to true" do
125
+ ["true", "1", 1, true].each do |v|
126
+ expect(Box.new(happy: v).happy).to eql(true)
127
+ end
128
+ end
129
+
130
+ it "converts non-truthy values to false" do
131
+ ["false", "0", 0, false].each do |v|
132
+ expect(Box.new(happy: v).happy).to eql(false)
133
+ end
134
+ end
135
+ end
136
+
137
+ describe "time" do
138
+ it "converts unix timestamps to DateTime" do
139
+ v = Box.new(birth: 1234567890).birth
140
+ expect(v.class).to eql(DateTime)
141
+ expect(v.to_time.to_i).to eql(1234567890)
142
+ expect(v.year).to eql(2009)
143
+ end
144
+
145
+ it "converts strings DateTime" do
146
+ dt = DateTime.new(2011,2,3,4,5,6)
147
+ v = Box.new(birth: dt.to_s).birth
148
+ expect(v.class).to eql(DateTime)
149
+ expect(v.to_time.to_i).to eql(1296705906)
150
+ expect(v.year).to eql(2011)
151
+ end
152
+ end
153
+
154
+ describe "array" do
155
+ it "defaults to []" do
156
+ v = Box.new.aliases
157
+ expect(v.class).to eql(Array)
158
+ expect(v).to eql([])
159
+ end
160
+
161
+ it "stores arrays as is" do
162
+ v = Box.new(aliases: ["chuck", "charles"]).aliases
163
+ expect(v).to eql(["chuck", "charles"])
164
+ end
165
+
166
+ it "wraps singualr values in an array" do
167
+ v = Box.new(aliases: "chuck").aliases
168
+ expect(v).to eql(["chuck"])
169
+ end
170
+ end
171
+
172
+ describe "hash" do
173
+ it "defaults to {}" do
174
+ v = Box.new.extras
175
+ expect(v.class).to eql(Hash)
176
+ expect(v).to eql({})
177
+ end
178
+
179
+ it "stores hashes as is" do
180
+ v = Box.new(extras: {"a" => 1, "b" => 2}).extras
181
+ expect(v).to eql({"a" => 1, "b" => 2})
182
+ end
183
+
184
+ it "converts nested array values to a hash" do
185
+ v = Box.new(extras: [["type", "person"], ["pet", "dog"]]).extras
186
+ expect(v).to eql({"type" => "person", "pet" => "dog"})
187
+ end
188
+ end
189
+
190
+ describe "model relationships" do
191
+ it "establishes single model relationships" do
192
+ v = Box.new(item: {id: 1}).item
193
+ expect(v).to be_an_instance_of(SpecialItem)
194
+ expect(v.id).to eql(1)
195
+ end
196
+
197
+ it "supports explicit class name" do
198
+ v = Box.new(special: {id: 1}).special
199
+ expect(v).to be_an_instance_of(SuperSpecialItem)
200
+ expect(v.id).to eql(1)
201
+ end
202
+
203
+ it "establishes many model relationships" do
204
+ v = Box.new(items: [{id: 1}, {id: 2}]).items
205
+ expect(v).to be_an_instance_of(Frenchy::Collection)
206
+ expect(v[0]).to be_an_instance_of(SpecialItem)
207
+ expect(v[0].id).to eql(1)
208
+ expect(v[1]).to be_an_instance_of(SpecialItem)
209
+ expect(v[1].id).to eql(2)
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,22 @@
1
+ require "spec_helper"
2
+
3
+ describe Frenchy::Request do
4
+ describe "path substitution" do
5
+ it "substitutes path parameters" do
6
+ request = Frenchy::Request.new("service", "get", "/v1/users/:id/:token", {"id" => 1234, "token" => "md5something"}, {})
7
+ expect(request.path).to eql("/v1/users/1234/md5something")
8
+ end
9
+
10
+ it "retains remaining parameters as query parameters" do
11
+ request = Frenchy::Request.new("service", "get", "/v1/users/:id", {"id" => 1234, "token" => "md5something"}, {})
12
+ expect(request.path).to eql("/v1/users/1234")
13
+ expect(request.params).to eql({"token" => "md5something"})
14
+ end
15
+
16
+ it "raises an error for missing path parameters" do
17
+ expect do
18
+ Frenchy::Request.new("service", "get", "/v1/users/:id/:token", {"id" => 1234}, {})
19
+ end.to raise_error(Frenchy::Error)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,105 @@
1
+ require "spec_helper"
2
+
3
+ class Bin
4
+ include Frenchy::Model
5
+ include Frenchy::Resource
6
+
7
+ resource service: "httpbin",
8
+ endpoints: {
9
+ default: { method: "get", path: "/get" },
10
+ one: { method: "get", path: "/get" },
11
+ many: { method: "get", path: "/get" },
12
+ search: { method: "get", path: "/get" },
13
+ }
14
+
15
+ field :args, type: "hash"
16
+ field :headers, type: "hash"
17
+ field :origin, type: "string"
18
+ field :url, type: "string"
19
+ end
20
+
21
+ class BinOneEndpoint
22
+ include Frenchy::Model
23
+ include Frenchy::Resource
24
+
25
+ resource service: "httpbin", endpoint: { path: "/get" }
26
+
27
+ field :args, type: "hash"
28
+ end
29
+
30
+ class BinNoEndpoints
31
+ include Frenchy::Model
32
+ include Frenchy::Resource
33
+
34
+ resource service: "httpbin"
35
+
36
+ field :id, type: "string"
37
+ end
38
+
39
+ class BinArgs
40
+ include Frenchy::Model
41
+ include Frenchy::Resource
42
+
43
+ resource service: "httpbin",
44
+ endpoints: {
45
+ nested: { method: "get", path: "/get", nesting: "args" },
46
+ }
47
+
48
+ field :my_arg, type: "string"
49
+ end
50
+
51
+ describe Frenchy::Resource do
52
+ before :all do
53
+ Frenchy.register_service("httpbin", {"host" => "http://httpbin.org"})
54
+ end
55
+
56
+ describe ".find" do
57
+ it "finds a single object with a single string parameter (id substitution)" do
58
+ resp = Bin.find("a")
59
+ expect(resp.args["id"]).to eql("a")
60
+ end
61
+
62
+ it "finds a single object with a parameters hash" do
63
+ resp = Bin.find(id: "a")
64
+ expect(resp.args["id"]).to eql("a")
65
+ end
66
+ end
67
+
68
+ describe ".find_one" do
69
+ it "finds a single object with id" do
70
+ resp = Bin.find_one("a")
71
+ expect(resp.args["id"]).to eql("a")
72
+ end
73
+ end
74
+
75
+ describe ".find_many" do
76
+ it "finds many objects with ids" do
77
+ resp = Bin.find_many(["a", "b", "c"])
78
+ expect(resp.args["ids"]).to eql("a,b,c")
79
+ end
80
+ end
81
+
82
+ describe ".find_with_endpoint" do
83
+ it "finds a single object with endpoint and params" do
84
+ resp = Bin.find_with_endpoint(:default, a: 1, b: 2)
85
+ expect(resp.args).to eql({"a" => "1", "b" => "2"})
86
+ end
87
+
88
+ it "finds a single object with a single endpoint and params" do
89
+ resp = BinOneEndpoint.find_with_endpoint(:default, a: 1, b: 2)
90
+ expect(resp.args).to eql({"a" => "1", "b" => "2"})
91
+ end
92
+
93
+ it "finds a single object with a nested endpoint and params" do
94
+ resp = BinArgs.find_with_endpoint(:nested, my_arg: "dataz")
95
+ expect(resp).to be_an_instance_of(BinArgs)
96
+ expect(resp.my_arg).to eql("dataz")
97
+ end
98
+
99
+ it "raises an exception if there are no endpoints" do
100
+ expect do
101
+ BinNoEndpoints.find_with_endpoint(:nonexist, myarg: "mydata")
102
+ end.to raise_exception(Frenchy::Error)
103
+ end
104
+ end
105
+ end