shokkenki-consumer 0.0.2

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 (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