pacto 0.3.0.pre → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (111) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.rubocop-todo.yml +0 -27
  4. data/.rubocop.yml +9 -0
  5. data/.ruby-gemset +1 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +4 -5
  8. data/CONTRIBUTING.md +112 -0
  9. data/Gemfile +5 -0
  10. data/Guardfile +18 -13
  11. data/README.md +157 -101
  12. data/Rakefile +3 -3
  13. data/features/configuration/strict_matchers.feature +97 -0
  14. data/features/evolve/README.md +11 -0
  15. data/features/evolve/existing_services.feature +82 -0
  16. data/features/generate/README.md +5 -0
  17. data/features/generate/generation.feature +28 -0
  18. data/features/steps/pacto_steps.rb +75 -0
  19. data/features/stub/README.md +2 -0
  20. data/features/stub/templates.feature +46 -0
  21. data/features/support/env.rb +11 -5
  22. data/features/validate/README.md +1 -0
  23. data/features/validate/body_only.feature +85 -0
  24. data/features/{journeys/validation.feature → validate/meta_validation.feature} +41 -24
  25. data/features/validate/validation.feature +36 -0
  26. data/lib/pacto.rb +61 -33
  27. data/lib/pacto/contract.rb +18 -15
  28. data/lib/pacto/contract_factory.rb +14 -11
  29. data/lib/pacto/contract_files.rb +17 -0
  30. data/lib/pacto/contract_list.rb +17 -0
  31. data/lib/pacto/contract_validator.rb +29 -0
  32. data/lib/pacto/core/configuration.rb +19 -17
  33. data/lib/pacto/core/contract_registry.rb +43 -0
  34. data/lib/pacto/core/{callback.rb → hook.rb} +3 -3
  35. data/lib/pacto/core/modes.rb +33 -0
  36. data/lib/pacto/core/validation_registry.rb +45 -0
  37. data/lib/pacto/erb_processor.rb +0 -1
  38. data/lib/pacto/extensions.rb +18 -4
  39. data/lib/pacto/generator.rb +34 -49
  40. data/lib/pacto/generator/filters.rb +41 -0
  41. data/lib/pacto/hooks/erb_hook.rb +4 -3
  42. data/lib/pacto/logger.rb +4 -2
  43. data/lib/pacto/meta_schema.rb +4 -2
  44. data/lib/pacto/rake_task.rb +28 -25
  45. data/lib/pacto/request_clause.rb +43 -0
  46. data/lib/pacto/request_pattern.rb +8 -0
  47. data/lib/pacto/response_clause.rb +15 -0
  48. data/lib/pacto/rspec.rb +102 -0
  49. data/lib/pacto/stubs/uri_pattern.rb +23 -0
  50. data/lib/pacto/stubs/webmock_adapter.rb +69 -0
  51. data/lib/pacto/stubs/webmock_helper.rb +71 -0
  52. data/lib/pacto/ui.rb +7 -0
  53. data/lib/pacto/uri.rb +9 -0
  54. data/lib/pacto/validation.rb +57 -0
  55. data/lib/pacto/validators/body_validator.rb +41 -0
  56. data/lib/pacto/validators/request_body_validator.rb +23 -0
  57. data/lib/pacto/validators/response_body_validator.rb +23 -0
  58. data/lib/pacto/validators/response_header_validator.rb +49 -0
  59. data/lib/pacto/validators/response_status_validator.rb +24 -0
  60. data/lib/pacto/version.rb +1 -1
  61. data/pacto.gemspec +33 -29
  62. data/resources/contract_schema.json +8 -176
  63. data/resources/draft-03.json +174 -0
  64. data/spec/integration/data/strict_contract.json +2 -2
  65. data/spec/integration/e2e_spec.rb +22 -31
  66. data/spec/integration/rspec_spec.rb +94 -0
  67. data/spec/integration/templating_spec.rb +9 -12
  68. data/{lib → spec}/pacto/server.rb +0 -0
  69. data/{lib → spec}/pacto/server/dummy.rb +11 -8
  70. data/{lib → spec}/pacto/server/playback_servlet.rb +1 -1
  71. data/spec/spec_helper.rb +2 -0
  72. data/spec/unit/hooks/erb_hook_spec.rb +15 -15
  73. data/spec/unit/pacto/configuration_spec.rb +2 -10
  74. data/spec/unit/pacto/contract_factory_spec.rb +16 -13
  75. data/spec/unit/pacto/contract_files_spec.rb +42 -0
  76. data/spec/unit/pacto/contract_list_spec.rb +35 -0
  77. data/spec/unit/pacto/contract_spec.rb +43 -44
  78. data/spec/unit/pacto/contract_validator_spec.rb +85 -0
  79. data/spec/unit/pacto/core/configuration_spec.rb +4 -11
  80. data/spec/unit/pacto/core/contract_registry_spec.rb +119 -0
  81. data/spec/unit/pacto/core/modes_spec.rb +18 -0
  82. data/spec/unit/pacto/core/validation_registry_spec.rb +76 -0
  83. data/spec/unit/pacto/core/validation_spec.rb +60 -0
  84. data/spec/unit/pacto/extensions_spec.rb +14 -23
  85. data/spec/unit/pacto/generator/filters_spec.rb +99 -0
  86. data/spec/unit/pacto/generator_spec.rb +34 -73
  87. data/spec/unit/pacto/meta_schema_spec.rb +46 -6
  88. data/spec/unit/pacto/pacto_spec.rb +17 -15
  89. data/spec/unit/pacto/{request_spec.rb → request_clause_spec.rb} +32 -44
  90. data/spec/unit/pacto/request_pattern_spec.rb +22 -0
  91. data/spec/unit/pacto/response_clause_spec.rb +54 -0
  92. data/spec/unit/pacto/stubs/uri_pattern_spec.rb +28 -0
  93. data/spec/unit/pacto/stubs/webmock_adapter_spec.rb +205 -0
  94. data/spec/unit/pacto/stubs/webmock_helper_spec.rb +20 -0
  95. data/spec/unit/pacto/uri_spec.rb +20 -0
  96. data/spec/unit/pacto/validators/body_validator_spec.rb +105 -0
  97. data/spec/unit/pacto/validators/response_header_validator_spec.rb +94 -0
  98. data/spec/unit/pacto/validators/response_status_validator_spec.rb +20 -0
  99. metadata +230 -146
  100. data/features/generation/generation.feature +0 -25
  101. data/lib/pacto/core/contract_repository.rb +0 -44
  102. data/lib/pacto/hash_merge_processor.rb +0 -14
  103. data/lib/pacto/request.rb +0 -57
  104. data/lib/pacto/response.rb +0 -63
  105. data/lib/pacto/response_adapter.rb +0 -24
  106. data/lib/pacto/stubs/built_in.rb +0 -57
  107. data/spec/unit/pacto/core/contract_repository_spec.rb +0 -133
  108. data/spec/unit/pacto/hash_merge_processor_spec.rb +0 -20
  109. data/spec/unit/pacto/response_adapter_spec.rb +0 -25
  110. data/spec/unit/pacto/response_spec.rb +0 -201
  111. data/spec/unit/pacto/stubs/built_in_spec.rb +0 -168
