shokkenki-consumer 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. data/lib/shokkenki/consumer/configuration/provider_configuration.rb +30 -0
  2. data/lib/shokkenki/consumer/configuration/session.rb +38 -0
  3. data/lib/shokkenki/consumer/consumer.rb +9 -0
  4. data/lib/shokkenki/consumer/dsl/http_methods.rb +16 -0
  5. data/lib/shokkenki/consumer/dsl/order.rb +70 -0
  6. data/lib/shokkenki/consumer/dsl/session.rb +13 -0
  7. data/lib/shokkenki/consumer/dsl/term_dsl.rb +13 -0
  8. data/lib/shokkenki/consumer/model/fixture.rb +21 -0
  9. data/lib/shokkenki/consumer/model/interaction.rb +29 -0
  10. data/lib/shokkenki/consumer/model/patronage.rb +31 -0
  11. data/lib/shokkenki/consumer/model/provider.rb +33 -0
  12. data/lib/shokkenki/consumer/model/role.rb +27 -0
  13. data/lib/shokkenki/consumer/model/simplification.rb +16 -0
  14. data/lib/shokkenki/consumer/model/ticket.rb +38 -0
  15. data/lib/shokkenki/consumer/rspec/example_group_binding.rb +17 -0
  16. data/lib/shokkenki/consumer/rspec/hooks.rb +32 -0
  17. data/lib/shokkenki/consumer/rspec/rspec_configuration.rb +25 -0
  18. data/lib/shokkenki/consumer/rspec.rb +2 -0
  19. data/lib/shokkenki/consumer/session.rb +83 -0
  20. data/lib/shokkenki/consumer/stubber/http_stubber.rb +97 -0
  21. data/lib/shokkenki/consumer/stubber/interaction.rb +51 -0
  22. data/lib/shokkenki/consumer/stubber/interactions.rb +41 -0
  23. data/lib/shokkenki/consumer/stubber/interactions_middleware.rb +34 -0
  24. data/lib/shokkenki/consumer/stubber/rack_response.rb +28 -0
  25. data/lib/shokkenki/consumer/stubber/request.rb +72 -0
  26. data/lib/shokkenki/consumer/stubber/restful_middleware.rb +33 -0
  27. data/lib/shokkenki/consumer/stubber/server.rb +69 -0
  28. data/lib/shokkenki/consumer/stubber/server_application_error.rb +16 -0
  29. data/lib/shokkenki/consumer/stubber/stub_server_middleware.rb +46 -0
  30. data/lib/shokkenki/consumer/stubber/stubbed_response_middleware.rb +34 -0
  31. data/lib/shokkenki/consumer/stubber/unmatched_requests_middleware.rb +22 -0
  32. data/lib/shokkenki/consumer/stubber/unused_interactions_middleware.rb +21 -0
  33. data/lib/shokkenki/consumer/version.rb +7 -0
  34. metadata +258 -0
