dupe 0.3.7 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +390 -147
- data/lib/dupe/active_resource_extensions.rb +25 -0
- data/lib/dupe/attribute_template.rb +71 -0
- data/lib/dupe/cucumber_hooks.rb +15 -7
- data/lib/dupe/custom_mocks.rb +102 -0
- data/lib/dupe/database.rb +51 -0
- data/lib/dupe/dupe.rb +359 -372
- data/lib/dupe/log.rb +38 -0
- data/lib/dupe/mock.rb +50 -0
- data/lib/dupe/model.rb +55 -0
- data/lib/dupe/network.rb +38 -0
- data/lib/dupe/record.rb +35 -56
- data/lib/dupe/rest_validation.rb +16 -0
- data/lib/dupe/schema.rb +36 -0
- data/lib/dupe/sequence.rb +11 -10
- data/lib/dupe/singular_plural_detection.rb +9 -0
- data/lib/dupe/string.rb +6 -8
- data/lib/dupe/symbol.rb +3 -0
- data/lib/dupe.rb +13 -12
- data/rails_generators/dupe/templates/custom_mocks.rb +4 -34
- data/rails_generators/dupe/templates/dupe_setup.rb +3 -23
- data/spec/lib_specs/active_resource_extensions_spec.rb +29 -0
- data/spec/lib_specs/attribute_template_spec.rb +173 -0
- data/spec/lib_specs/database_spec.rb +133 -0
- data/spec/lib_specs/dupe_spec.rb +307 -0
- data/spec/lib_specs/log_spec.rb +78 -0
- data/spec/lib_specs/logged_request_spec.rb +22 -0
- data/spec/lib_specs/mock_definitions_spec.rb +32 -0
- data/spec/lib_specs/mock_spec.rb +67 -0
- data/spec/lib_specs/model_spec.rb +90 -0
- data/spec/lib_specs/network_spec.rb +77 -0
- data/spec/lib_specs/record_spec.rb +70 -0
- data/spec/lib_specs/rest_validation_spec.rb +17 -0
- data/spec/lib_specs/schema_spec.rb +90 -0
- data/spec/lib_specs/sequence_spec.rb +26 -0
- data/spec/lib_specs/string_spec.rb +31 -0
- data/spec/lib_specs/symbol_spec.rb +17 -0
- data/spec/spec_helper.rb +2 -5
- metadata +29 -7
- data/lib/dupe/active_resource.rb +0 -135
- data/lib/dupe/attribute.rb +0 -17
- data/lib/dupe/configuration.rb +0 -20
- data/lib/dupe/mock_service_response.rb +0 -55
- data/spec/lib_specs/dupe_record_spec.rb +0 -57
data/lib/dupe/log.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
class Dupe
|
2
|
+
class Network #:nodoc:
|
3
|
+
class Log #:nodoc:
|
4
|
+
include RestValidation #:nodoc:
|
5
|
+
attr_reader :requests #:nodoc:
|
6
|
+
|
7
|
+
class Request #:nodoc:
|
8
|
+
attr_reader :verb, :path, :response_body
|
9
|
+
|
10
|
+
def initialize(verb, path, response_body)
|
11
|
+
@verb, @path, @response_body = verb, path, response_body
|
12
|
+
end
|
13
|
+
|
14
|
+
def pretty_print
|
15
|
+
"Request: #{@verb.to_s.upcase} #{@path}\n" +
|
16
|
+
"Response:\n" + @response_body.indent
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize #:nodoc:
|
21
|
+
@requests = []
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_request(verb, path, response_body='') #:nodoc:
|
25
|
+
validate_request_type verb
|
26
|
+
@requests << Request.new(verb, path, response_body)
|
27
|
+
end
|
28
|
+
|
29
|
+
def pretty_print
|
30
|
+
"Logged Requests:\n" + requests.map {|r| r.pretty_print.indent }.join("\n\n") + "\n\n"
|
31
|
+
end
|
32
|
+
|
33
|
+
def reset #:nodoc:
|
34
|
+
@requests = []
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/dupe/mock.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
class Dupe
|
2
|
+
class Network #:nodoc:
|
3
|
+
class Mock #:nodoc:
|
4
|
+
include Dupe::Network::RestValidation
|
5
|
+
|
6
|
+
attr_reader :verb
|
7
|
+
attr_reader :url_pattern
|
8
|
+
attr_reader :response
|
9
|
+
|
10
|
+
def initialize(verb, url_pattern, response_proc=nil)
|
11
|
+
validate_request_type verb
|
12
|
+
|
13
|
+
raise(
|
14
|
+
ArgumentError,
|
15
|
+
"The URL pattern parameter must be a type of regular expression."
|
16
|
+
) unless url_pattern.kind_of?(Regexp)
|
17
|
+
|
18
|
+
@response = response_proc || proc {}
|
19
|
+
@verb = verb
|
20
|
+
@url_pattern = url_pattern
|
21
|
+
end
|
22
|
+
|
23
|
+
def match?(url)
|
24
|
+
url_pattern =~ url ? true : false
|
25
|
+
end
|
26
|
+
|
27
|
+
def mocked_response(url)
|
28
|
+
raise(
|
29
|
+
StandardError,
|
30
|
+
"Tried to mock a response to a non-matched url! This should never occur."
|
31
|
+
) unless match?(url)
|
32
|
+
|
33
|
+
grouped_results = url_pattern.match(url)[1..-1]
|
34
|
+
resp = @response.call *grouped_results
|
35
|
+
|
36
|
+
case resp
|
37
|
+
when Dupe::Database::Record
|
38
|
+
resp = resp.to_xml(:root => resp.__model__.name.to_s)
|
39
|
+
when Array
|
40
|
+
resp = resp.to_xml(:root => resp.first.__model__.name.to_s.pluralize)
|
41
|
+
end
|
42
|
+
|
43
|
+
Dupe.network.log.add_request @verb, url, resp
|
44
|
+
|
45
|
+
resp
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/dupe/model.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
class Dupe
|
2
|
+
class Model #:nodoc:
|
3
|
+
attr_reader :schema
|
4
|
+
attr_reader :name
|
5
|
+
attr_reader :id_sequence
|
6
|
+
|
7
|
+
def initialize(name)
|
8
|
+
@schema = Dupe::Model::Schema.new
|
9
|
+
@name = name.to_sym
|
10
|
+
@id_sequence = Sequence.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def define(definition_proc)
|
14
|
+
definition_proc.call @schema
|
15
|
+
end
|
16
|
+
|
17
|
+
def create(attributes={})
|
18
|
+
Database::Record.new.tap do |record|
|
19
|
+
record.__model__ = self
|
20
|
+
record.id = @id_sequence.next
|
21
|
+
record.merge! default_record
|
22
|
+
record.merge! transform(attributes)
|
23
|
+
@schema.after_create_callbacks.each do |callback|
|
24
|
+
callback.call record
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
def default_record
|
31
|
+
Database::Record.new.tap do |record|
|
32
|
+
# setup all the required attributes
|
33
|
+
@schema.attribute_templates.each do |attribute_template_name, attribute_template|
|
34
|
+
required_attribute_name, required_attribute_value =
|
35
|
+
attribute_template.generate
|
36
|
+
record[required_attribute_name] = required_attribute_value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def transform(attributes)
|
42
|
+
Database::Record.new.tap do |record|
|
43
|
+
# override the required attributes or create new attributes
|
44
|
+
attributes.each do |attribute_name, attribute_value|
|
45
|
+
if @schema.attribute_templates[attribute_name]
|
46
|
+
k, v = @schema.attribute_templates[attribute_name].generate attribute_value
|
47
|
+
record[attribute_name] = v
|
48
|
+
else
|
49
|
+
record[attribute_name] = attribute_value
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/dupe/network.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
class Dupe
|
2
|
+
class Network #:nodoc:
|
3
|
+
include RestValidation #:nodoc:
|
4
|
+
|
5
|
+
class RequestNotFoundError < StandardError; end #:nodoc:
|
6
|
+
|
7
|
+
attr_reader :mocks, :log
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@mocks = {}
|
11
|
+
@log = Dupe::Network::Log.new
|
12
|
+
VERBS.each { |verb| @mocks[verb] = [] }
|
13
|
+
end
|
14
|
+
|
15
|
+
def request(verb, url)
|
16
|
+
validate_request_type verb
|
17
|
+
match(verb, url).mocked_response(url)
|
18
|
+
end
|
19
|
+
|
20
|
+
def define_service_mock(verb, url_pattern, response_proc=nil)
|
21
|
+
Mock.new(verb, url_pattern, response_proc).tap do |mock|
|
22
|
+
@mocks[verb] << mock
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def match(verb, url)
|
28
|
+
@mocks[verb].each do |mock|
|
29
|
+
return mock if mock.match?(url)
|
30
|
+
end
|
31
|
+
raise(
|
32
|
+
RequestNotFoundError,
|
33
|
+
"No mocked service response found for '#{url}'"
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
data/lib/dupe/record.rb
CHANGED
@@ -1,63 +1,42 @@
|
|
1
1
|
class Dupe
|
2
|
-
class
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
2
|
+
class Database #:nodoc:
|
3
|
+
class Record < Hash #:nodoc:
|
4
|
+
attr_accessor :__model__
|
5
|
+
|
6
|
+
def id
|
7
|
+
self[:id]
|
8
|
+
end
|
9
|
+
|
10
|
+
def id=(value)
|
11
|
+
self[:id] = value
|
12
|
+
end
|
13
|
+
|
14
|
+
def method_missing(method_name, *args, &block)
|
15
|
+
if attempting_to_assign(method_name)
|
16
|
+
method_name = method_name.to_s[0..-2].to_sym
|
17
|
+
self[method_name] = args.first
|
18
|
+
else
|
19
|
+
self[method_name.to_sym]
|
9
20
|
end
|
10
|
-
|
21
|
+
end
|
11
22
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
# ==> 'Science Fiction'
|
21
|
-
def method_missing(method_name, *args, &block)
|
22
|
-
if method_name.to_s[-1..-1] == '='
|
23
|
-
@internal_attributes_hash[method_name.to_s[0..-2].to_sym] =
|
24
|
-
process_value(args[0])
|
25
|
-
else
|
26
|
-
@internal_attributes_hash[method_name.to_sym]
|
23
|
+
def record_inspect
|
24
|
+
class_name = __model__ ? "Duped::#{__model__.name.to_s.titleize}" : self.class.to_s
|
25
|
+
"<##{class_name}".tap do |inspection|
|
26
|
+
keys.each do |key|
|
27
|
+
inspection << " #{key}=#{self[key].inspect}"
|
28
|
+
end
|
29
|
+
inspection << ">"
|
30
|
+
end
|
27
31
|
end
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
# ==> 'Andreas Eschbach'
|
36
|
-
# irb> book.genre = 'Science Fiction'
|
37
|
-
# irb> book.genre
|
38
|
-
# ==> 'Science Fiction'
|
39
|
-
def [](key)
|
40
|
-
@internal_attributes_hash[key.to_sym]
|
41
|
-
end
|
42
|
-
|
43
|
-
# allows you to set a record like:
|
44
|
-
# irb> book = Dupe::Record.new :title => 'The Carpet Makers', :author => {:name => 'Andreas Eschbach'}
|
45
|
-
# irb> book[:genre] = 'Science Fiction'
|
46
|
-
# irb> book[:genre]
|
47
|
-
# ==> 'Science Fiction'
|
48
|
-
def []=(key, value)
|
49
|
-
@internal_attributes_hash[key.to_sym] = process_value(value)
|
50
|
-
end
|
51
|
-
|
52
|
-
private
|
53
|
-
def process_value(v)
|
54
|
-
if v.is_a?(Hash)
|
55
|
-
Record.new(v)
|
56
|
-
elsif v.is_a?(Array)
|
57
|
-
v.map {|r| process_value(r)}
|
58
|
-
else
|
59
|
-
v
|
32
|
+
|
33
|
+
alias_method :hash_inspect, :inspect
|
34
|
+
alias_method :inspect, :record_inspect
|
35
|
+
|
36
|
+
private
|
37
|
+
def attempting_to_assign(method_name)
|
38
|
+
method_name.to_s[-1..-1] == '='
|
60
39
|
end
|
61
40
|
end
|
62
41
|
end
|
63
|
-
end
|
42
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class Dupe
|
2
|
+
class Network #:nodoc:
|
3
|
+
#:nodoc:
|
4
|
+
class UnknownRestVerbError < StandardError; end #:nodoc:
|
5
|
+
VERBS = [:get, :post, :put, :delete] #:nodoc:
|
6
|
+
|
7
|
+
module RestValidation #:nodoc:
|
8
|
+
def validate_request_type(verb)
|
9
|
+
raise(
|
10
|
+
Dupe::Network::UnknownRestVerbError,
|
11
|
+
"Unknown REST verb ':#{verb}'. Valid REST verbs are :get, :post, :put, and :delete."
|
12
|
+
) unless Dupe::Network::VERBS.include?(verb)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/dupe/schema.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
class Dupe
|
2
|
+
class Model #:nodoc:
|
3
|
+
class Schema #:nodoc:
|
4
|
+
attr_reader :attribute_templates
|
5
|
+
attr_reader :after_create_callbacks
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@attribute_templates = {}
|
9
|
+
@after_create_callbacks = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def method_missing(method_name, *args, &block)
|
13
|
+
attribute_name = method_name.to_s[-1..-1] == '=' ? method_name.to_s[0..-2].to_sym : method_name
|
14
|
+
if block && block.arity < 1
|
15
|
+
default_value = block
|
16
|
+
transformer = nil
|
17
|
+
else
|
18
|
+
default_value = args[0]
|
19
|
+
transformer = block
|
20
|
+
end
|
21
|
+
|
22
|
+
@attribute_templates[method_name.to_sym] =
|
23
|
+
AttributeTemplate.new method_name.to_sym, :default => default_value, :transformer => transformer
|
24
|
+
end
|
25
|
+
|
26
|
+
def after_create(&block)
|
27
|
+
raise(
|
28
|
+
ArgumentError,
|
29
|
+
"You must pass a block that accepts a single parameter to 'after_create'"
|
30
|
+
) if !block || block.arity != 1
|
31
|
+
|
32
|
+
@after_create_callbacks << block
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/dupe/sequence.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
|
-
class
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
def next
|
8
|
-
@sequence_value += 1
|
9
|
-
end
|
1
|
+
class Sequence #:nodoc:
|
2
|
+
attr_reader :current_value
|
3
|
+
|
4
|
+
def initialize(starting_at=1)
|
5
|
+
@current_value = starting_at
|
10
6
|
end
|
11
|
-
|
7
|
+
|
8
|
+
def next
|
9
|
+
@current_value += 1
|
10
|
+
@current_value - 1
|
11
|
+
end
|
12
|
+
end
|
data/lib/dupe/string.rb
CHANGED
@@ -1,9 +1,7 @@
|
|
1
|
-
class String
|
2
|
-
|
3
|
-
|
1
|
+
class String #:nodoc:
|
2
|
+
include SingularPluralDetection
|
3
|
+
|
4
|
+
def indent(spaces=2)
|
5
|
+
split("\n").map {|l| (" " * spaces) + l }.join("\n")
|
4
6
|
end
|
5
|
-
|
6
|
-
def singular?
|
7
|
-
self.to_s == singularize
|
8
|
-
end
|
9
|
-
end
|
7
|
+
end
|
data/lib/dupe/symbol.rb
ADDED
data/lib/dupe.rb
CHANGED
@@ -1,18 +1,19 @@
|
|
1
1
|
require 'active_resource'
|
2
2
|
require 'active_resource/http_mock'
|
3
|
+
require 'dupe/singular_plural_detection'
|
4
|
+
require 'dupe/symbol'
|
3
5
|
require 'dupe/string'
|
6
|
+
require 'dupe/rest_validation'
|
7
|
+
require 'dupe/mock'
|
8
|
+
require 'dupe/log'
|
9
|
+
require 'dupe/network'
|
4
10
|
require 'dupe/dupe'
|
11
|
+
require 'dupe/database'
|
5
12
|
require 'dupe/sequence'
|
6
|
-
require 'dupe/mock_service_response'
|
7
|
-
require 'dupe/configuration'
|
8
|
-
require 'dupe/attribute'
|
9
|
-
require 'dupe/active_resource'
|
10
|
-
require 'dupe/cucumber_hooks'
|
11
13
|
require 'dupe/record'
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
end
|
14
|
+
require 'dupe/attribute_template'
|
15
|
+
require 'dupe/schema'
|
16
|
+
require 'dupe/model'
|
17
|
+
require 'dupe/custom_mocks'
|
18
|
+
require 'dupe/active_resource_extensions'
|
19
|
+
require 'dupe/cucumber_hooks'
|
@@ -1,34 +1,4 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
# For example, suppose you have a Book < ActiveResource::Base class, and
|
6
|
-
# somewhere your code does:
|
7
|
-
#
|
8
|
-
# Book.find :all, :params => {:limit => 10, :offset => 20}
|
9
|
-
#
|
10
|
-
# That in turn will send off a request to a url like:
|
11
|
-
#
|
12
|
-
# /books.xml?limit=10&offset=20
|
13
|
-
#
|
14
|
-
# In this file, you could add a "when" statement like:
|
15
|
-
#
|
16
|
-
# when %r{/books.xml\?limit=(\d+)&offset=(\d+)$}
|
17
|
-
# start = $2.to_i
|
18
|
-
# finish = start + $1.to_i - 1
|
19
|
-
# Dupe.find(:books)[start..finish]
|
20
|
-
def custom_service(url)
|
21
|
-
case url
|
22
|
-
|
23
|
-
# remove this and replace it with a real custom mock
|
24
|
-
when %r{/bogus_url}
|
25
|
-
''
|
26
|
-
|
27
|
-
else
|
28
|
-
raise StandardError.new(
|
29
|
-
"There is no custom service mapping for \"#{url}\"." +
|
30
|
-
"Now go to features/support/custom_mocks.rb and add it."
|
31
|
-
)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
1
|
+
# Example:
|
2
|
+
# Get %r{/books/([\d\w-]+)\.xml} do |label|
|
3
|
+
# Dupe.find(:book) {|b| b.label == label}
|
4
|
+
# end
|
@@ -1,23 +1,3 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
global_config.debug true
|
5
|
-
end
|
6
|
-
|
7
|
-
# You can also place your resource definitions in this file.
|
8
|
-
=begin
|
9
|
-
# Example resource definition
|
10
|
-
Dupe.define :books do |book|
|
11
|
-
book.name 'default name'
|
12
|
-
|
13
|
-
# supporting Dupe.create :book, :genre => 'Sci-fi'
|
14
|
-
book.genre do |genre_name|
|
15
|
-
Dupe.find(:genre) {|g| g.name == genre_name}
|
16
|
-
end
|
17
|
-
|
18
|
-
# supporting Dupe.create :book, :authors => 'Arthur C. Clarke, Gentry Lee'
|
19
|
-
book.authors do |author_names|
|
20
|
-
Dupe.find(:authors) {|a| author_names.split(/,\ */).include?(a.name)}
|
21
|
-
end
|
22
|
-
end
|
23
|
-
=end
|
1
|
+
# set this to false if you don't care to see the requests mocked
|
2
|
+
# for each cucumber scenario.
|
3
|
+
Dupe.debug = true
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe ActiveResource::Connection do
|
4
|
+
before do
|
5
|
+
Dupe.reset
|
6
|
+
end
|
7
|
+
|
8
|
+
describe "#get" do
|
9
|
+
before do
|
10
|
+
@book = Dupe.create :book, :title => 'Rooby', :label => 'rooby'
|
11
|
+
class Book < ActiveResource::Base
|
12
|
+
self.site = ''
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should pass a request off to the Dupe network if the original request failed" do
|
17
|
+
Dupe.network.should_receive(:request).with(:get, '/books.xml').once.and_return(Dupe.find(:books).to_xml(:root => 'books'))
|
18
|
+
books = Book.find(:all)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should parse the xml and turn the result into active resource objects" do
|
22
|
+
books = Book.find(:all)
|
23
|
+
books.length.should == 1
|
24
|
+
books.first.id.should == 1
|
25
|
+
books.first.title.should == 'Rooby'
|
26
|
+
books.first.label.should == 'rooby'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|