superdupe 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 (43) hide show
  1. data/README.markdown +10 -0
  2. data/lib/superdupe/active_resource_extensions.rb +161 -0
  3. data/lib/superdupe/attribute_template.rb +71 -0
  4. data/lib/superdupe/cucumber_hooks.rb +16 -0
  5. data/lib/superdupe/custom_mocks.rb +116 -0
  6. data/lib/superdupe/database.rb +69 -0
  7. data/lib/superdupe/dupe.rb +534 -0
  8. data/lib/superdupe/hash_pruner.rb +37 -0
  9. data/lib/superdupe/log.rb +38 -0
  10. data/lib/superdupe/mock.rb +127 -0
  11. data/lib/superdupe/model.rb +59 -0
  12. data/lib/superdupe/network.rb +56 -0
  13. data/lib/superdupe/record.rb +42 -0
  14. data/lib/superdupe/rest_validation.rb +16 -0
  15. data/lib/superdupe/schema.rb +52 -0
  16. data/lib/superdupe/sequence.rb +20 -0
  17. data/lib/superdupe/singular_plural_detection.rb +9 -0
  18. data/lib/superdupe/string.rb +7 -0
  19. data/lib/superdupe/symbol.rb +3 -0
  20. data/lib/superdupe.rb +20 -0
  21. data/rails_generators/dupe/dupe_generator.rb +20 -0
  22. data/rails_generators/dupe/templates/custom_mocks.rb +4 -0
  23. data/rails_generators/dupe/templates/definitions.rb +9 -0
  24. data/rails_generators/dupe/templates/load_dupe.rb +9 -0
  25. data/spec/lib_specs/active_resource_extensions_spec.rb +141 -0
  26. data/spec/lib_specs/attribute_template_spec.rb +173 -0
  27. data/spec/lib_specs/database_spec.rb +163 -0
  28. data/spec/lib_specs/dupe_spec.rb +522 -0
  29. data/spec/lib_specs/hash_pruner_spec.rb +73 -0
  30. data/spec/lib_specs/log_spec.rb +78 -0
  31. data/spec/lib_specs/logged_request_spec.rb +22 -0
  32. data/spec/lib_specs/mock_definitions_spec.rb +58 -0
  33. data/spec/lib_specs/mock_spec.rb +194 -0
  34. data/spec/lib_specs/model_spec.rb +95 -0
  35. data/spec/lib_specs/network_spec.rb +130 -0
  36. data/spec/lib_specs/record_spec.rb +70 -0
  37. data/spec/lib_specs/rest_validation_spec.rb +17 -0
  38. data/spec/lib_specs/schema_spec.rb +109 -0
  39. data/spec/lib_specs/sequence_spec.rb +39 -0
  40. data/spec/lib_specs/string_spec.rb +31 -0
  41. data/spec/lib_specs/symbol_spec.rb +17 -0
  42. data/spec/spec_helper.rb +8 -0
  43. metadata +142 -0