@@ -0,0 +1,30 @@
1
+ require_relative '../model/provider'
2
+
3
+ module Shokkenki
4
+ module Consumer
5
+ module Configuration
6
+ class ProviderConfiguration
7
+
8
+ attr_writer :label
9
+
10
+ def initialize name, stubber_classes
11
+ @name = name
12
+ @stubber_classes = stubber_classes
13
+ end
14
+
15
+ def stub_with stubber_name, attributes = {}
16
+ stubber_class = @stubber_classes[stubber_name] || raise("No stubber found named '#{stubber_name}'.")
17
+ @stubber = stubber_class.new attributes
18
+ end
19
+
20
+ def to_provider
21
+ Shokkenki::Consumer::Model::Provider.new(
22
+ :stubber => @stubber,
23
+ :name => @name,
24
+ :label => @label
25
+ )
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,38 @@
1
+ require_relative 'provider_configuration'
2
+ require_relative '../stubber/http_stubber'
3
+
4
+ module Shokkenki
5
+ module Consumer
6
+ module Configuration
7
+ module Session
8
+
9
+ attr_reader :stubber_classes
10
+
11
+ def configure &block
12
+ instance_eval &block if block
13
+ end
14
+
15
+ def tickets location
16
+ self.ticket_location = location
17
+ end
18
+
19
+ def stubber_classes
20
+ @stubber_classes ||= { :local_server => Shokkenki::Consumer::Stubber::HttpStubber }
21
+ end
22
+
23
+ def register_stubber name, clazz
24
+ stubber_classes[name] = clazz
25
+ end
26
+
27
+ def define_provider name, &block
28
+ provider_config = ProviderConfiguration.new(
29
+ name,
30
+ stubber_classes
31
+ )
32
+ provider_config.instance_eval &block if block
33
+ add_provider provider_config.to_provider
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,9 @@
1
+ require_relative 'session'
2
+
3
+ module Shokkenki
4
+
5
+ def self.consumer
6
+ @consumer_session ||= Shokkenki::Consumer::Session.new
7
+ end
8
+
9
+ end
@@ -0,0 +1,16 @@
1
+ module Shokkenki
2
+ module Consumer
3
+ module DSL
4
+ module HttpMethods
5
+
6
+ [:get, :put, :post, :delete, :options, :head].each do |meth|
7
+
8
+ define_method meth do |path, details={}|
9
+ receive({:path => path, :method => meth}.merge(details))
10
+ end
11
+ end
12
+
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,70 @@
1
+ require 'shokkenki/term/core_ext'
2
+ require_relative '../model/interaction'
3
+ require_relative '../model/fixture'
4
+ require_relative 'http_methods'
5
+ require_relative 'term_dsl'
6
+
7
+ module Shokkenki
8
+ module Consumer
9
+ module DSL
10
+ class Order
11
+ include HttpMethods
12
+ include TermDSL
13
+
14
+ def initialize patronage
15
+ @patronage = patronage
16
+ @fixtures = []
17
+ end
18
+
19
+ def during label
20
+ @interaction_label = label
21
+ self
22
+ end
23
+
24
+ def receive details
25
+ raise "No request method has been specified." unless details.has_key?(:method)
26
+ raise "The request method must be a symbol." unless @request_details = details[:method].is_a?(Symbol)
27
+
28
+ raise "No request path has been specified." unless details.has_key?(:path)
29
+ raise "The request path must be a string." unless details[:path].is_a?(String)
30
+
31
+ @request_details = details
32
+ self
33
+ end
34
+
35
+ def and_respond details
36
+ @response_details = details
37
+ self
38
+ end
39
+
40
+ def given name, arguments=nil
41
+ @fixtures << Shokkenki::Consumer::Model::Fixture.new(
42
+ :name => name,
43
+ :arguments => arguments
44
+ )
45
+ self
46
+ end
47
+
48
+ def validate!
49
+ raise "No request has been specified." unless @request_details
50
+ raise "No response has been specified." unless @response_details
51
+ end
52
+
53
+ def to_interaction
54
+ Shokkenki::Consumer::Model::Interaction.new(
55
+ :label => @interaction_label,
56
+ :request => @request_details.to_shokkenki_term,
57
+ :response => @response_details.to_shokkenki_term,
58
+ :fixtures => @fixtures
59
+ )
60
+ end
61
+
62
+ def to &block
63
+ instance_eval &block
64
+ validate!
65
+ @patronage.add_interaction to_interaction
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,13 @@
1
+ require_relative 'order'
2
+
3
+ module Shokkenki
4
+ module Consumer
5
+ module DSL
6
+ module Session
7
+ def order provider_name
8
+ order = Order.new current_patronage_for(provider_name)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ require 'shokkenki/term/json_paths_term'
2
+
3
+ module Shokkenki
4
+ module Consumer
5
+ module DSL
6
+ module TermDSL
7
+ def json paths
8
+ Shokkenki::Term::JsonPathsTerm.new paths
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ module Shokkenki
2
+ module Consumer
3
+ module Model
4
+ class Fixture
5
+
6
+ attr_reader :name, :arguments
7
+
8
+ def initialize attributes
9
+ @name = attributes[:name]
10
+ @arguments = attributes[:arguments]
11
+ end
12
+
13
+ def to_hash
14
+ hash = {:name => @name}
15
+ hash.merge!(:arguments => @arguments) if @arguments
16
+ hash
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,29 @@
1
+ module Shokkenki
2
+ module Consumer
3
+ module Model
4
+ class Interaction
5
+
6
+ attr_reader :label, :request, :response, :time, :fixtures
7
+
8
+ def initialize attributes
9
+ @label = attributes[:label]
10
+ @request = attributes[:request]
11
+ @response = attributes[:response]
12
+ @fixtures = attributes[:fixtures]
13
+ @time = Time.now
14
+ end
15
+
16
+ def to_hash
17
+ hash = {
18
+ :request => @request.to_hash,
19
+ :response => @response.to_hash,
20
+ :time => @time.utc.iso8601
21
+ }
22
+ hash.merge!(:label => @label) if @label
23
+ hash.merge!(:fixtures => @fixtures.map{ |f| f.to_hash }) if @fixtures
24
+ hash
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,31 @@
1
+ require_relative 'ticket'
2
+
3
+ module Shokkenki
4
+ module Consumer
5
+ module Model
6
+ class Patronage
7
+
8
+ attr_reader :provider, :consumer, :interactions
9
+
10
+ def initialize attributes
11
+ @provider = attributes[:provider]
12
+ @consumer = attributes[:consumer]
13
+ @interactions = []
14
+ end
15
+
16
+ def add_interaction interaction
17
+ @interactions << interaction
18
+ @provider.stub_interaction interaction
19
+ end
20
+
21
+ def ticket
22
+ Ticket.new(
23
+ :consumer => @consumer,
24
+ :provider => @provider,
25
+ :interactions => @interactions
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,33 @@
1
+ require_relative 'role'
2
+ require_relative '../stubber/http_stubber'
3
+
4
+ module Shokkenki
5
+ module Consumer
6
+ module Model
7
+ class Provider < Role
8
+ extend Forwardable
9
+ def_delegators :@stubber, :stub_interaction, :clear_interaction_stubs,
10
+ :session_started, :session_closed
11
+
12
+ attr_reader :stubber
13
+
14
+ def initialize attributes
15
+ super attributes
16
+ @stubber = attributes[:stubber] || Shokkenki::Consumer::Stubber::HttpStubber.new({})
17
+ end
18
+
19
+ def assert_all_requests_matched!
20
+ unmatched_requests = @stubber.unmatched_requests
21
+ message = "In provider '#{@name}' the following requests were not matched: #{JSON.pretty_generate(unmatched_requests)}"
22
+ raise message unless unmatched_requests.empty?
23
+ end
24
+
25
+ def assert_all_interactions_used!
26
+ unused_interactions = @stubber.unused_interactions
27
+ message = "In provider '#{@name}' the following interactions were never used: #{JSON.pretty_generate(unused_interactions)}"
28
+ raise message unless unused_interactions.empty?
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,27 @@
1
+ require_relative 'simplification'
2
+ require 'active_support/core_ext/string/inflections'
3
+
4
+ module Shokkenki
5
+ module Consumer
6
+ module Model
7
+ class Role
8
+ include Simplification
9
+
10
+ attr_reader :name, :label
11
+
12
+ def initialize arguments
13
+ @name = simplify(arguments[:name])
14
+ @label = arguments[:label] || @name.to_s.titleize
15
+ end
16
+
17
+ def to_hash
18
+ {
19
+ :name => @name,
20
+ :label => @label
21
+ }
22
+ end
23
+
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,16 @@
1
+ module Shokkenki
2
+ module Consumer
3
+ module Model
4
+ module Simplification
5
+ def simplify name
6
+ name.to_s.
7
+ strip.
8
+ gsub(' ', '_').
9
+ gsub(/\W/, '').
10
+ downcase.
11
+ to_sym if name
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,38 @@
1
+ require_relative '../version'
2
+ require 'json'
3
+
4
+ module Shokkenki
5
+ module Consumer
6
+ module Model
7
+ class Ticket
8
+
9
+ attr_reader :provider, :consumer, :interactions
10
+ attr_accessor :time, :version
11
+
12
+ def initialize attributes
13
+ @provider = attributes[:provider]
14
+ @consumer = attributes[:consumer]
15
+ @interactions = attributes[:interactions]
16
+ @version = Shokkenki::Consumer::Version::STRING
17
+ end
18
+
19
+ def filename
20
+ "#{@consumer.name}-#{@provider.name}.json"
21
+ end
22
+
23
+ def to_hash
24
+ {
25
+ :consumer => @consumer.to_hash,
26
+ :provider => @provider.to_hash,
27
+ :interactions => @interactions.collect(&:to_hash),
28
+ :version => @version
29
+ }
30
+ end
31
+
32
+ def to_json
33
+ JSON.pretty_generate to_hash
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,17 @@
1
+ require_relative '../consumer'
2
+
3
+ module Shokkenki
4
+ module Consumer
5
+ module RSpec
6
+ module ExampleGroupBinding
7
+ def shokkenki
8
+ Shokkenki.consumer
9
+ end
10
+
11
+ def order provider
12
+ shokkenki.order provider
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,32 @@
1
+ require_relative '../consumer'
2
+ require_relative '../model/role'
3
+
4
+ module Shokkenki
5
+ module Consumer
6
+ module RSpec
7
+ class Hooks
8
+
9
+ def self.before_suite
10
+ Shokkenki.consumer.start
11
+ end
12
+
13
+ def self.before_each name
14
+ Shokkenki.consumer.add_consumer(Shokkenki::Consumer::Model::Role.new(:name => name)) unless Shokkenki.consumer.consumer(name)
15
+ Shokkenki.consumer.set_current_consumer name
16
+ Shokkenki.consumer.clear_interaction_stubs
17
+ end
18
+
19
+ def self.after_each
20
+ Shokkenki.consumer.assert_all_requests_matched!
21
+ Shokkenki.consumer.assert_all_interactions_used!
22
+ end
23
+
24
+ def self.after_suite
25
+ Shokkenki.consumer.print_tickets
26
+ Shokkenki.consumer.close
27
+ end
28
+
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,25 @@
1
+ require_relative 'example_group_binding'
2
+ require_relative 'hooks'
3
+ require 'rspec'
4
+
5
+ RSpec.configure do |config|
6
+ config.treat_symbols_as_metadata_keys_with_true_values = true
7
+ config.include(
8
+ Shokkenki::Consumer::RSpec::ExampleGroupBinding,
9
+ :shokkenki_consumer => lambda{ |x| true }
10
+ )
11
+
12
+ config.before(:suite) { Shokkenki::Consumer::RSpec::Hooks.before_suite }
13
+
14
+ shokkenki_consumer_examples = { :shokkenki_consumer => lambda{ |x| true } }
15
+
16
+ config.before(:each, shokkenki_consumer_examples) do |example_group|
17
+ Shokkenki::Consumer::RSpec::Hooks.before_each example_group.example.metadata[:shokkenki_consumer]
18
+ end
19
+
20
+ config.after(:each, shokkenki_consumer_examples) do
21
+ Shokkenki::Consumer::RSpec::Hooks.after_each
22
+ end
23
+
24
+ config.after(:suite) { Shokkenki::Consumer::RSpec::Hooks.after_suite }
25
+ end
@@ -0,0 +1,2 @@
1
+ require_relative 'consumer'
2
+ require_relative 'rspec/rspec_configuration'
@@ -0,0 +1,83 @@
1
+ require_relative 'model/patronage'
2
+ require_relative 'model/simplification'
3
+ require_relative 'dsl/session'
4
+ require_relative 'configuration/session'
5
+
6
+ module Shokkenki
7
+ module Consumer
8
+ class Session
9
+ include Shokkenki::Consumer::Model::Simplification
10
+ include Shokkenki::Consumer::DSL::Session
11
+ include Shokkenki::Consumer::Configuration::Session
12
+
13
+ attr_reader :current_consumer, :patronages, :configuration, :providers, :consumers
14
+ attr_accessor :ticket_location
15
+
16
+ def initialize
17
+ @consumers = {}
18
+ @providers = {}
19
+ @patronages = {}
20
+ @ticket_location = 'tickets'
21
+ end
22
+
23
+ def current_patronage_for provider_name
24
+ consumer = @current_consumer
25
+ provider = provider(provider_name) || raise("The provider '#{provider_name}' is not recognised. Have you defined it?")
26
+ key = { consumer => provider }
27
+ @patronages[key] ||= Shokkenki::Consumer::Model::Patronage.new :consumer => consumer, :provider => provider
28
+ end
29
+
30
+ def add_provider provider
31
+ @providers[simplify(provider.name)] = provider
32
+ end
33
+
34
+ def add_provider provider
35
+ @providers[simplify(provider.name)] = provider
36
+ end
37
+
38
+ def add_consumer consumer
39
+ @consumers[simplify(consumer.name)] = consumer
40
+ end
41
+
42
+ def provider name
43
+ @providers[simplify(name)]
44
+ end
45
+
46
+ def consumer name
47
+ @consumers[simplify(name)]
48
+ end
49
+
50
+ def set_current_consumer name
51
+ @current_consumer = consumer name
52
+ end
53
+
54
+ def clear_interaction_stubs
55
+ @providers.values.each { |p| p.clear_interaction_stubs }
56
+ end
57
+
58
+ def start
59
+ @providers.values.each { |p| p.session_started }
60
+ end
61
+
62
+ def close
63
+ @providers.values.each { |p| p.session_closed }
64
+ end
65
+
66
+ def print_tickets
67
+ @patronages.values.collect(&:ticket).each do |ticket|
68
+ ticket_path = File.expand_path(File.join(ticket_location, ticket.filename))
69
+ File.open(ticket_path, 'w') { |file| file.write(ticket.to_json) }
70
+ end
71
+ end
72
+
73
+ def assert_all_requests_matched!
74
+ @providers.values.each { |p| p.assert_all_requests_matched! }
75
+ end
76
+
77
+ def assert_all_interactions_used!
78
+ @providers.values.each { |p| p.assert_all_interactions_used! }
79
+ end
80
+
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,97 @@
1
+ require_relative 'server'
2
+ require_relative 'stub_server_middleware'
3
+ require 'httparty'
4
+ require 'uri'
5
+ require 'find_a_port'
6
+
7
+ module Shokkenki
8
+ module Consumer
9
+ module Stubber
10
+ class HttpStubber
11
+
12
+ attr_reader :port, :host, :scheme, :interactions_path, :server
13
+
14
+ def initialize attributes
15
+ @port = attributes[:port]
16
+ @scheme = attributes[:scheme] || :http
17
+ @host = attributes[:host] || 'localhost'
18
+ @interactions_path = attributes[:interactions_path] || '/shokkenki/interactions'
19
+ @unmatched_requests_path = '/shokkenki/requests/unmatched'
20
+ @unused_interactions_path = '/shokkenki/interactions/unused'
21
+ end
22
+
23
+ def stub_interaction interaction
24
+ response = HTTParty.post(interactions_uri,
25
+ :body => interaction.to_hash.to_json,
26
+ :headers => { 'Content-Type' => 'application/json' }
27
+ )
28
+ server.assert_ok!
29
+ raise "Failed to stub interaction: #{response.inspect}" unless successful?(response.code)
30
+ end
31
+
32
+ def clear_interaction_stubs
33
+ response = HTTParty.delete interactions_uri
34
+ server.assert_ok!
35
+ raise "Failed to clear interaction stubs: #{response.inspect}" unless successful?(response.code)
36
+ end
37
+
38
+ def successful? response_code
39
+ (200 <= response_code) && (response_code < 300)
40
+ end
41
+
42
+ def session_started
43
+ # Find a port as late as possible to minimise the
44
+ # chance that it starts being used in between finding it
45
+ # and using it.
46
+ @port = FindAPort.available_port unless @port
47
+
48
+ @server = Server.new(
49
+ :app => StubServerMiddleware.new,
50
+ :host => @host,
51
+ :port => @port
52
+ )
53
+ @server.start
54
+ end
55
+
56
+ def session_closed
57
+ @server.shutdown
58
+ end
59
+
60
+ def unmatched_requests
61
+ response = HTTParty.get unmatched_requests_uri
62
+ server.assert_ok!
63
+ raise "Failed to find unmatched requests: #{response.inspect}" unless successful?(response.code)
64
+ JSON.parse response.body
65
+ end
66
+
67
+ def unused_interactions
68
+ response = HTTParty.get unused_interactions_uri
69
+ server.assert_ok!
70
+ raise "Failed to find unused interactions: #{response.inspect}" unless successful?(response.code)
71
+ JSON.parse response.body
72
+ end
73
+
74
+ def server_properties
75
+ {
76
+ :scheme => @scheme.to_s,
77
+ :host => @host.to_s,
78
+ :port => @port
79
+ }
80
+ end
81
+
82
+ def interactions_uri
83
+ URI::Generic.build(server_properties.merge(:path => @interactions_path.to_s)).to_s
84
+ end
85
+
86
+ def unmatched_requests_uri
87
+ URI::Generic.build(server_properties.merge(:path => @unmatched_requests_path.to_s)).to_s
88
+ end
89
+
90
+ def unused_interactions_uri
91
+ URI::Generic.build(server_properties.merge(:path => @unused_interactions_path.to_s)).to_s
92
+ end
93
+
94
+ end
95
+ end
96
+ end
97
+ end