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.
- 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
|