superdupe 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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
data/README.markdown ADDED
@@ -0,0 +1,10 @@
1
+ ## SuperDupe
2
+ SuperDupe provides two things:
3
+
4
+ * Mock ActiveResource objects like the originally gem dupe
5
+ * Superdupe send no requests to external services registerd by the ARes. It has an extra parameter to send explicitly extrnal requests.
6
+
7
+ SuperDupe is a fork of the originally gem dupe 0.5.1 (Matt Parker). At first, the gem try to use only the available mocked resources. If you have the requirement to send external requests without mocking, take an extra parameter for this situation.
8
+
9
+ # Install the gem
10
+ gem install superdupe
@@ -0,0 +1,161 @@
1
+ ActiveResource::HttpMock.instance_eval do #:nodoc:
2
+ def delete_mock(http_method, path) #:nodoc:
3
+ responses.reject! {|r| r[0].path == path && r[0].method == http_method}
4
+ end
5
+ end
6
+
7
+ module ActiveResource #:nodoc:
8
+ class Base
9
+ class << self
10
+
11
+ # Overwrite the configuration of ActiveResource authentication (self.user). Don't need it for mocking.
12
+ def user=(user)
13
+ end
14
+
15
+ # Overwrite the configuration of ActiveResource authentication (self.password). Don't need it for mocking.
16
+ def password=(password)
17
+ end
18
+
19
+ def find(*arguments)
20
+ scope = arguments.slice!(0)
21
+ options = arguments.slice!(0) || {}
22
+
23
+ case scope
24
+ when :all then find_every(options)
25
+ when :first then find_every(options).first
26
+ when :last then find_every(options).last
27
+ when :one then find_one(options)
28
+ else find_single(scope, options)
29
+ end
30
+ end
31
+
32
+ private
33
+ def find_every(options)
34
+ external_request = options[:external_request] if options.has_key?(:external_request)
35
+
36
+ case from = options[:from]
37
+ when Symbol
38
+ instantiate_collection(get(from, options[:params]))
39
+ when String
40
+ path = "#{from}#{query_string(options[:params])}"
41
+ instantiate_collection(connection.get(path, headers, external_request) || [])
42
+ else
43
+ prefix_options, query_options = split_options(options[:params])
44
+ path = collection_path(prefix_options, query_options)
45
+ instantiate_collection( (connection.get(path, headers, external_request) || []), prefix_options )
46
+ end
47
+ end
48
+
49
+ def find_one(options)
50
+ external_request = options[:external_request] if options.has_key?(:external_request)
51
+
52
+ case from = options[:from]
53
+ when Symbol
54
+ instantiate_record(get(from, options[:params]))
55
+ when String
56
+ path = "#{from}#{query_string(options[:params])}"
57
+ instantiate_record(connection.get(path, headers, external_request))
58
+ end
59
+ end
60
+
61
+ def find_single(scope, options)
62
+ external_request = options[:external_request] if options.has_key?(:external_request)
63
+
64
+ prefix_options, query_options = split_options(options[:params])
65
+ path = element_path(scope, prefix_options, query_options)
66
+ instantiate_record(connection.get(path, headers, external_request), prefix_options)
67
+ end
68
+ end
69
+ end
70
+
71
+ class Connection #:nodoc:
72
+ def get(path, headers = {}, external_request = false) #:nodoc:
73
+ if external_request
74
+ # Without mocking - standard ActiveResource
75
+ response = request(:get, path, build_request_headers(headers, :get))
76
+ else
77
+ mocked_response = Dupe.network.request(:get, path)
78
+ ActiveResource::HttpMock.respond_to do |mock|
79
+ mock.get(path, {}, mocked_response)
80
+ end
81
+ response = request(:get, path, build_request_headers(headers, :get))
82
+ ActiveResource::HttpMock.delete_mock(:get, path)
83
+ end
84
+ format.decode(response.body)
85
+ end
86
+
87
+
88
+ def post(path, body = '', headers = {}, external_request = false) #:nodoc:
89
+ if external_request
90
+ # external request - standard ActiveResource
91
+ response = request(:post, path, body.to_s, build_request_headers(headers, :post))
92
+ else
93
+ resource_hash = Hash.from_xml(body)
94
+ resource_hash = resource_hash[resource_hash.keys.first]
95
+ resource_hash = {} unless resource_hash.kind_of?(Hash)
96
+ begin
97
+ mocked_response, new_path = Dupe.network.request(:post, path, resource_hash)
98
+ error = false
99
+ rescue Dupe::UnprocessableEntity => e
100
+ mocked_response = {:error => e.message.to_s}.to_xml(:root => 'errors')
101
+ error = true
102
+ end
103
+ ActiveResource::HttpMock.respond_to do |mock|
104
+ if error
105
+ mock.post(path, {}, mocked_response, 422, "Content-Type" => 'application/xml')
106
+ else
107
+ mock.post(path, {}, mocked_response, 201, "Location" => new_path)
108
+ end
109
+ end
110
+ response = request(:post, path, body.to_s, build_request_headers(headers, :post))
111
+ ActiveResource::HttpMock.delete_mock(:post, path)
112
+ end
113
+ response
114
+ end
115
+
116
+
117
+ def put(path, body = '', headers = {}, external_request = false) #:nodoc:
118
+ if external_request
119
+ response = request(:put, path, body.to_s, build_request_headers(headers, :put))
120
+ else
121
+ resource_hash = Hash.from_xml(body)
122
+ resource_hash = resource_hash[resource_hash.keys.first]
123
+ resource_hash = {} unless resource_hash.kind_of?(Hash)
124
+ resource_hash.symbolize_keys!
125
+
126
+ begin
127
+ error = false
128
+ mocked_response, path = Dupe.network.request(:put, path, resource_hash)
129
+ rescue Dupe::UnprocessableEntity => e
130
+ mocked_response = {:error => e.message.to_s}.to_xml(:root => 'errors')
131
+ error = true
132
+ end
133
+ ActiveResource::HttpMock.respond_to do |mock|
134
+ if error
135
+ mock.put(path, {}, mocked_response, 422, "Content-Type" => 'application/xml')
136
+ else
137
+ mock.put(path, {}, mocked_response, 204)
138
+ end
139
+ end
140
+ response = request(:put, path, body.to_s, build_request_headers(headers, :put))
141
+ ActiveResource::HttpMock.delete_mock(:put, path)
142
+ end
143
+
144
+ response
145
+ end
146
+
147
+
148
+ def delete(path, headers = {}, external_request = false)
149
+ # External requests are not allowed!
150
+ Dupe.network.request(:delete, path)
151
+
152
+ ActiveResource::HttpMock.respond_to do |mock|
153
+ mock.delete(path, {}, nil, 200)
154
+ end
155
+ response = request(:delete, path, build_request_headers(headers, :delete))
156
+
157
+ ActiveResource::HttpMock.delete_mock(:delete, path)
158
+ response
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,71 @@
1
+ class Dupe
2
+ class Model #:nodoc:
3
+ class Schema #:nodoc:
4
+ # This class represents an attribute template.
5
+ # An attribute template consists of an attribute name (a symbol),
6
+ # a potential default value (nil if not specified),
7
+ # and a potential transformer proc.
8
+ class AttributeTemplate #:nodoc:
9
+
10
+ class NilValue; end
11
+
12
+ attr_reader :name
13
+ attr_reader :transformer
14
+ attr_reader :default
15
+
16
+ def initialize(name, options={})
17
+ default = options[:default]
18
+ transformer = options[:transformer]
19
+
20
+ if transformer
21
+ raise ArgumentError, "Your transformer must be a kind of proc." if !transformer.kind_of?(Proc)
22
+ raise ArgumentError, "Your transformer must accept a parameter." if transformer.arity != 1
23
+ end
24
+
25
+ @name = name
26
+ @default = default
27
+ @transformer = transformer
28
+ end
29
+
30
+ # Suppose we have the following attribute template:
31
+ #
32
+ # console> a = Dupe::Model::Schema::AttributeTemplate.new(:title)
33
+ #
34
+ # If we call generate with no parameter, we'll get back the following:
35
+ #
36
+ # console> a.generate
37
+ # ===> :title, nil
38
+ #
39
+ # If we call generate with a parameter, we'll get back the following:
40
+ #
41
+ # console> a.generate 'my value'
42
+ # ===> :title, 'my value'
43
+ #
44
+ # If we setup an attribute template with a transformer:
45
+ #
46
+ # console> a =
47
+ # Dupe::Model::Schema::AttributeTemplate.new(
48
+ # :title,
49
+ # :default => 'default value',
50
+ # :transformer => proc {|dont_care| 'test'}
51
+ # )
52
+ # Then we'll get back the following when we call generate:
53
+ #
54
+ # console> a.generate
55
+ # ===> :title, 'default value'
56
+ #
57
+ # console> a.generate 'my value'
58
+ # ===> :title, 'test'
59
+ def generate(value=NilValue)
60
+ if value == NilValue
61
+ v = @default.respond_to?(:call) ? @default.call : (@default.dup rescue @default)
62
+ else
63
+ v = (@transformer ? @transformer.call(value) : value)
64
+ end
65
+
66
+ return @name, v
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,16 @@
1
+ begin
2
+ After do |scenario|
3
+ # print the requests logged during the scenario if in Dupe debug mode
4
+ if Dupe.debug
5
+ log = Dupe.network.log.pretty_print
6
+ puts "\n\n" + log.indent(4) + "\n\n" if log
7
+ end
8
+
9
+ # remove any data created during the scenario from the dupe database
10
+ Dupe.database.truncate_tables
11
+
12
+ # clear out the network log
13
+ Dupe.network.log.reset
14
+ end
15
+ rescue
16
+ end
@@ -0,0 +1,116 @@
1
+ # Dupe knows how to handle simple find by id and find :all lookups from ActiveResource. But what about other requests we might potentially make?
2
+ #
3
+ # irb# Dupe.create :author, :name => 'Monkey', :published => true
4
+ # ==> <#Duped::Author name="Monkey" published=true id=1>
5
+ #
6
+ # irb# Dupe.create :author, :name => 'Tiger', :published => false
7
+ # ==> <#Duped::Author name="Tiger" published=false id=2>
8
+ #
9
+ # irb# class Author < ActiveResource::Base; self.site = ''; end
10
+ # ==> ""
11
+ #
12
+ # irb# Author.find :all, :from => :published
13
+ # Dupe::Network::RequestNotFoundError: No mocked service response found for '/authors/published.xml'
14
+ # from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/network.rb:32:in `match'
15
+ # from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/network.rb:17:in `request'
16
+ # from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/active_resource_extensions.rb:15:in `get'
17
+ # from /Library/Ruby/Gems/1.8/gems/activeresource-2.3.5/lib/active_resource/custom_methods.rb:57:in `get'
18
+ # from /Library/Ruby/Gems/1.8/gems/activeresource-2.3.5/lib/active_resource/base.rb:632:in `find_every'
19
+ # from /Library/Ruby/Gems/1.8/gems/activeresource-2.3.5/lib/active_resource/base.rb:582:in `find'
20
+ # from (irb):12
21
+ #
22
+ # Obviously, Dupe had no way of anticipating this possibility. However, you can create your own custom intercept mock for this:
23
+ #
24
+ # irb# Get %r{/authors/published.xml} do
25
+ # --# Dupe.find(:authors) {|a| a.published == true}
26
+ # --# end
27
+ # ==> #<Dupe::Network::Mock:0x1833e88 @url_pattern=/\/authors\/published.xml/, @verb=:get, @response=#<Proc:0x01833f14@(irb):13>
28
+ #
29
+ # irb# Author.find :all, :from => :published
30
+ # ==> [#<Author:0x1821d3c @attributes={"name"=>"Monkey", "published"=>true, "id"=>1}, prefix_options{}]
31
+ #
32
+ # irb# puts Dupe.network.log.pretty_print
33
+ #
34
+ # Logged Requests:
35
+ # Request: GET /authors/published.xml
36
+ # Response:
37
+ # <?xml version="1.0" encoding="UTF-8"?>
38
+ # <authors type="array">
39
+ # <author>
40
+ # <name>Monkey</name>
41
+ # <published type="boolean">true</published>
42
+ # <id type="integer">1</id>
43
+ # </author>
44
+ # </authors>
45
+ #
46
+ #
47
+ # The "Get" method requires a url pattern and a block. In most cases, your block will return a Dupe.find result. Internally, Dupe will transform that into XML. However, if your "Get" block returns a string, Dupe will use that as the response body and not attempt to do any transformations on it.
48
+ #
49
+ # Suppose instead the service expected us to pass published as a query string parameter:
50
+ #
51
+ # irb# Author.find :all, :params => {:published => true}
52
+ # Dupe::Network::RequestNotFoundError: No mocked service response found for '/authors.xml?published=true'
53
+ # from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/network.rb:32:in `match'
54
+ # from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/network.rb:17:in `request'
55
+ # from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/active_resource_extensions.rb:15:in `get'
56
+ # from /Library/Ruby/Gems/1.8/gems/activeresource-2.3.5/lib/active_resource/base.rb:639:in `find_every'
57
+ # from /Library/Ruby/Gems/1.8/gems/activeresource-2.3.5/lib/active_resource/base.rb:582:in `find'
58
+ # from (irb):18
59
+ #
60
+ # We can mock this with the following:
61
+ #
62
+ # irb# Get %r{/authors\.xml\?published=(true|false)$} do |published|
63
+ # --# if published == 'true'
64
+ # --# Dupe.find(:authors) {|a| a.published == true}
65
+ # --# else
66
+ # --# Dupe.find(:authors) {|a| a.published == false}
67
+ # --# end
68
+ # --# end
69
+ #
70
+ # irb# Author.find :all, :params => {:published => true}
71
+ # ==> [#<Author:0x17db094 @attributes={"name"=>"Monkey", "published"=>true, "id"=>1}, prefix_options{}]
72
+ #
73
+ # irb# Author.find :all, :params => {:published => false}
74
+ # ==> [#<Author:0x17c68c4 @attributes={"name"=>"Tiger", "published"=>false, "id"=>2}, prefix_options{}]
75
+ #
76
+ # irb# puts Dupe.network.log.pretty_print
77
+ #
78
+ # Logged Requests:
79
+ # Request: GET /authors.xml?published=true
80
+ # Response:
81
+ # <?xml version="1.0" encoding="UTF-8"?>
82
+ # <authors type="array">
83
+ # <author>
84
+ # <name>Monkey</name>
85
+ # <published type="boolean">true</published>
86
+ # <id type="integer">1</id>
87
+ # </author>
88
+ # </authors>
89
+ #
90
+ # Request: GET /authors.xml?published=false
91
+ # Response:
92
+ # <?xml version="1.0" encoding="UTF-8"?>
93
+ # <authors type="array">
94
+ # <author>
95
+ # <name>Tiger</name>
96
+ # <published type="boolean">false</published>
97
+ # <id type="integer">2</id>
98
+ # </author>
99
+ # </authors>
100
+
101
+
102
+ def Get(url_pattern, &block)
103
+ Dupe.network.define_service_mock :get, url_pattern, block
104
+ end
105
+
106
+ def Post(url_pattern, &block)
107
+ Dupe.network.define_service_mock :post, url_pattern, block
108
+ end
109
+
110
+ def Put(url_pattern, &block)
111
+ Dupe.network.define_service_mock :put, url_pattern, block
112
+ end
113
+
114
+ def Delete(url_pattern, &block)
115
+ Dupe.network.define_service_mock :delete, url_pattern, block
116
+ end
@@ -0,0 +1,69 @@
1
+ class Dupe
2
+ class Database #:nodoc:
3
+ attr_reader :tables
4
+
5
+ #:nodoc:
6
+ class TableDoesNotExistError < StandardError; end
7
+
8
+ #:nodoc:
9
+ class InvalidQueryError < StandardError; end
10
+
11
+ # by default, there are not tables in the database
12
+ def initialize
13
+ @tables = {}
14
+ end
15
+
16
+ # database.delete :books # --> would delete all books
17
+ # database.delete :book # --> would delete the first book record found
18
+ # database.delete :book, proc {|b| b.id < 10} # --> would delete all books found who's id is less than 10
19
+ # database.delete :books, proc {|b| b.id < 10} # --> would delete all books found who's id is less than 10
20
+ def delete(resource_name, conditions=nil)
21
+ model_name = resource_name.to_s.singularize.to_sym
22
+ raise StandardError, "Invalid DELETE operation: The resource #{model_name} has not been defined" unless @tables[model_name]
23
+
24
+ if conditions
25
+ @tables[model_name].reject!(&conditions)
26
+ elsif resource_name.singular?
27
+ @tables[model_name].shift
28
+ elsif resource_name.plural?
29
+ @tables[model_name] = []
30
+ end
31
+ end
32
+
33
+ # pass in a Dupe::Database::Record object, and this method will store the record
34
+ # in the appropriate table
35
+ def insert(record)
36
+ if !record.kind_of?(Dupe::Database::Record) || !record.__model__ || !record[:id]
37
+ raise ArgumentError, "You may only insert well-defined Dupe::Database::Record objects"
38
+ end
39
+ @tables[record.__model__.name] ||= []
40
+ @tables[record.__model__.name] << record
41
+ record.__model__.run_after_create_callbacks(record)
42
+ end
43
+
44
+ # pass in a model_name (e.g., :book) and optionally a proc with
45
+ # conditions (e.g., {|b| b.genre == 'Science Fiction'})
46
+ # and recieve a (possibly empty) array of results
47
+ def select(model_name, conditions=nil)
48
+ raise TableDoesNotExistError, "The table ':#{model_name}' does not exist." unless @tables[model_name]
49
+ raise(
50
+ InvalidQueryError,
51
+ "There was a problem with your select conditions. Please consult the API."
52
+ ) if conditions and (!conditions.kind_of?(Proc) || conditions.arity != 1)
53
+
54
+ return @tables[model_name] if !conditions
55
+ @tables[model_name].select {|r| conditions.call(r)}
56
+ end
57
+
58
+ def create_table(model_name)
59
+ @tables[model_name.to_sym] ||= []
60
+ end
61
+
62
+ def truncate_tables
63
+ @tables.each do |table_name, table_records|
64
+ @tables[table_name] = []
65
+ end
66
+ end
67
+
68
+ end
69
+ end