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.
Files changed (44) hide show
  1. data/README.rdoc +390 -147
  2. data/lib/dupe/active_resource_extensions.rb +25 -0
  3. data/lib/dupe/attribute_template.rb +71 -0
  4. data/lib/dupe/cucumber_hooks.rb +15 -7
  5. data/lib/dupe/custom_mocks.rb +102 -0
  6. data/lib/dupe/database.rb +51 -0
  7. data/lib/dupe/dupe.rb +359 -372
  8. data/lib/dupe/log.rb +38 -0
  9. data/lib/dupe/mock.rb +50 -0
  10. data/lib/dupe/model.rb +55 -0
  11. data/lib/dupe/network.rb +38 -0
  12. data/lib/dupe/record.rb +35 -56
  13. data/lib/dupe/rest_validation.rb +16 -0
  14. data/lib/dupe/schema.rb +36 -0
  15. data/lib/dupe/sequence.rb +11 -10
  16. data/lib/dupe/singular_plural_detection.rb +9 -0
  17. data/lib/dupe/string.rb +6 -8
  18. data/lib/dupe/symbol.rb +3 -0
  19. data/lib/dupe.rb +13 -12
  20. data/rails_generators/dupe/templates/custom_mocks.rb +4 -34
  21. data/rails_generators/dupe/templates/dupe_setup.rb +3 -23
  22. data/spec/lib_specs/active_resource_extensions_spec.rb +29 -0
  23. data/spec/lib_specs/attribute_template_spec.rb +173 -0
  24. data/spec/lib_specs/database_spec.rb +133 -0
  25. data/spec/lib_specs/dupe_spec.rb +307 -0
  26. data/spec/lib_specs/log_spec.rb +78 -0
  27. data/spec/lib_specs/logged_request_spec.rb +22 -0
  28. data/spec/lib_specs/mock_definitions_spec.rb +32 -0
  29. data/spec/lib_specs/mock_spec.rb +67 -0
  30. data/spec/lib_specs/model_spec.rb +90 -0
  31. data/spec/lib_specs/network_spec.rb +77 -0
  32. data/spec/lib_specs/record_spec.rb +70 -0
  33. data/spec/lib_specs/rest_validation_spec.rb +17 -0
  34. data/spec/lib_specs/schema_spec.rb +90 -0
  35. data/spec/lib_specs/sequence_spec.rb +26 -0
  36. data/spec/lib_specs/string_spec.rb +31 -0
  37. data/spec/lib_specs/symbol_spec.rb +17 -0
  38. data/spec/spec_helper.rb +2 -5
  39. metadata +29 -7
  40. data/lib/dupe/active_resource.rb +0 -135
  41. data/lib/dupe/attribute.rb +0 -17
  42. data/lib/dupe/configuration.rb +0 -20
  43. data/lib/dupe/mock_service_response.rb +0 -55
  44. 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
@@ -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 Record
3
- attr_accessor :internal_attributes_hash
4
-
5
- def initialize(hash={})
6
- @internal_attributes_hash =
7
- hash.merge(hash) do |k,v|
8
- process_value(v)
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
- end
21
+ end
11
22
 
12
- # allows you to access a record like:
13
- # irb> book = Dupe::Record.new :title => 'The Carpet Makers', :author => {:name => 'Andreas Eschbach'}
14
- # irb> book.title
15
- # ==> 'The Carpet Makers'
16
- # irb> book.author.name
17
- # ==> 'Andreas Eschbach'
18
- # irb> book.genre = 'Science Fiction'
19
- # irb> book.genre
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
- end
29
-
30
- # allows you to access a record like:
31
- # irb> book = Dupe::Record.new :title => 'The Carpet Makers', :author => {:name => 'Andreas Eschbach'}
32
- # irb> book[:title]
33
- # ==> 'The Carpet Makers'
34
- # irb> book[:author][:name]
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
@@ -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 Dupe
2
- class Sequence #:nodoc:
3
- def initialize(start=0)
4
- @sequence_value = start
5
- end
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
- end
7
+
8
+ def next
9
+ @current_value += 1
10
+ @current_value - 1
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ module SingularPluralDetection #:nodoc:
2
+ def singular?
3
+ self.to_s.singularize == self.to_s
4
+ end
5
+
6
+ def plural?
7
+ self.to_s.pluralize == self.to_s
8
+ end
9
+ end
data/lib/dupe/string.rb CHANGED
@@ -1,9 +1,7 @@
1
- class String
2
- def plural?
3
- self.to_s == pluralize
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
@@ -0,0 +1,3 @@
1
+ class Symbol #:nodoc:
2
+ include SingularPluralDetection
3
+ end
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
- path = defined?(RAILS_ROOT) ? RAILS_ROOT + '/features/dupe_definitions' : '../features/dupe_definitions'
14
- if File.directory? path
15
- Dir[File.join(path, '*.rb')].each do |file|
16
- require file
17
- end
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
- module CustomMocks
2
- # Maps a service request url to a Dupe find. By default, Dupe will only
3
- # mock simple requests like SomeResource.find(some_id) or SomeResource.find(:all)
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
- Dupe.configure do |global_config|
2
- # set this to false if you don't want to see the mocked
3
- # xml output after each scenario
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