typhoid 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +19 -0
- data/.rvmrc +5 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +34 -0
- data/LICENSE +22 -0
- data/README.md +3 -0
- data/Rakefile +13 -0
- data/lib/typhoid/attributes.rb +76 -0
- data/lib/typhoid/builder.rb +58 -0
- data/lib/typhoid/multi.rb +21 -0
- data/lib/typhoid/parser.rb +52 -0
- data/lib/typhoid/queued_request.rb +32 -0
- data/lib/typhoid/request_builder.rb +29 -0
- data/lib/typhoid/request_queue.rb +33 -0
- data/lib/typhoid/resource.rb +116 -0
- data/lib/typhoid/uri.rb +37 -0
- data/lib/typhoid/version.rb +3 -0
- data/lib/typhoid.rb +10 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/support/controller.rb +5 -0
- data/spec/support/game.rb +15 -0
- data/spec/support/player_stat.rb +10 -0
- data/spec/typhoid/builder_spec.rb +83 -0
- data/spec/typhoid/multi_spec.rb +33 -0
- data/spec/typhoid/parser_spec.rb +59 -0
- data/spec/typhoid/request_builder_spec.rb +15 -0
- data/spec/typhoid/resource_spec.rb +143 -0
- data/spec/typhoid/typhoid_spec.rb +4 -0
- data/spec/typhoid/uri_spec.rb +27 -0
- data/typhoid.gemspec +22 -0
- metadata +140 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
typhoid (0.0.1)
|
5
|
+
typhoeus
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
diff-lcs (1.1.3)
|
11
|
+
ffi (1.0.11)
|
12
|
+
json_pure (1.7.3)
|
13
|
+
mime-types (1.18)
|
14
|
+
rake (0.9.2.2)
|
15
|
+
rspec (2.10.0)
|
16
|
+
rspec-core (~> 2.10.0)
|
17
|
+
rspec-expectations (~> 2.10.0)
|
18
|
+
rspec-mocks (~> 2.10.0)
|
19
|
+
rspec-core (2.10.1)
|
20
|
+
rspec-expectations (2.10.0)
|
21
|
+
diff-lcs (~> 1.1.3)
|
22
|
+
rspec-mocks (2.10.1)
|
23
|
+
typhoeus (0.4.2)
|
24
|
+
ffi (~> 1.0)
|
25
|
+
mime-types (~> 1.18)
|
26
|
+
|
27
|
+
PLATFORMS
|
28
|
+
ruby
|
29
|
+
|
30
|
+
DEPENDENCIES
|
31
|
+
json_pure (>= 1.4.1)
|
32
|
+
rake
|
33
|
+
rspec
|
34
|
+
typhoid!
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Doug Rohde
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
require "rspec/core/rake_task"
|
4
|
+
|
5
|
+
RSpec::Core::RakeTask.new(:spec)
|
6
|
+
|
7
|
+
namespace :spec do
|
8
|
+
RSpec::Core::RakeTask.new(:docs) do |t|
|
9
|
+
t.rspec_opts = ["--format doc"]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
task :default => :spec
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Typhoid
|
2
|
+
module Attributes
|
3
|
+
def load_values(params = {})
|
4
|
+
@attributes = Hash[params.map { |key, value| [key.to_s, value] }]
|
5
|
+
end
|
6
|
+
|
7
|
+
def attributes
|
8
|
+
@attributes ||= {}
|
9
|
+
@attributes
|
10
|
+
end
|
11
|
+
|
12
|
+
def read_attribute(name)
|
13
|
+
attributes[name.to_s]
|
14
|
+
end
|
15
|
+
alias :[] :read_attribute
|
16
|
+
|
17
|
+
def after_build(response, exception = nil)
|
18
|
+
assign_request_error(exception) if !response.success? || !exception.nil?
|
19
|
+
end
|
20
|
+
|
21
|
+
def assign_request_error(exception = nil)
|
22
|
+
self.resource_exception = exception || StandardError.new("Could not retrieve data from remote service")
|
23
|
+
end
|
24
|
+
private :assign_request_error
|
25
|
+
|
26
|
+
def self.included(base)
|
27
|
+
base.extend(ClassMethods)
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
module ClassMethods
|
33
|
+
def field(*field_names)
|
34
|
+
raise ArgumentError, "Must specify at least one field" if field_names.length == 0
|
35
|
+
@auto_init_fields ||= []
|
36
|
+
field_names.each do |field_name|
|
37
|
+
define_accessor field_name
|
38
|
+
@auto_init_fields << field_name.to_sym
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def define_accessor(field_name)
|
43
|
+
define_method field_name do
|
44
|
+
attributes[field_name.to_s]
|
45
|
+
end
|
46
|
+
define_method "#{field_name}=" do |new_value|
|
47
|
+
attributes[field_name.to_s] = new_value
|
48
|
+
end
|
49
|
+
end
|
50
|
+
private :define_accessor
|
51
|
+
|
52
|
+
def auto_init_fields
|
53
|
+
@auto_init_fields || []
|
54
|
+
end
|
55
|
+
|
56
|
+
def builder
|
57
|
+
Builder
|
58
|
+
end
|
59
|
+
|
60
|
+
def parser
|
61
|
+
Parser
|
62
|
+
end
|
63
|
+
|
64
|
+
def build(klass, response)
|
65
|
+
builder.call(klass, response)
|
66
|
+
end
|
67
|
+
|
68
|
+
def load_values(object, response)
|
69
|
+
object.tap { |obj|
|
70
|
+
obj.load_values(parser.call response.body)
|
71
|
+
obj.after_build response
|
72
|
+
}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Typhoid
|
2
|
+
class Builder
|
3
|
+
attr_reader :klass
|
4
|
+
attr_reader :response
|
5
|
+
attr_reader :body
|
6
|
+
attr_reader :parsed_body
|
7
|
+
attr_reader :exception
|
8
|
+
|
9
|
+
def self.call(klass, response)
|
10
|
+
new(klass, response).build
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(klass, response)
|
14
|
+
@klass = klass
|
15
|
+
@response = response
|
16
|
+
@body = response.body
|
17
|
+
begin
|
18
|
+
@parsed_body = parser.call body
|
19
|
+
rescue StandardError => e
|
20
|
+
@parsed_body = {}
|
21
|
+
@exception = e
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def build
|
26
|
+
array? ? build_array : build_single
|
27
|
+
end
|
28
|
+
|
29
|
+
def build_from_klass(attributes)
|
30
|
+
klass.new(attributes).tap { |item|
|
31
|
+
item.after_build(response, exception) if item.respond_to? :after_build
|
32
|
+
}
|
33
|
+
end
|
34
|
+
private :build_from_klass
|
35
|
+
|
36
|
+
def array?
|
37
|
+
parsed_body.is_a?(Array)
|
38
|
+
end
|
39
|
+
private :array?
|
40
|
+
|
41
|
+
def build_array
|
42
|
+
parsed_body.collect { |single|
|
43
|
+
build_from_klass(single)
|
44
|
+
}
|
45
|
+
end
|
46
|
+
private :build_array
|
47
|
+
|
48
|
+
def build_single
|
49
|
+
build_from_klass parsed_body
|
50
|
+
end
|
51
|
+
private :build_single
|
52
|
+
|
53
|
+
def parser
|
54
|
+
klass.parser
|
55
|
+
end
|
56
|
+
private :parser
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Typhoid
|
2
|
+
module Multi
|
3
|
+
def remote_resources(hydra = nil)
|
4
|
+
request_queue = RequestQueue.new(self, hydra)
|
5
|
+
yield request_queue if block_given?
|
6
|
+
|
7
|
+
request_queue.run
|
8
|
+
|
9
|
+
request_queue.requests.each do |req|
|
10
|
+
parse_queued_response req
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
def parse_queued_response(req)
|
17
|
+
varname = "@" + req.name.to_s
|
18
|
+
req.target.instance_variable_set varname.to_sym, Typhoid::Resource.build(req.klass, req.response)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Typhoid
|
2
|
+
class Parser
|
3
|
+
attr_reader :json_string
|
4
|
+
|
5
|
+
def self.call(json_string)
|
6
|
+
new(json_string).parse
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(json_string)
|
10
|
+
@json_string = json_string
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse
|
14
|
+
parsed_body
|
15
|
+
end
|
16
|
+
|
17
|
+
def parsed_body
|
18
|
+
engine.call json_string
|
19
|
+
rescue
|
20
|
+
raise ReadError, json_string
|
21
|
+
end
|
22
|
+
private :parsed_body
|
23
|
+
|
24
|
+
def engine
|
25
|
+
JSON.method(:parse)
|
26
|
+
end
|
27
|
+
private :engine
|
28
|
+
end
|
29
|
+
|
30
|
+
class ReadError < StandardError
|
31
|
+
attr_reader :body
|
32
|
+
def initialize(body)
|
33
|
+
@body = body
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_s
|
37
|
+
"Could not parse JSON body: #{cleaned_body}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def cleaned_body
|
41
|
+
clean = body[0..10]
|
42
|
+
clean = clean + "..." if add_dots?
|
43
|
+
clean
|
44
|
+
end
|
45
|
+
private :cleaned_body
|
46
|
+
|
47
|
+
def add_dots?
|
48
|
+
body.length > 10
|
49
|
+
end
|
50
|
+
private :add_dots?
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Typhoid
|
2
|
+
class QueuedRequest
|
3
|
+
attr_accessor :name, :request, :target, :klass
|
4
|
+
attr_accessor :on_complete
|
5
|
+
|
6
|
+
def initialize(hydra, name, req, target)
|
7
|
+
self.name = name
|
8
|
+
self.request = Typhoeus::Request.new(req.request_uri, req.options)
|
9
|
+
self.klass = req.klass
|
10
|
+
self.target = target
|
11
|
+
hydra.queue(self.request)
|
12
|
+
end
|
13
|
+
|
14
|
+
def on_complete
|
15
|
+
self.request.on_complete do
|
16
|
+
yield self if block_given?
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def status
|
21
|
+
self.request.handled_response.code
|
22
|
+
end
|
23
|
+
|
24
|
+
def response
|
25
|
+
self.request.handled_response
|
26
|
+
end
|
27
|
+
|
28
|
+
def klass
|
29
|
+
@klass
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Typhoid
|
2
|
+
class RequestBuilder
|
3
|
+
attr_accessor :klass
|
4
|
+
attr_writer :method
|
5
|
+
|
6
|
+
def initialize(klass, uri, options = {})
|
7
|
+
@uri = uri
|
8
|
+
@request_options = options
|
9
|
+
@klass = klass
|
10
|
+
end
|
11
|
+
|
12
|
+
def request_uri
|
13
|
+
@uri
|
14
|
+
end
|
15
|
+
|
16
|
+
def options
|
17
|
+
@request_options
|
18
|
+
end
|
19
|
+
|
20
|
+
def http_method
|
21
|
+
options[:method] || :get
|
22
|
+
end
|
23
|
+
|
24
|
+
def run
|
25
|
+
klass.run(self)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'typhoeus'
|
2
|
+
|
3
|
+
module Typhoid
|
4
|
+
class RequestQueue
|
5
|
+
attr_reader :queue
|
6
|
+
attr_accessor :target
|
7
|
+
|
8
|
+
def initialize(target, hydra = nil)
|
9
|
+
@target = target
|
10
|
+
@hydra = hydra || Typhoeus::Hydra.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def resource(name, req, &block)
|
14
|
+
@queue ||= []
|
15
|
+
@queue << QueuedRequest.new(@hydra, name, req, @target)
|
16
|
+
#@queue[name].on_complete &block if block != nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def resource_with_target(name, req, target, &block)
|
20
|
+
@queue ||= []
|
21
|
+
@queue << QueuedRequest.new(@hydra, name, req, target)
|
22
|
+
#@queue[name].on_complete &block if block != nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def requests
|
26
|
+
@queue ||= []
|
27
|
+
end
|
28
|
+
|
29
|
+
def run
|
30
|
+
@hydra.run
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'typhoid/uri'
|
3
|
+
|
4
|
+
module Typhoid
|
5
|
+
class Resource
|
6
|
+
include Typhoid::Multi
|
7
|
+
include Typhoid::Attributes
|
8
|
+
|
9
|
+
class << self
|
10
|
+
attr_accessor :site, :path
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_accessor :resource_exception
|
14
|
+
|
15
|
+
def self.build_request(uri, options = {})
|
16
|
+
Typhoid::RequestBuilder.new(self, uri, options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.run(request)
|
20
|
+
method = request.http_method
|
21
|
+
build(request.klass, (Typhoeus::Request.send method, request.request_uri, request.options))
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.uri_join(*paths)
|
25
|
+
Uri.new(*paths).to_s
|
26
|
+
end
|
27
|
+
|
28
|
+
# Get this request URI based on site and path, can attach
|
29
|
+
# more paths
|
30
|
+
def self.request_uri(*more_paths)
|
31
|
+
uri_join site, path, *more_paths
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize(params = {})
|
35
|
+
load_values(params)
|
36
|
+
end
|
37
|
+
|
38
|
+
def success?
|
39
|
+
!resource_exception
|
40
|
+
end
|
41
|
+
|
42
|
+
def save!(method = nil)
|
43
|
+
save method
|
44
|
+
raise resource_exception unless success?
|
45
|
+
end
|
46
|
+
|
47
|
+
def destroy!
|
48
|
+
destroy
|
49
|
+
raise resource_exception unless success?
|
50
|
+
end
|
51
|
+
|
52
|
+
def save(method = nil)
|
53
|
+
request_and_load do
|
54
|
+
Typhoeus::Request.send save_http_method(method), save_request.request_uri, save_request.options
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def destroy
|
59
|
+
request_and_load do
|
60
|
+
Typhoeus::Request.delete(delete_request.request_uri, delete_request.options)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def save_request
|
65
|
+
(new_record?) ? create_request : update_request
|
66
|
+
end
|
67
|
+
|
68
|
+
def save_http_method(method = nil)
|
69
|
+
return method if method
|
70
|
+
(new_record?) ? :post : :put
|
71
|
+
end
|
72
|
+
|
73
|
+
# Request URI is either in the object we retrieveed initially, built from
|
74
|
+
# site + path + id, or fail to the regular class#request_uri
|
75
|
+
#
|
76
|
+
# Also, check that the server we're speaking to isn't hypermedia inclined so
|
77
|
+
# look at our attributes for a URI
|
78
|
+
def request_uri
|
79
|
+
attributes["uri"] || (new_record? ? self.class.request_uri : self.class.request_uri(id))
|
80
|
+
end
|
81
|
+
|
82
|
+
def request_and_load(&block)
|
83
|
+
self.resource_exception = nil
|
84
|
+
response = yield
|
85
|
+
self.class.load_values(self, response)
|
86
|
+
success?
|
87
|
+
end
|
88
|
+
|
89
|
+
def persisted?
|
90
|
+
!new_record?
|
91
|
+
end
|
92
|
+
|
93
|
+
def new_record?
|
94
|
+
id.to_s.length < 1
|
95
|
+
end
|
96
|
+
alias new? new_record?
|
97
|
+
|
98
|
+
protected
|
99
|
+
|
100
|
+
def to_params
|
101
|
+
attributes
|
102
|
+
end
|
103
|
+
|
104
|
+
def create_request(method = :post)
|
105
|
+
Typhoid::RequestBuilder.new(self.class, request_uri, :body => to_params.to_json, :method => method, :headers => {"Content-Type" => 'application/json'})
|
106
|
+
end
|
107
|
+
|
108
|
+
def update_request(method = :put)
|
109
|
+
Typhoid::RequestBuilder.new(self.class, request_uri, :body => to_params.to_json, :method => method, :headers => {"Content-Type" => 'application/json'})
|
110
|
+
end
|
111
|
+
|
112
|
+
def delete_request(method = :delete)
|
113
|
+
Typhoid::RequestBuilder.new(self.class, request_uri, :method => method)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/lib/typhoid/uri.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'uri'
|
2
|
+
module Typhoid
|
3
|
+
class Uri
|
4
|
+
private
|
5
|
+
attr_writer :base
|
6
|
+
attr_writer :paths
|
7
|
+
|
8
|
+
public
|
9
|
+
attr_reader :base
|
10
|
+
attr_reader :paths
|
11
|
+
|
12
|
+
def initialize(*paths)
|
13
|
+
self.base = URI.parse paths.shift.to_s
|
14
|
+
self.paths = sanitize(base.path) + sanitize(paths)
|
15
|
+
base.path = ""
|
16
|
+
raise "Invalid Base on #uri_join: #{base}" unless base.scheme || base.host
|
17
|
+
end
|
18
|
+
|
19
|
+
def join(*more_paths)
|
20
|
+
full_path = (paths + sanitize(more_paths)).join "/"
|
21
|
+
base.clone.merge(full_path).to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
join
|
26
|
+
end
|
27
|
+
|
28
|
+
def sanitize(*need_sanitizing)
|
29
|
+
need_sanitizing.
|
30
|
+
flatten.
|
31
|
+
compact.
|
32
|
+
map { |p| p.to_s.split("/").compact.delete_if(&:empty?) }.
|
33
|
+
flatten
|
34
|
+
end
|
35
|
+
private :sanitize
|
36
|
+
end
|
37
|
+
end
|
data/lib/typhoid.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require "typhoid/version"
|
2
|
+
require 'typhoid/uri'
|
3
|
+
require 'typhoid/parser'
|
4
|
+
require 'typhoid/builder'
|
5
|
+
require "typhoid/request_queue"
|
6
|
+
require "typhoid/queued_request"
|
7
|
+
require "typhoid/multi"
|
8
|
+
require 'typhoid/attributes'
|
9
|
+
require 'typhoid/resource'
|
10
|
+
require 'typhoid/request_builder'
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# Require this file using `require "spec_helper.rb"` to ensure that it is only
|
4
|
+
# loaded once.
|
5
|
+
#
|
6
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
7
|
+
|
8
|
+
require 'json'
|
9
|
+
|
10
|
+
Dir["spec/support/**/*.rb"].each {|f| require "./#{f}"}
|
11
|
+
|
12
|
+
RSpec.configure do |config|
|
13
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
14
|
+
config.run_all_when_everything_filtered = true
|
15
|
+
config.filter_run :focus
|
16
|
+
config.color = true
|
17
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'typhoid'
|
2
|
+
|
3
|
+
class Game < Typhoid::Resource
|
4
|
+
field :id
|
5
|
+
field :team_1_name
|
6
|
+
field :team_2_name
|
7
|
+
field :start_time
|
8
|
+
|
9
|
+
self.site = 'http://localhost:3000/'
|
10
|
+
self.path = 'games/'
|
11
|
+
|
12
|
+
def self.get_game
|
13
|
+
build_request("http://localhost:3000/games/1")
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
module Typhoid
|
3
|
+
describe Builder do
|
4
|
+
let(:example_json) { <<-JSON
|
5
|
+
{
|
6
|
+
"metadata": {
|
7
|
+
"current_user": {
|
8
|
+
"first_name": "Jon",
|
9
|
+
"id": 1,
|
10
|
+
"last_name": "Gilmore",
|
11
|
+
"uri": "http://user-service.dev/users/1",
|
12
|
+
"user_name": "admin"
|
13
|
+
}
|
14
|
+
},
|
15
|
+
"result": {
|
16
|
+
"first_name": "Jon",
|
17
|
+
"id": 15,
|
18
|
+
"last_name": "Phenow",
|
19
|
+
"type": "orphan",
|
20
|
+
"uri": "http://user-service.dev/personas/15",
|
21
|
+
"user": null
|
22
|
+
}
|
23
|
+
}
|
24
|
+
JSON
|
25
|
+
}
|
26
|
+
|
27
|
+
let(:example_array) { <<-JSON
|
28
|
+
[{"metadata": null }, {"metadata": null }]
|
29
|
+
JSON
|
30
|
+
}
|
31
|
+
let(:klass) { Resource }
|
32
|
+
let(:response) { double body: mocked_body, success?: mocked_success }
|
33
|
+
let(:mocked_body) { example_json }
|
34
|
+
let(:mocked_success) { true }
|
35
|
+
describe "class" do
|
36
|
+
subject { Builder }
|
37
|
+
describe "call" do
|
38
|
+
subject { Builder.call klass, response }
|
39
|
+
it { should be_a Resource }
|
40
|
+
its(:attributes) { should have_key "metadata" }
|
41
|
+
its(:attributes) { should have_key "result" }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "instance" do
|
46
|
+
subject { Builder.new klass, response }
|
47
|
+
describe "successful" do
|
48
|
+
describe "singular" do
|
49
|
+
it "calls expected building methods" do
|
50
|
+
klass.any_instance.should_receive(:after_build).once
|
51
|
+
subject.build.should be_a Resource
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "array" do
|
56
|
+
let(:mocked_body) { example_array }
|
57
|
+
it "calls expected building methods" do
|
58
|
+
subject.build.should be_an Array
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "unsuccessful" do
|
64
|
+
let(:mocked_success) { false }
|
65
|
+
subject { Builder.new(klass, response).build }
|
66
|
+
describe "singular" do
|
67
|
+
it { should be_a Resource }
|
68
|
+
its(:resource_exception) { should_not be_nil }
|
69
|
+
its(:attributes) { should have_key "metadata" }
|
70
|
+
end
|
71
|
+
|
72
|
+
describe "array" do
|
73
|
+
let(:mocked_body) { example_array }
|
74
|
+
subject { Builder.new(klass, response).build.first }
|
75
|
+
|
76
|
+
it { should be_a Resource }
|
77
|
+
its(:resource_exception) { should_not be_nil }
|
78
|
+
its(:attributes) { should have_key "metadata" }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
|
5
|
+
describe Typhoid::Multi do
|
6
|
+
context "making multiple requests" do
|
7
|
+
before(:each) do
|
8
|
+
@fake_hydra = Typhoeus::Hydra.new
|
9
|
+
game = Typhoeus::Response.new(:code => 200, :headers => "", :body => {"team_1_name" => "Bears"}.to_json, :time => 0.03)
|
10
|
+
@fake_hydra.stub(:get, "http://localhost:3000/games/1").and_return(game)
|
11
|
+
|
12
|
+
stats = Typhoeus::Response.new(:code => 200, :headers => "",
|
13
|
+
:body => [{'player_name' => 'Bob', 'goals' => 1}, {'player_name' => 'Mike', 'goals' => 1}].to_json, :time => 0.02)
|
14
|
+
@fake_hydra.stub(:get, "http://localhost:3000/stats/2").and_return(stats)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should assign the response to instance variables" do
|
18
|
+
controller = Controller.new
|
19
|
+
controller.remote_resources(@fake_hydra) do |req|
|
20
|
+
req.resource(:game, Game.get_game)
|
21
|
+
req.resource(:stats, PlayerStat.get_stats)
|
22
|
+
end
|
23
|
+
#games returns a single object
|
24
|
+
controller.instance_variable_get("@game").class.should eql Game
|
25
|
+
controller.instance_variable_get("@game").team_1_name.should eql "Bears"
|
26
|
+
|
27
|
+
#stats returns an array
|
28
|
+
controller.instance_variable_get("@stats").class.should eql Array
|
29
|
+
controller.instance_variable_get("@stats")[0].class.should eql PlayerStat
|
30
|
+
controller.instance_variable_get("@stats")[0].player_name.should eql 'Bob'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Typhoid
|
4
|
+
describe Parser do
|
5
|
+
let(:example_json) { <<-JSON
|
6
|
+
{
|
7
|
+
"metadata": {
|
8
|
+
"current_user": {
|
9
|
+
"first_name": "Jon",
|
10
|
+
"id": 1,
|
11
|
+
"last_name": "Gilmore",
|
12
|
+
"uri": "http://user-service.dev/users/1",
|
13
|
+
"user_name": "admin"
|
14
|
+
}
|
15
|
+
},
|
16
|
+
"result": {
|
17
|
+
"first_name": "Jon",
|
18
|
+
"id": 15,
|
19
|
+
"last_name": "Phenow",
|
20
|
+
"type": "orphan",
|
21
|
+
"uri": "http://user-service.dev/personas/15",
|
22
|
+
"user": null
|
23
|
+
}
|
24
|
+
}
|
25
|
+
JSON
|
26
|
+
}
|
27
|
+
|
28
|
+
let(:example_array) { <<-JSON
|
29
|
+
[{"metadata": null }, {"metadata": null }]
|
30
|
+
JSON
|
31
|
+
}
|
32
|
+
|
33
|
+
describe "class" do
|
34
|
+
subject { Parser }
|
35
|
+
|
36
|
+
describe "call" do
|
37
|
+
subject { Parser.call(example_json) }
|
38
|
+
|
39
|
+
it { should be_a Hash }
|
40
|
+
it "has an expected element" do
|
41
|
+
subject["result"]["first_name"].should == "Jon"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "instance" do
|
47
|
+
subject { Parser.new example_json }
|
48
|
+
describe "parse" do
|
49
|
+
it "looks like a hash" do
|
50
|
+
subject.parse.should be_a Hash
|
51
|
+
end
|
52
|
+
|
53
|
+
it "has an expected element" do
|
54
|
+
subject.parse["result"]["type"].should == "orphan"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Typhoid::RequestBuilder do
|
4
|
+
context "a request builder object" do
|
5
|
+
it "should provide an http method by default" do
|
6
|
+
req = Typhoid::RequestBuilder.new(Game, 'http://localhost/')
|
7
|
+
req.http_method.should eql :get
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should set http method from options" do
|
11
|
+
req = Typhoid::RequestBuilder.new(Game, 'http://localhost', :method => :post)
|
12
|
+
req.http_method.should eql :post
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Typhoid::Resource do
|
4
|
+
it "synchronizes field with attribute" do
|
5
|
+
response_data = {"team_1_name" => 'Bears', "team_2_name" => 'Lions'}
|
6
|
+
game = Game.new(response_data)
|
7
|
+
game.team_1_name.should == 'Bears'
|
8
|
+
game.attributes["team_1_name"].should == 'Bears'
|
9
|
+
game.attributes["team_1_name"] = 'Da Bears'
|
10
|
+
game.attributes["team_1_name"].should == 'Da Bears'
|
11
|
+
game.team_1_name.should == 'Da Bears'
|
12
|
+
|
13
|
+
game.team_1_name = 'Orange'
|
14
|
+
game.team_1_name.should == 'Orange'
|
15
|
+
game.attributes["team_1_name"].should == 'Orange'
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should have fields defined" do
|
19
|
+
game = Game.new
|
20
|
+
game.should respond_to(:team_1_name)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should populate defined attributes" do
|
24
|
+
response_data = {"team_1_name" => 'Bears', "team_2_name" => 'Lions'}
|
25
|
+
game = Game.new(response_data)
|
26
|
+
game.team_1_name.should eql 'Bears'
|
27
|
+
game.start_time.should be_nil
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should populate attributes" do
|
31
|
+
game = Game.new({"team_1_name" => 'Bears', "team_2_name" => 'Lions'})
|
32
|
+
game.read_attribute(:team_1_name).should eql 'Bears'
|
33
|
+
game[:team_2_name].should eql 'Lions'
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should return the request path" do
|
37
|
+
game = Game.new
|
38
|
+
game.request_uri.should eql "http://localhost:3000/games"
|
39
|
+
end
|
40
|
+
|
41
|
+
context "making a standalone request" do
|
42
|
+
after { hydra.clear_stubs }
|
43
|
+
let(:hydra) { Typhoeus::Hydra.hydra }
|
44
|
+
let(:game_response) { Typhoeus::Response.new(:code => 200, :headers => "", :body => {"team_1_name" => "Bears", "id" => "1"}.to_json) }
|
45
|
+
let(:failed_game_response) { Typhoeus::Response.new(:code => 404, :headers => "", :body => {}.to_json) }
|
46
|
+
it "should retrieve an object" do
|
47
|
+
hydra.stub(:get, "http://localhost:3000/games/1").and_return(game_response)
|
48
|
+
|
49
|
+
game = Game.get_game.run
|
50
|
+
game.class.should eql Game
|
51
|
+
game.team_1_name.should eql 'Bears'
|
52
|
+
end
|
53
|
+
|
54
|
+
it "raises error on save!" do
|
55
|
+
hydra.stub(:post, "http://localhost:3000/games").and_return(failed_game_response)
|
56
|
+
|
57
|
+
game = Game.new
|
58
|
+
expect { game.save! }.to raise_error
|
59
|
+
end
|
60
|
+
|
61
|
+
it "raises error on destroy!" do
|
62
|
+
hydra.stub(:delete, "http://localhost:3000/games/1").and_return(failed_game_response)
|
63
|
+
|
64
|
+
game = Game.new("id" => 1, "team_1_name" => 'Tigers')
|
65
|
+
expect { game.destroy! }.to raise_error
|
66
|
+
end
|
67
|
+
|
68
|
+
it "raises error on save!" do
|
69
|
+
hydra.stub(:post, "http://localhost:3000/games").and_return(game_response)
|
70
|
+
|
71
|
+
game = Game.new
|
72
|
+
expect { game.save! }.to_not raise_error
|
73
|
+
end
|
74
|
+
|
75
|
+
it "raises error on save!" do
|
76
|
+
hydra.stub(:delete, "http://localhost:3000/games/1").and_return(game_response)
|
77
|
+
|
78
|
+
game = Game.new("id" => 1, "team_1_name" => 'Tigers')
|
79
|
+
expect { game.destroy! }.to_not raise_error
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should create an object" do
|
83
|
+
hydra.stub(:post, "http://localhost:3000/games").and_return(game_response)
|
84
|
+
|
85
|
+
game = Game.new
|
86
|
+
game.save
|
87
|
+
|
88
|
+
game.id.should == "1"
|
89
|
+
game.team_1_name.should == "Bears"
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should update an object" do
|
93
|
+
update_response = Typhoeus::Response.new(:code => 200, :headers => "", :body => {"team_1_name" => "Bears", "id" => "1"}.to_json)
|
94
|
+
hydra.stub(:put, "http://localhost:3000/games/1").and_return(update_response)
|
95
|
+
|
96
|
+
game = Game.new("id" => 1, "team_1_name" => 'Tigers')
|
97
|
+
game.save
|
98
|
+
|
99
|
+
game.resource_exception.should be_nil
|
100
|
+
game.team_1_name.should == "Bears"
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should delete an object" do
|
104
|
+
hydra.stub(:delete, "http://localhost:3000/games/1").and_return(game_response)
|
105
|
+
|
106
|
+
game = Game.new("id" => 1, "team_1_name" => 'Tigers')
|
107
|
+
game.destroy
|
108
|
+
|
109
|
+
game.resource_exception.should be nil
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should be able to specify save http verb" do
|
114
|
+
update_response = Typhoeus::Response.new(:code => 200, :headers => "", :body => {"team_1_name" => "Bears", "id" => "1"}.to_json)
|
115
|
+
hydra.stub(:post, "http://localhost:3000/games/1").and_return(update_response)
|
116
|
+
|
117
|
+
game = Game.new("id" => 1, "team_1_name" => 'Tigers')
|
118
|
+
game.save(:post)
|
119
|
+
|
120
|
+
game.resource_exception.should be nil
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
context "handling bad requests" do
|
126
|
+
let(:fake_hydra) { Typhoeus::Hydra.new }
|
127
|
+
before do
|
128
|
+
bad_game = Typhoeus::Response.new(:code => 500, :headers => "", :body => "<htmlasdfasdfasdf")
|
129
|
+
fake_hydra.stub(:get, "http://localhost:3000/games/1").and_return(bad_game)
|
130
|
+
end
|
131
|
+
|
132
|
+
it "should assign an exception object on a bad request" do
|
133
|
+
controller = Controller.new
|
134
|
+
controller.remote_resources(fake_hydra) do |req|
|
135
|
+
req.resource(:game, Game.get_game)
|
136
|
+
end
|
137
|
+
|
138
|
+
bad_game = controller.instance_variable_get("@game")
|
139
|
+
bad_game.team_1_name.should be_nil
|
140
|
+
bad_game.resource_exception.class.should be_true
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
module Typhoid
|
3
|
+
describe "Uri" do
|
4
|
+
subject { Uri.new *uris }
|
5
|
+
let(:uris) { ["http://localhost/", "users"] }
|
6
|
+
|
7
|
+
its(:to_s) { should == "http://localhost/users" }
|
8
|
+
it "sets base" do
|
9
|
+
subject.base.to_s.should == "http://localhost"
|
10
|
+
end
|
11
|
+
|
12
|
+
it "sets path" do
|
13
|
+
subject.paths.should == ["users"]
|
14
|
+
end
|
15
|
+
|
16
|
+
it "appends paths" do
|
17
|
+
subject.join("/","/a/","b","/c","d/").should == "http://localhost/users/a/b/c/d"
|
18
|
+
end
|
19
|
+
|
20
|
+
it "when joining it doesn't change itself" do
|
21
|
+
expect {
|
22
|
+
subject.join("/","/a/","b","/c","d/").should == "http://localhost/users/a/b/c/d"
|
23
|
+
}.
|
24
|
+
to_not change { subject.to_s }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/typhoid.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/typhoid/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Doug Rohde"]
|
6
|
+
gem.email = ["doug.rohde@tstmedia.com"]
|
7
|
+
gem.description = %q{A lightweight ORM-like wrapper around Typhoeus}
|
8
|
+
gem.summary = %q{A lightweight ORM-like wrapper around Typhoeus}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "typhoid"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Typhoid::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency 'typhoeus'
|
19
|
+
|
20
|
+
gem.add_development_dependency 'rspec'
|
21
|
+
gem.add_development_dependency 'json_pure', [">= 1.4.1"]
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: typhoid
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Doug Rohde
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-03-28 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: typhoeus
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: json_pure
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.4.1
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.4.1
|
62
|
+
description: A lightweight ORM-like wrapper around Typhoeus
|
63
|
+
email:
|
64
|
+
- doug.rohde@tstmedia.com
|
65
|
+
executables: []
|
66
|
+
extensions: []
|
67
|
+
extra_rdoc_files: []
|
68
|
+
files:
|
69
|
+
- .gitignore
|
70
|
+
- .rvmrc
|
71
|
+
- Gemfile
|
72
|
+
- Gemfile.lock
|
73
|
+
- LICENSE
|
74
|
+
- README.md
|
75
|
+
- Rakefile
|
76
|
+
- lib/typhoid.rb
|
77
|
+
- lib/typhoid/attributes.rb
|
78
|
+
- lib/typhoid/builder.rb
|
79
|
+
- lib/typhoid/multi.rb
|
80
|
+
- lib/typhoid/parser.rb
|
81
|
+
- lib/typhoid/queued_request.rb
|
82
|
+
- lib/typhoid/request_builder.rb
|
83
|
+
- lib/typhoid/request_queue.rb
|
84
|
+
- lib/typhoid/resource.rb
|
85
|
+
- lib/typhoid/uri.rb
|
86
|
+
- lib/typhoid/version.rb
|
87
|
+
- spec/spec_helper.rb
|
88
|
+
- spec/support/controller.rb
|
89
|
+
- spec/support/game.rb
|
90
|
+
- spec/support/player_stat.rb
|
91
|
+
- spec/typhoid/builder_spec.rb
|
92
|
+
- spec/typhoid/multi_spec.rb
|
93
|
+
- spec/typhoid/parser_spec.rb
|
94
|
+
- spec/typhoid/request_builder_spec.rb
|
95
|
+
- spec/typhoid/resource_spec.rb
|
96
|
+
- spec/typhoid/typhoid_spec.rb
|
97
|
+
- spec/typhoid/uri_spec.rb
|
98
|
+
- typhoid.gemspec
|
99
|
+
homepage: ''
|
100
|
+
licenses: []
|
101
|
+
post_install_message:
|
102
|
+
rdoc_options: []
|
103
|
+
require_paths:
|
104
|
+
- lib
|
105
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
segments:
|
112
|
+
- 0
|
113
|
+
hash: -3289300841199421232
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
115
|
+
none: false
|
116
|
+
requirements:
|
117
|
+
- - ! '>='
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0'
|
120
|
+
segments:
|
121
|
+
- 0
|
122
|
+
hash: -3289300841199421232
|
123
|
+
requirements: []
|
124
|
+
rubyforge_project:
|
125
|
+
rubygems_version: 1.8.24
|
126
|
+
signing_key:
|
127
|
+
specification_version: 3
|
128
|
+
summary: A lightweight ORM-like wrapper around Typhoeus
|
129
|
+
test_files:
|
130
|
+
- spec/spec_helper.rb
|
131
|
+
- spec/support/controller.rb
|
132
|
+
- spec/support/game.rb
|
133
|
+
- spec/support/player_stat.rb
|
134
|
+
- spec/typhoid/builder_spec.rb
|
135
|
+
- spec/typhoid/multi_spec.rb
|
136
|
+
- spec/typhoid/parser_spec.rb
|
137
|
+
- spec/typhoid/request_builder_spec.rb
|
138
|
+
- spec/typhoid/resource_spec.rb
|
139
|
+
- spec/typhoid/typhoid_spec.rb
|
140
|
+
- spec/typhoid/uri_spec.rb
|