@@ -0,0 +1,43 @@
1
+ module Pacto
2
+ class ContractRegistry
3
+ def initialize
4
+ @registry = Hash.new { |hash, key| hash[key] = Set.new }
5
+ end
6
+
7
+ def [](tag)
8
+ @registry[tag]
9
+ end
10
+
11
+ def register(contract, *tags)
12
+ tags << :default if tags.empty?
13
+
14
+ tags.each do |tag|
15
+ @registry[tag] << contract
16
+ end
17
+
18
+ self
19
+ end
20
+
21
+ def use(tag, values = {})
22
+ merged_contracts = @registry[:default] + @registry[tag]
23
+
24
+ fail ArgumentError, "contract \"#{tag}\" not found" if merged_contracts.empty?
25
+
26
+ merged_contracts.each do |contract|
27
+ contract.stub_contract! values
28
+ end
29
+
30
+ self
31
+ end
32
+
33
+ def contracts_for(request_signature)
34
+ all_contracts.select { |c| c.matches? request_signature }
35
+ end
36
+
37
+ private
38
+
39
+ def all_contracts
40
+ @registry.values.inject(Set.new, :+)
41
+ end
42
+ end
43
+ end
@@ -1,11 +1,11 @@
1
1
  module Pacto
2
- class Callback
2
+ class Hook
3
3
  def initialize(&block)
4
- @callback = block
4
+ @hook = block
5
5
  end
6
6
 
7
7
  def process(contracts, request_signature, response)