@@ -0,0 +1,127 @@
1
+ class Dupe
2
+ class Network #:nodoc:
3
+ class Mock #:nodoc:
4
+ class ResourceNotFoundError < StandardError; end
5
+
6
+ attr_reader :url_pattern
7
+ attr_reader :response
8
+
9
+ def initialize(url_pattern, response_proc=nil)
10
+ raise(
11
+ ArgumentError,
12
+ "The URL pattern parameter must be a type of regular expression."
13
+ ) unless url_pattern.kind_of?(Regexp)
14
+
15
+ @response = response_proc || proc {}
16
+ @url_pattern = url_pattern
17
+ end
18
+
19
+ def match?(url)
20
+ url_pattern =~ url ? true : false
21
+ end
22
+
23
+ def mocked_response(url, body = nil)
24
+ raise(
25
+ StandardError,
26
+ "Tried to mock a response to a non-matched url! This should never occur. My pattern: #{@url_pattern}. Url: #{url}"
27
+ ) unless match?(url)
28
+
29
+ grouped_results = url_pattern.match(url)[1..-1]
30
+ grouped_results << body if body
31
+ resp = @response.call *grouped_results
32
+ process_response(resp, url)
33
+ end
34
+
35
+ def process_response(resp, url)
36
+ raise NotImplementedError, "When you extend Dupe::Network::Mock, you must implement the #process_response instance method."
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ class Dupe
43
+ class Network
44
+ class GetMock < Mock #:nodoc:
45
+ def process_response(resp, url)
46
+ case resp
47
+
48
+ when NilClass
49
+ raise ResourceNotFoundError, "Failed with 404: the request '#{url}' returned nil."
50
+
51
+ when Dupe::Database::Record
52
+ resp = resp.to_xml_safe(:root => resp.__model__.name.to_s)
53
+
54
+ when Array
55
+ if resp.empty?
56
+ resp = [].to_xml :root => 'results'
57
+ else
58
+ resp = resp.map {|r| HashPruner.prune(r)}.to_xml(:root => resp.first.__model__.name.to_s.pluralize)
59
+ end
60
+ end
61
+ Dupe.network.log.add_request :get, url, resp
62
+ resp
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ class Dupe
69
+ class Network
70
+ class PostMock < Mock #:nodoc:
71
+
72
+ # returns a tuple representing the xml of the processed entity, plus the url to the entity.
73
+ def process_response(resp, url)
74
+ case resp
75
+
76
+ when NilClass
77
+ raise StandardError, "Failed with 500: the request '#{url}' returned nil."
78
+
79
+ when Dupe::Database::Record
80
+ new_path = "/#{resp.__model__.name.to_s.pluralize}/#{resp.id}.xml"
81
+ resp = resp.to_xml_safe(:root => resp.__model__.name.to_s)
82
+ Dupe.network.log.add_request :post, url, resp
83
+ return resp, new_path
84
+
85
+ else
86
+ raise StandardError, "Unknown PostMock Response. Your Post intercept mocks must return a Duped resource object or nil"
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ class Dupe
94
+ class Network
95
+ class PutMock < Mock #:nodoc:
96
+
97
+ # returns a tuple representing the xml of the processed entity, plus the url to the entity.
98
+ def process_response(resp, url)
99
+ case resp
100
+
101
+ when NilClass
102
+ raise StandardError, "Failed with 500: the request '#{url}' returned nil."
103
+
104
+ when Dupe::Database::Record
105
+ resp = nil
106
+ Dupe.network.log.add_request :put, url, ""
107
+ return resp, url
108
+
109
+ else
110
+ raise StandardError, "Unknown PutMock Response. Your Post intercept mocks must return a Duped resource object or nil"
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ class Dupe
118
+ class Network
119
+ class DeleteMock < Mock #:nodoc:
120
+ # logs the request
121
+ def process_response(resp, url)
122
+ Dupe.network.log.add_request :delete, url, ""
123
+ nil
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,59 @@
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
+ end
24
+ end
25
+
26
+ # called by the Dupe::Database#insert method
27
+ def run_after_create_callbacks(record)
28
+ @schema.after_create_callbacks.each do |callback|
29
+ callback.call record
30
+ end
31
+ end
32
+
33
+ private
34
+ def default_record
35
+ Database::Record.new.tap do |record|
36
+ # setup all the required attributes
37
+ @schema.attribute_templates.each do |attribute_template_name, attribute_template|
38
+ required_attribute_name, required_attribute_value =
39
+ attribute_template.generate
40
+ record[required_attribute_name] = required_attribute_value
41
+ end
42
+ end
43
+ end
44
+
45
+ def transform(attributes)
46
+ Database::Record.new.tap do |record|
47
+ # override the required attributes or create new attributes
48
+ attributes.each do |attribute_name, attribute_value|
49
+ if @schema.attribute_templates[attribute_name]
50
+ k, v = @schema.attribute_templates[attribute_name].generate attribute_value
51
+ record[attribute_name] = v
52
+ else
53
+ record[attribute_name] = attribute_value
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,56 @@
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, body=nil)
16
+ validate_request_type verb
17
+ match(verb, url).mocked_response(url, body)
18
+ end
19
+
20
+ def define_service_mock(verb, url_pattern, response_proc=nil)
21
+ validate_request_type verb
22
+ case verb
23
+ when :get
24
+ GetMock.new(url_pattern, response_proc).tap do |mock|
25
+ @mocks[verb] << mock
26
+ end
27
+ when :post
28
+ PostMock.new(url_pattern, response_proc).tap do |mock|
29
+ @mocks[verb].unshift mock
30
+ end
31
+ when :put
32
+ PutMock.new(url_pattern, response_proc).tap do |mock|
33
+ @mocks[verb].unshift mock
34
+ end
35
+ when :delete
36
+ DeleteMock.new(url_pattern, response_proc).tap do |mock|
37
+ @mocks[verb].unshift mock
38
+ end
39
+ else
40
+ raise StandardError, "Dupe does not support mocking #{verb.to_s.upcase} requests."
41
+ end
42
+ end
43
+
44
+ private
45
+ def match(verb, url)
46
+ @mocks[verb].each do |mock|
47
+ return mock if mock.match?(url)
48
+ end
49
+ raise(
50
+ RequestNotFoundError,
51
+ "No mocked service response found for '#{url}'"
52
+ )
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,42 @@
1
+ class Dupe
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]
20
+ end
21
+ end
22
+
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
31
+ end
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] == '='
39
+ end
40
+ end
41
+ 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,52 @@
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
+
35
+ def uniquify(*args)
36
+ raise ArgumentError, "You must pass at least one attribute to uniquify." if args.empty?
37
+ raise ArgumentError, "You may only pass symbols to uniquify." unless all_members_of_class(args, Symbol)
38
+
39
+ args.each do |attribute|
40
+ after_create do |record|
41
+ record[attribute] = "#{record.__model__.name} #{record.id} #{attribute}" unless record[attribute]
42
+ end
43
+ end
44
+ end
45
+
46
+ private
47
+ def all_members_of_class(ary, klass)
48
+ ary.inject(true) {|bool, v| bool && v.kind_of?(klass)}
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,20 @@
1
+ class Sequence #:nodoc:
2
+ attr_reader :current_value
3
+
4
+ def initialize(starting_at=1, sequencer=nil)
5
+ @current_value = starting_at
6
+ if sequencer && sequencer.arity != 1
7
+ raise ArgumentError, "Your block must accept a single parameter"
8
+ end
9
+ @transformer = sequencer
10
+ end
11
+
12
+ def next
13
+ @current_value += 1
14
+ if @transformer
15
+ @transformer.call(@current_value - 1)
16
+ else
17
+ @current_value - 1
18
+ end
19
+ end
20
+ 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
@@ -0,0 +1,7 @@
1
+ class String #:nodoc:
2
+ include SingularPluralDetection
3
+
4
+ def indent(spaces=2)
5
+ split("\n").map {|l| (" " * spaces) + l }.join("\n")
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ class Symbol #:nodoc:
2
+ include SingularPluralDetection
3
+ end
data/lib/superdupe.rb ADDED
@@ -0,0 +1,20 @@
1
+ require 'superdupe/hash_pruner'
2
+ require 'active_resource'
3
+ require 'active_resource/http_mock'
4
+ require 'superdupe/singular_plural_detection'
5
+ require 'superdupe/symbol'
6
+ require 'superdupe/string'
7
+ require 'superdupe/rest_validation'
8
+ require 'superdupe/mock'
9
+ require 'superdupe/log'
10
+ require 'superdupe/network'
11
+ require 'superdupe/dupe'
12
+ require 'superdupe/database'
13
+ require 'superdupe/sequence'
14
+ require 'superdupe/record'
15
+ require 'superdupe/attribute_template'
16
+ require 'superdupe/schema'
17
+ require 'superdupe/model'
18
+ require 'superdupe/custom_mocks'
19
+ require 'superdupe/active_resource_extensions'
20
+ require 'superdupe/cucumber_hooks'
@@ -0,0 +1,20 @@
1
+ class DupeGenerator < Rails::Generator::Base
2
+ def manifest
3
+ record do |m|
4
+ # make sure the features and features/support directories exist
5
+ m.directory 'features/support'
6
+ m.directory 'features/dupe'
7
+ m.directory 'features/dupe/custom_mocks'
8
+ m.directory 'features/dupe/definitions'
9
+
10
+ # copy the custom_mocks.rb example file into features/dupe/custom_mocks
11
+ m.template 'custom_mocks.rb', 'features/dupe/custom_mocks/custom_mocks.rb'
12
+
13
+ # copy the definitions.rb example file into features/dupe/definitions
14
+ m.template 'definitions.rb', 'features/dupe/definitions/definitions.rb'
15
+
16
+ # copy the load_dupe.rb into the features/support directory
17
+ m.template 'load_dupe.rb', 'features/support/load_dupe.rb'
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,4 @@
1
+ # Example:
2
+ # Get %r{/books/([\d\w-]+)\.xml} do |label|
3
+ # Dupe.find(:book) {|b| b.label == label}
4
+ # end
@@ -0,0 +1,9 @@
1
+ # set this to false if you don't care to see the requests mocked
2
+ # for each cucumber scenario.
3
+ Dupe.debug = true
4
+
5
+
6
+ # Define your models
7
+ # Dupe.define :example_model do |attrs|
8
+ # attrs.example_attribute 'Example Default Value'
9
+ # end
@@ -0,0 +1,9 @@
1
+ # First, load the definitions
2
+ Dir[File.join(File.dirname(__FILE__), '../dupe/definitions/*.rb')].each do |file|
3
+ require file
4
+ end
5
+
6
+ # next, load the custom mocks
7
+ Dir[File.join(File.dirname(__FILE__), '../dupe/custom_mocks/*.rb')].each do |file|
8
+ require file
9
+ end
@@ -0,0 +1,141 @@
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
+
30
+ describe "#post" do
31
+ before do
32
+ @book = Dupe.create :book, :label => 'rooby', :title => 'Rooby'
33
+ @book.delete(:id)
34
+ class Book < ActiveResource::Base
35
+ self.site = ''
36
+ end
37
+ end
38
+
39
+ it "should pass a request off to the Dupe network if the original request failed" do
40
+ Dupe.network.should_receive(:request).with(:post, '/books.xml', Hash.from_xml(@book.to_xml(:root => 'book'))["book"] ).once
41
+ book = Book.create({:label => 'rooby', :title => 'Rooby'})
42
+ end
43
+
44
+ it "should parse the xml and turn the result into active resource objects" do
45
+ book = Book.create({:label => 'rooby', :title => 'Rooby'})
46
+ book.id.should == 2
47
+ book.title.should == 'Rooby'
48
+ book.label.should == 'rooby'
49
+ end
50
+
51
+ it "should make ActiveResource throw an unprocessable entity exception if our Post mock throws a Dupe::UnprocessableEntity exception" do
52
+ Post %r{/books\.xml} do |post_data|
53
+ raise Dupe::UnprocessableEntity.new(:title => "must be present.") unless post_data["title"]
54
+ Dupe.create :book, post_data
55
+ end
56
+
57
+ b = Book.create
58
+ b.new?.should be_true
59
+ b.errors.errors.should_not be_empty
60
+ b = Book.create(:title => "hello")
61
+ b.new?.should be_false
62
+ b.errors.should be_empty
63
+ end
64
+ end
65
+
66
+ describe "#put" do
67
+ before do
68
+ @book = Dupe.create :book, :label => 'rooby', :title => 'Rooby'
69
+ class Book < ActiveResource::Base
70
+ self.site = ''
71
+ end
72
+ @ar_book = Book.find(1)
73
+ end
74
+
75
+ it "should pass a request off to the Dupe network if the original request failed" do
76
+ Dupe.network.should_receive(:request).with(:put, '/books/1.xml', Hash.from_xml(@book.merge(:title => "Rails!").to_xml(:root => 'book'))["book"].symbolize_keys!).once
77
+ @ar_book.title = 'Rails!'
78
+ @ar_book.save
79
+ end
80
+
81
+ it "should parse the xml and turn the result into active resource objects" do
82
+ @book.title.should == "Rooby"
83
+ @ar_book.title = "Rails!"
84
+ @ar_book.save
85
+ @ar_book.new?.should == false
86
+ @ar_book.valid?.should == true
87
+ @ar_book.id.should == 1
88
+ @ar_book.label.should == "rooby"
89
+ @book.title.should == "Rails!"
90
+ @book.id.should == 1
91
+ @book.label.should == 'rooby'
92
+ end
93
+
94
+ it "should make ActiveResource throw an unprocessable entity exception if our Put mock throws a Dupe::UnprocessableEntity exception" do
95
+ Put %r{/books/(\d+)\.xml} do |id, put_data|
96
+ raise Dupe::UnprocessableEntity.new(:title => " must be present.") unless put_data[:title]
97
+ Dupe.find(:book) {|b| b.id == id.to_i}.merge!(put_data)
98
+ end
99
+
100
+ @ar_book.title = nil
101
+ @ar_book.save.should == false
102
+ @ar_book.errors.on_base.should_not be_empty
103
+
104
+ @ar_book.title = "Rails!"
105
+ @ar_book.save.should == true
106
+ # the following line should be true, were it not for a bug in active_resource 2.3.3 - 2.3.5
107
+ # i reported the bug here: https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/4169-activeresourcebasesave-put-doesnt-clear-out-errors
108
+ # @ar_book.errors.should be_empty
109
+ end
110
+ end
111
+
112
+ describe "#delete" do
113
+ before do
114
+ @book = Dupe.create :book, :label => 'rooby', :title => 'Rooby'
115
+ class Book < ActiveResource::Base
116
+ self.site = ''
117
+ end
118
+ @ar_book = Book.find(1)
119
+ end
120
+
121
+ it "should pass a request off to the Dupe network if the original request failed" do
122
+ Dupe.network.should_receive(:request).with(:delete, '/books/1.xml').once
123
+ @ar_book.destroy
124
+ end
125
+
126
+ it "trigger a Dupe.delete to delete the mocked resource from the duped database" do
127
+ Dupe.find(:books).length.should == 1
128
+ @ar_book.destroy
129
+ Dupe.find(:books).length.should == 0
130
+ end
131
+
132
+ it "should allow you to override the default DELETE intercept mock" do
133
+ Delete %r{/books/(\d+)\.xml} do |id|
134
+ raise StandardError, "Testing Delete override"
135
+ end
136
+
137
+ proc {@ar_book.destroy}.should raise_error(StandardError, "Testing Delete override")
138
+ end
139
+ end
140
+
141
+ end