dupe 0.3.7 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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