8
- @callback.call contracts, request_signature, response
8
+ @hook.call contracts, request_signature, response
9
9
  end
10
10
  end
11
11
  end
@@ -0,0 +1,33 @@
1
+ module Pacto
2
+ class << self
3
+ def generate!
4
+ modes << :generate
5
+ end
6
+
7
+ def stop_generating!
8
+ modes.delete :generate
9
+ end
10
+
11
+ def generating?
12
+ modes.include? :generate
13
+ end
14
+
15
+ def validate!
16
+ modes << :validate
17
+ end
18
+
19
+ def stop_validating!
20
+ modes.delete :validate
21
+ end
22
+
23
+ def validating?
24
+ modes.include? :validate
25
+ end
26
+
27
+ private
28
+
29
+ def modes
30
+ @modes ||= []
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,45 @@
1
+ module Pacto
2
+ class ValidationRegistry
3
+ include Singleton
4
+ attr_reader :validations
5
+
6
+ def initialize
7
+ @validations = []
8
+ end
9
+
10
+ def reset!
11
+ @validations.clear
12
+ end
13
+
14
+ def validated?(request_pattern)
15
+ matched_validations = @validations.select do |validation|
16
+ request_pattern.matches? validation.request
17
+ end
18
+ matched_validations unless matched_validations.empty?
19
+ end
20
+
21
+ def register_validation(validation)
22
+ @validations << validation
23
+ logger.info "Detected #{validation.summary}"
24
+ validation
25
+ end
26
+
27
+ def unmatched_validations
28
+ @validations.select do |validation|
29
+ validation.contract.nil?
30
+ end
31
+ end
32
+
33
+ def failed_validations
34
+ @validations.select do |validation|
35
+ !validation.successful?
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def logger
42
+ @logger ||= Logger.instance
43
+ end
44
+ end
45
+ end
@@ -1,7 +1,6 @@
1
1
  module Pacto
2
2
  class ERBProcessor
3
3
  def process(contract, values = {})
4
- values ||= {}
5
4
  erb = ERB.new(contract)
6
5
  erb_result = erb.result hash_binding(values)
7
6
  Logger.instance.debug "Processed contract: #{erb_result.inspect}"
@@ -1,12 +1,25 @@
1
1
  module Pacto
2
2
  module Extensions
3
- module HashSubsetOf
4
- def subset_of?(other)
5
- (to_a - other.to_a).empty?
3
+ # Adapted from Faraday
4
+ HeaderKeyMap = Hash.new do |map, key|
5
+ split_char = key.to_s.include?('-') ? '-' : '_'
6
+ map[key] = key.to_s.split(split_char). # :user_agent => %w(user agent)
7
+ each { |w| w.capitalize! }. # => %w(User Agent)
8
+ join('-') # => "User-Agent"
9
+ end
10
+ HeaderKeyMap[:etag] = 'ETag'
11
+
12
+ def self.normalize_header_keys(headers)
13
+ headers.reduce({}) do |normalized, (key, value)|
14
+ normalized[HeaderKeyMap[key]] = value
15
+ normalized
6
16
  end
17
+ end
7
18
 
19
+ module HashSubsetOf
20
+ # FIXME: Only used by HashMergeProcessor, which I'd like to deprecate
8
21
  def normalize_keys
9
- inject({}) do |normalized, (key, value)|
22
+ reduce({}) do |normalized, (key, value)|
10
23
  normalized[key.to_s.downcase] = value
11
24
  normalized
12
25
  end
@@ -15,4 +28,5 @@ module Pacto
15
28
  end
16
29
  end
17
30
 
31
+ # FIXME: Let's not extend Hash...
18
32
  Hash.send(:include, Pacto::Extensions::HashSubsetOf)
@@ -2,73 +2,58 @@ require 'json/schema_generator'
2
2
 
3
3
  module Pacto
4
4
  class Generator
5
- attr_accessor :request_headers_to_filter
6
- attr_accessor :response_headers_to_filter
7
-
8
- INFORMATIONAL_REQUEST_HEADERS =
9
- %w{
10
- content-length
11
- via
12
- }
13
-
14
- INFORMATIONAL_RESPONSE_HEADERS =
15
- %w{
16
- server
17
- date
18
- content-length
19
- connection
20
- }
21
-
22
5
  def initialize(schema_version = 'draft3',
23
6
  schema_generator = JSON::SchemaGenerator,
24
- validator = Pacto::MetaSchema.new)
7
+ validator = Pacto::MetaSchema.new,
8
+ generator_options = Pacto.configuration.generator_options,
9
+ filters = Pacto::Generator::Filters.new)
25
10
  @schema_version = schema_version
