frenchy 0.0.9 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +3 -0
- data/.travis.yml +4 -0
- data/Guardfile +6 -0
- data/README.md +13 -11
- data/Rakefile +7 -0
- data/frenchie.gemspec +6 -5
- data/lib/frenchy.rb +8 -13
- data/lib/frenchy/client.rb +74 -46
- data/lib/frenchy/collection.rb +1 -1
- data/lib/frenchy/core_ext.rb +34 -0
- data/lib/frenchy/error.rb +31 -0
- data/lib/frenchy/instrumentation.rb +45 -41
- data/lib/frenchy/model.rb +71 -44
- data/lib/frenchy/request.rb +20 -11
- data/lib/frenchy/resource.rb +36 -33
- data/lib/frenchy/veneer.rb +24 -20
- data/lib/frenchy/version.rb +1 -1
- data/spec/lib/frenchy/client_spec.rb +63 -0
- data/spec/lib/frenchy/collection_spec.rb +38 -0
- data/spec/lib/frenchy/core_ext_spec.rb +42 -0
- data/spec/lib/frenchy/error_spec.rb +69 -0
- data/spec/lib/frenchy/model_spec.rb +213 -0
- data/spec/lib/frenchy/request_spec.rb +22 -0
- data/spec/lib/frenchy/resource_spec.rb +105 -0
- data/spec/lib/frenchy/veneer_spec.rb +19 -0
- data/spec/lib/frenchy_spec.rb +18 -0
- data/spec/spec_helper.rb +21 -0
- metadata +62 -23
@@ -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
|