dupe 0.3.7 → 0.4.0
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.
- 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
|