26
11
  @validator = validator
27
12
  @schema_generator = schema_generator
28
- @response_headers_to_filter = INFORMATIONAL_RESPONSE_HEADERS
29
- @request_headers_to_filter = INFORMATIONAL_REQUEST_HEADERS
30
- end
31
-
32
- def generate(request_file, host)
33
- contract = Pacto.build_from_file request_file, host
34
- request = contract.request
35
- response = request.execute
36
- save(request_file, request, response)
13
+ @generator_options = generator_options
14
+ @filters = filters
37
15
  end
38
16
 
39
17
  def save(source, request, response)
40
- body_schema = JSON::SchemaGenerator.generate source, response.body, @schema_version
41
- contract = {
42
- :request => {
43
- :headers => filter_request_headers(request.headers),
44
- :method => request.method,
45
- :params => request.params,
46
- :path => request.path
47
- },
48
- :response => {
49
- :headers => filter_response_headers(response.headers),
50
- :status => response.status,
51
- :body => MultiJson.load(body_schema)
52
- }
53
- }
18
+ contract = generate_contract source, request, response
54
19
  pretty_contract = MultiJson.encode(contract, :pretty => true)
55
20
  # This is because of a discrepency w/ jruby vs MRI pretty json
56
- pretty_contract.gsub! /^$\n/, ''
21
+ pretty_contract.gsub!(/^$\n/, '')
57
22
  @validator.validate pretty_contract
58
23
  pretty_contract
59
24
  end
60
25
 
61
26
  private
62
27
 
63
- def filter_request_headers headers
64
- headers.reject do |header|
65
- @request_headers_to_filter.include? header.downcase
66
- end
28
+ def generate_contract(source, request, response)
29
+ {
30
+ :request => generate_request(request, response, source),
31
+ :response => generate_response(request, response, source)
32
+ }
33
+ end
34
+
35
+ def generate_request(request, response, source)
36
+ {
37
+ :headers => @filters.filter_request_headers(request, response),
38
+ :method => request.method,
39
+ :params => request.params,
40
+ :path => request.path,
41
+ :body => generate_body(source, request.body)
42
+ }.delete_if { |k, v| v.nil? }
43
+ end
44
+
45
+ def generate_response(request, response, source)
46
+ {
47
+ :headers => @filters.filter_response_headers(request, response),
48
+ :status => response.status,
49
+ :body => generate_body(source, response.body)
50
+ }.delete_if { |k, v| v.nil? }
67
51
  end
68
52
 
69
- def filter_response_headers headers
70
- headers.reject do |header|
71
- @response_headers_to_filter.include? header.downcase
53
+ def generate_body(source, body)
54
+ if body && !body.empty?
55
+ body_schema = JSON::SchemaGenerator.generate source, body, @generator_options
56
+ MultiJson.load(body_schema)
72
57
  end
73
58
  end
74
59
  end
@@ -0,0 +1,41 @@
1
+ module Pacto
2
+ class Generator
3
+ class Filters
4
+ CONNECTION_CONTROL_HEADERS = %w{
5
+ Via
6
+ Server
7
+ Connection
8
+ Transfer-Encoding
9
+ Content-Length
10
+ }
11
+
12
+ FRESHNESS_HEADERS =
13
+ %w{
14
+ Date
15
+ Last-Modified
16
+ ETag
17
+ }
18
+
19
+ HEADERS_TO_FILTER = CONNECTION_CONTROL_HEADERS + FRESHNESS_HEADERS
20
+
21
+ def filter_request_headers(request, response)
22
+ # FIXME: Do we need to handle all these cases in real situations, or just because of stubbing?
23
+ vary_headers = response.headers['Vary'] || []
24
+ vary_headers = [vary_headers] if vary_headers.is_a? String
25
+ vary_headers = vary_headers.map do |h|
26
+ h.split(',').map(&:strip)
27
+ end.flatten
28
+
29
+ request.headers.select do |header|
30
+ vary_headers.map(&:downcase).include? header.downcase
31
+ end
32
+ end
33
+
34
+ def filter_response_headers(request, response)
35
+ Pacto::Extensions.normalize_header_keys(response.headers).reject do |header|
36
+ (HEADERS_TO_FILTER.include? header) || (header.start_with?('X-'))
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,17 +1,18 @@
1
+ require_relative '../erb_processor'
2
+
1
3
  module Pacto
2
4
  module Hooks
3
- class ERBHook < Pacto::Callback
5
+ class ERBHook < Pacto::Hook
4
6
  def initialize
5
7
  @processor = ERBProcessor.new
6
8
  end
7
9
 
8
10
  def process(contracts, request_signature, response)
9
11
  bound_values = contracts.empty? ? {} : contracts.first.values
10
- bound_values.merge!({:req => { 'HEADERS' => request_signature.headers}})
12
+ bound_values.merge!(:req => { 'HEADERS' => request_signature.headers})
11
13
  response.body = @processor.process response.body, bound_values
12
14
  response.body
13
15
  end
14
-
15
16
  end
16
17
  end
17
18
  end
@@ -1,3 +1,5 @@
1
+ require 'forwardable'
2
+
1
3
  module Pacto
2
4
  class Logger
3
5
  include Singleton
@@ -9,13 +11,13 @@ module Pacto
9
11
  log ::Logger.new STDOUT
10
12
  end
11
13
 
12
- def log log
14
+ def log(log)
13
15
  @log = log
14
16
  @log.level = default_level
15
17
  @log.progname = 'Pacto'
16
18
  end
17
19
 
18
- def level= level
20
+ def level=(level)
19
21
  @log.level = log_levels.fetch(level, default_level)
20
22
  end
21
23
 
@@ -4,13 +4,15 @@ module Pacto
4
4
 
5
5
  def initialize(engine = JSON::Validator)
6
6
  @schema = File.join(File.dirname(File.expand_path(__FILE__)), '../../resources/contract_schema.json')
7
+ @base_schema = File.join(File.dirname(File.expand_path(__FILE__)), '../../resources/draft-03.json')
8
+ JSON::Validator.validate!(@base_schema, @schema)
7
9
  @engine = engine
8
10
  end
9
11
 
10
- def validate definition
12
+ def validate(definition)
11
13
  errors = engine.fully_validate(schema, definition, :version => :draft3)
12
14
  unless errors.empty?
13
- raise InvalidContract.new(errors)
15
+ fail InvalidContract, errors
14
16
  end
15
17
  end
16
18
  end
@@ -1,13 +1,7 @@
1
1
  require 'pacto'
2
2
 
3
- unless String.respond_to?(:colors)
4
- class String
5
- def colorize(*args)
6
- self
7
- end
8
- end
9
- end
10
-
3
+ # FIXME: RakeTask is a huge class, refactor this please
4
+ # rubocop:disable ClassLength
11
5
  module Pacto
12
6
  class RakeTask
13
7
  include Rake::DSL
@@ -29,7 +23,7 @@ module Pacto
29
23
  desc 'Validates all contracts in a given directory against a given host'
30
24
  task :validate, :host, :dir do |t, args|
31
25
  if args.to_a.size < 2
32
- fail 'USAGE: rake pacto:validate[<host>, <contract_dir>]'.colorize(:yellow)
26
+ fail Pacto::UI.yellow('USAGE: rake pacto:validate[<host>, <contract_dir>]')
33
27
  end
34
28
 
35
29
  validate_contracts(args[:host], args[:dir])
@@ -40,24 +34,26 @@ module Pacto
40
34
  desc 'Generates contracts from partial contracts'
41
35
  task :generate, :input_dir, :output_dir, :host do |t, args|
42
36
  if args.to_a.size < 3
43
- fail 'USAGE: rake pacto:generate[<request_contract_dir>, <output_dir>, <record_host>]'.colorize(:yellow)
37
+ fail Pacto::UI.yellow('USAGE: rake pacto:generate[<request_contract_dir>, <output_dir>, <record_host>]')
44
38
  end
45
39
 
46
40
  generate_contracts(args[:input_dir], args[:output_dir], args[:host])
47
41
  end
48
42
  end
49
43
 
44
+ # FIXME: meta_validate is a big method =(. Needs refactoring
45
+ # rubocop:disable MethodLength
50
46
  def meta_validate
51
47
  desc 'Validates a directory of contract definitions'
52
48
  task :meta_validate, :dir do |t, args|
53
49
  if args.to_a.size < 1
54
- fail 'USAGE: rake pacto:meta_validate[<contract_dir>]'.colorize(:yellow)
50
+ fail Pacto::UI.yellow('USAGE: rake pacto:meta_validate[<contract_dir>]')
55
51
  end
56
52
 
57
53
  each_contract(args[:dir]) do |contract_file|
58
- puts "Validating #{contract_file}"
59
54
  fail unless Pacto.validate_contract contract_file
60
55
  end
56
+ puts 'All contracts successfully meta-validated'
61
57
  end
62
58
  end
63
59
 
@@ -66,31 +62,36 @@ module Pacto
66
62
  puts "Validating contracts in directory #{dir} against host #{host}\n\n"
67
63
 
68
64
  total_failed = 0
69
- each_contract(dir) do |contact_file|
65
+ contracts = []
66
+ each_contract(dir) do |contract_file|
67
+ contracts << contract_file
70
68
  print "#{contract_file.split('/').last}:"
71
- contract = Pacto.build_from_file(contract_file, host)
72
- errors = contract.validate
69
+ contract = Pacto.load_contract(contract_file, host)
70
+ errors = contract.validate_provider
73
71
 
74
72
  if errors.empty?
75
- puts ' OK!'.colorize(:green)
73
+ puts Pacto::UI.green(' OK!')
76
74
  else
77
75
  @exit_with_error = true
78
76
  total_failed += 1
79
- puts ' FAILED!'.colorize(:red)
77
+ puts Pacto::UI.red(' FAILED!')
80
78
  errors.each do |error|
81
- puts "\t* #{error}".colorize(:light_red)
79
+ puts Pacto::UI.red("\t* #{error}")
82
80
  end
83
81
  puts ''
84
82
  end
85
83
  end
86
84
 
87
85
  if @exit_with_error
88
- fail "#{total_failed} of #{contracts.size} failed. Check output for detailed error messages.".colorize(:red)
86
+ fail Pacto::UI.red("#{total_failed} of #{contracts.size} failed. Check output for detailed error messages.")
89
87
  else
90
- puts "#{contracts.size} valid contract#{contracts.size > 1 ? 's' : nil}".colorize(:green)
88
+ puts Pacto::UI.green("#{contracts.size} valid contract#{contracts.size > 1 ? 's' : nil}")
91
89
  end
92
90
  end
91
+ # rubocop:enable MethodLength
93
92
 
93
+ # FIXME: generate_contracts is a big method =(. Needs refactoring
94
+ # rubocop:disable MethodLength
94
95
  def generate_contracts(input_dir, output_dir, host)
95
96
  WebMock.allow_net_connect!
96
97
  generator = Pacto::Generator.new
@@ -107,16 +108,17 @@ module Pacto
107
108
  output_file.close
108
109
  rescue InvalidContract => e
109
110
  failed_contracts << contract_file
110
- puts e.message.colorize(:red)
111
+ puts Pacto::UI.red(e.message)
111
112
  end
112
113
  end
113
114
 
114
115
  if failed_contracts.empty?
115
- puts 'Successfully generated all contracts'.colorize(:green)
116
+ puts Pacto::UI.green('Successfully generated all contracts')
116
117
  else
117
- fail "The following contracts could not be generated: #{failed_contracts.join ','}".colorize(:red)
118
+ fail Pacto::UI.red("The following contracts could not be generated: #{failed_contracts.join ','}")
118
119
  end
119
120
  end
121
+ # rubocop:enable MethodLength
120
122
 
121
123
  private
122
124
 
@@ -124,8 +126,8 @@ module Pacto
124
126
  if File.file? dir
125
127
  yield dir
126
128
  else
127
- contracts = Dir[File.join(dir, '*{.json.erb,.json}')]
128
- fail "No contracts found in directory #{dir}".colorize(:yellow) if contracts.empty?
129
+ contracts = Dir[File.join(dir, '**/*{.json.erb,.json}')]
130
+ fail Pacto::UI.yellow("No contracts found in directory #{dir}") if contracts.empty?
129
131
 
130
132
  contracts.sort.each do |contract_file|
131
133
  yield contract_file
@@ -134,5 +136,6 @@ module Pacto
134
136
  end
135
137
  end
136
138
  end
139
+ # rubocop:enable ClassLength
137
140
 
138
141
  Pacto::RakeTask.new.install