Skip to content
This repository

Message type and status code #4

Merged
merged 18 commits into from almost 2 years ago

3 participants

proby Myron Marston Ryan LeCompte
proby
Owner
proby commented

No description provided.

README.md
((7 lines not shown))
97 102
 * The `versions` array lists the endpoint versions that should be associated with a
98 103
   particular schema definition.
  104
+* The `status_codes` is an optional array of status code strings describing for which
  105
+  status code or codes this schema applies to. `status_codes` is ignored if used with the
  106
+  `request` `message_type`. When used with the `response` `message_type`
  107
+  It is an optional attribute that when omitted defaults to all status codes. The attribute
  108
+  is ignored if part of a `request` `message_type`. Valid formats for a status code are either
1
Myron Marston Owner

This is quite a mouthful. Maybe it can be simplified to something like this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/interpol/endpoint.rb
@@ -27,15 +27,20 @@ def initialize(endpoint_hash)
27 27
       validate_name!
28 28
     end
29 29
 
30  
-    def find_definition!(version)
31  
-      @definitions.fetch(version) do
32  
-        message = "No definition found for #{name} endpoint for version #{version}"
33  
-        raise ArgumentError.new(message)
  30
+    def find_definition!(version, message_type)
  31
+      @definitions.each do |definition|
  32
+        if definition.version == version &&
  33
+            definition.message_type == message_type
  34
+          return definition
  35
+        end
34 36
       end
1
Myron Marston Owner

This was fetching an entry from a hash in O(1); now it's an O(N) linear scan over an array.

I imagine there's a reason for this change but it's not obvious to me...we should discuss on Monday.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/interpol/endpoint.rb
((9 lines not shown))
71 86
 
72 87
       fetch_from(endpoint_hash, 'definitions').each do |definition|
73 88
         fetch_from(definition, 'versions').each do |version|
74  
-          definitions[version] = EndpointDefinition.new(name, version, definition)
  89
+          message_type = definition['message_type'] || DEFAULT_MESSAGE_TYPE
1
Myron Marston Owner

This can be simplified a bit with Hash#fetch:

definitions.fetch('message_type', DEFAULT_MESSAGE_TYPE)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/interpol/endpoint.rb
((38 lines not shown))
  190
+        return nil if codes.nil?
  191
+        [].tap do |arr|
  192
+          codes.each do |code|
  193
+            arr << {:value => code, :type => code_type_for(code)}
  194
+          end
  195
+        end
  196
+      end
  197
+
  198
+      def code_type_for(code)
  199
+        # http://rubular.com/r/gvx8TztkRE - match either 3-digits or 1 digit then xx
  200
+        return :exact if code =~ /\d{3}/ # match 3 digits
  201
+        return :partial if code =~ /\dxx/ # match 1 digit then xx
  202
+        raise StatusCodeMatcherArgumentError, "#{code} is not a valid format"
  203
+      end
  204
+  end
  205
+
1
Myron Marston Owner

I think there's a simpler way to do this, in a way that's also more flexible. Rather than figuring out the type of match (exact vs partial), I think we can convert it to a regular expression, and rely on that to do the matching for us:

class StatusCodeMatcher
  attr_reader :code_strings

  def initialize(codes)
    codes = ["xxx"] if codes.none?
    @code_strings = codes
  end

  def code_regexes
    @code_regexes ||= code_strings.map do |string|
      /\A#{string.gsub('x', '\d')}\z/
    end
  end

  def matches?(status_code)
    code_regexes.any? { |re| re =~ status_code.to_s }
  end
end

A few things to note:

  • This is more flexible than what you currently have, as it supports things like 20x, x00, etc. I doubt these cases are particularly useful, but the consistency of being able to use "x" as a wild card anywhere is nice.
  • No need to differentiate between partial and exact matches...if you have a status code like "200" then it'll create a regex like /\A200\z/ which will only match exactly.
  • I'm a huge fan of the null object pattern (which helps you avoid unnecessary nil checks by putting a suitable null implementation in nil's place). I used that in this snippet, by setting the status code to "xxx" if none is given, which will automatically match everything w/o needing to special case it.
  • I didn't implement #to_codes here as it's not clear to me what that method is for just yet.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Myron Marston

The sentence Currently, partial status codes can only be a digit followed by two lower-case x's is no longer true, so it's probably worth updating this.

Myron Marston

I would consider code_regexes to be an implementation detail of how StatusCodeMatcher works, not part of the public interface. To that end, I'd recommend you make #code_regexes private, and not test it directly. Instead, your tests that exercise #matches? would exercise the code_regexes. #matches? is the primary behavior of StatusCodeMatcher that matters. Any bugs in regex construction would be exposed through matches?, anyway.

lib/interpol/endpoint.rb
@@ -127,6 +153,41 @@ def make_schema_strict!(raw_schema, modify_object=true)
127 153
     end
128 154
   end
129 155
 
  156
+  # Holds the acceptable status codes for an enpoint entry
  157
+  # Acceptable status code are either exact status codes (200, 404, etc)
  158
+  # or partial status codes (2xx, 3xx, 4xx, etc). Currently, partial status
  159
+  # codes can only be a digit followed by two lower-case x's.
  160
+  class StatusCodeMatcher
  161
+    attr_reader :code_strings
  162
+
  163
+    def initialize(codes)
  164
+      codes = ["xxx"] if codes.nil? || codes.none?
2
Myron Marston Owner

I'm fond of the Array method for coercing things to an array. Array(nil) returns [], which means this line could be codes = ["xxx"] if Array(code).none?.

What you have works fine, so there's no need to change to the Array version, but I wanted to mention in case you didn't know about #Array. It's a nice way to be able to confidently treat an object as an array without worrying about special casing nil.

One minor gotcha with using #none? (in case you didn't already know) is that it will indeed return true for [false, false, false, false]. In this case I'd probably just use #empty?, although it probably doesn't matter too much in this specific example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Myron Marston

Are you using matches_status_code? in multiple places? If so, it may make sense to move the status_code.nil? check in there--it seems like it should always be considered to match if there is no status code given.

Myron Marston

Ruby lets you use any object as a hash key...it uses the #hash method for that. For a case like this, where a key is a tuple, you can just use an array as the key because ruby defines #hash in terms of the entries in the array:

1.9.3-p194 :004 > hash = {}
 => {} 
1.9.3-p194 :005 > hash[['response', '1.0']] = "1.0 response"
 => "1.0 response" 
1.9.3-p194 :006 > hash[['response', '2.0']] = "2.0 response"
 => "2.0 response" 
1.9.3-p194 :007 > hash[['response', '3.0']]
 => nil 
1.9.3-p194 :008 > hash[['response', '1.0']]
 => "1.0 response" 

...so there's no need to convert it to a string here.

Myron Marston

This is a perfect use case for giving a hash a default block:

definitions = Hash.new { |h, k| h[k] = [] }
h[any_key] # => an array

...then you don't have to put nil handling here, as it's built in to the hash that it auto-vivifies missing keys as empty arrays.

Myron Marston

It's a bit confusing/weird that that each entry in endpoint.definitions is itself a definitions array. Maybe the variables/methods can be renamed for clarity? Or do they need to be so deeply nested?

Owner

It's something we/I should look at cleaning up. With the addition of status_code and message_type I've added one more level of nesting.

Myron Marston

It always makes be a bit nervous to set an expectation in an iterator, because if the array is empty, the example will pass even though you probably expect there to be at least one item in the array. If there's a failure, it's also nice to see the failure of the entire array rather than individual elements in the array, since seeing a common failure on multiple elements may give you a clue about why it failed. To that end, I would tend to do something like:

dfs = endpoint.definitions.first.
dfs.should have_at_least(1).item
dfs.reject { |d| d.schema.fetch("properties").has_key?("name") }.should eq([])

The 2nd line ensures we have some items in the array like we expect; the last line will give you a list of all of the items that fail the assertion (rather than just giving you the first failure).

Don't worry too much about this, though...what you have is totally fine :).

Myron Marston
Owner

Looks great, merge away.

Myron Marston
Owner

Actually, looks like the build is now failing. Feel free to relax the abc complexity threshold a bit if you need to to get this to pass. 16 isn't bad.

proby proby merged commit 301263d into from
proby proby closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
13  README.md
Source Rendered
@@ -51,7 +51,9 @@ name: user_projects
51 51
 route: /users/:user_id/projects
52 52
 method: GET
53 53
 definitions:
54  
-  - versions: ["1.0"]
  54
+  - message_type: response
  55
+    versions: ["1.0"]
  56
+    status_codes: ["2xx", "404"]
55 57
     schema:
56 58
       description: Returns a list of projects for the given user.
57 59
       type: object
@@ -94,8 +96,17 @@ Let's look at this YAML file, point-by-point:
94 96
 * The `definitions` array contains a list of versioned schema definitions, with
95 97
   corresponding examples.  Everytime you modify your schema and change the version,
96 98
   you should add a new entry here.
  99
+* The `message_type` describes whether the following schema is for requests or responses.
  100
+  It is an optional attribute that when omitted defaults to response. The only valid values
  101
+  are `request` and `response`.
97 102
 * The `versions` array lists the endpoint versions that should be associated with a
98 103
   particular schema definition.
  104
+* The `status_codes` is an optional array of status code strings describing for which
  105
+  status code or codes this schema applies to. `status_codes` is ignored if used with the
  106
+  `request` `message_type`. When used with the `response` `message_type` it is an optional
  107
+  attribute that defaults to all status codes. Valid formats for a status code are 3
  108
+  characters. Each character must be a digit (0-9) or 'x' (wildcard). The following strings
  109
+  are all valid: "200", "4xx", "x0x".
99 110
 * The `schema` contains a [JSON schema](http://tools.ietf.org/html/draft-zyp-json-schema-03)
100 111
   description of the contents of the endpoint. This schema definition is used by the
101 112
   `SchemaValidation` middleware to ensure that your implementation of the endpoint
2  Rakefile 100644 → 100755
@@ -12,7 +12,7 @@ if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'ruby' # MRI only
12 12
 
13 13
   desc "Run cane to check quality metrics"
14 14
   Cane::RakeTask.new(:quality) do |cane|
15  
-    cane.abc_max = 12
  15
+    cane.abc_max = 16
16 16
     cane.add_threshold 'coverage/coverage_percent.txt', :==, 100
17 17
     cane.style_measure = 100
18 18
   end
87  example/definitions/add_address.yml
... ...
@@ -0,0 +1,87 @@
  1
+---
  2
+name: add_contact_address
  3
+route: /contacts/:contact_id/address
  4
+method: PUT
  5
+definitions:
  6
+  - message_type: request
  7
+    versions: ["1.0"]
  8
+    schema:
  9
+      description: Adds a new address for a contact.
  10
+      type: object
  11
+      properties:
  12
+        street:
  13
+          description: "Street address"
  14
+          type: string
  15
+        city:
  16
+          description: "The City"
  17
+          type: string
  18
+        state:
  19
+          description: "The State"
  20
+          type: string
  21
+        zip:
  22
+          description: "Zip code"
  23
+          type: string
  24
+    examples:
  25
+      - street: 1300 Pine St.
  26
+        city: Seattle
  27
+        state: WA
  28
+        zip: '98111'
  29
+  - message_type: request
  30
+    versions: ["2.0"]
  31
+    schema:
  32
+      description: Adds a new address for a contact.
  33
+      type: object
  34
+      properties:
  35
+        street:
  36
+          description: "Street address"
  37
+          type: string
  38
+        apartment:
  39
+          description: "Apartment number"
  40
+          type: string
  41
+          optional: true
  42
+        city:
  43
+          description: "The City"
  44
+          type: string
  45
+        state:
  46
+          description: "The State"
  47
+          type: string
  48
+        postal:
  49
+          description: "Postal code"
  50
+          type: string
  51
+    examples:
  52
+      - street: 1300 Pine St.
  53
+        apartment: 'Suite 400'
  54
+        city: Seattle
  55
+        state: WA
  56
+        postal: '98111'
  57
+  - message_type: response
  58
+    versions: ["1.0"]
  59
+    status_codes: ["2xx"]
  60
+    schema:
  61
+      description: Successfully added an address for a contact.
  62
+      type: object
  63
+      properties:
  64
+        added_address_id:
  65
+          description: "The id of the newly added address"
  66
+          type: integer
  67
+        contact_address_count:
  68
+          description: "The number of addresses for this contact after adding the new one"
  69
+          type: integer
  70
+    examples:
  71
+      - added_address_id: 2185
  72
+        contact_address_count: 5
  73
+  - message_type: response
  74
+    versions: ["1.0"]
  75
+    schema:
  76
+      description: Error messaging for failure to add the new address for a contact.
  77
+      type: object
  78
+      properties:
  79
+        error_code:
  80
+          description: "Error code value for the failure reason"
  81
+          type: integer
  82
+        error_message:
  83
+          description: "Human readable failure message"
  84
+          type: string
  85
+    examples:
  86
+      - error_code: 34
  87
+        error_message: Address already exists for this contact
11  lib/interpol/configuration.rb
@@ -8,14 +8,21 @@ module DefinitionFinder
8 8
     include HashFetcher
9 9
     NoDefinitionFound = Class.new
10 10
 
11  
-    def find_definition(method, path)
  11
+    def find_definition(method, path, message_type, status_code = nil)
12 12
       with_endpoint_matching(method, path) do |endpoint|
13 13
         version = yield endpoint
14  
-        endpoint.definitions.find { |d| d.version == version }
  14
+        find_definitions_for(endpoint, version, message_type).find do |definition|
  15
+          definition.matches_status_code?(status_code)
  16
+        end
15 17
       end
16 18
     end
17 19
 
18 20
   private
  21
+    def find_definitions_for(endpoint, version, message_type)
  22
+      endpoint.definitions.find do |d|
  23
+          d.first.version == version && d.first.message_type == message_type
  24
+      end || []
  25
+    end
19 26
 
20 27
     def with_endpoint_matching(method, path)
21 28
       method = method.downcase.to_sym
16  lib/interpol/documentation_app/views/endpoint.erb
@@ -3,13 +3,15 @@
3 3
     <h1><%= endpoint.name %></h1>
4 4
     <h2><%= endpoint.method.to_s.upcase %> <%= endpoint.route %></h2>
5 5
 
6  
-    <% endpoint.definitions.reverse.each do |definition| %>
7  
-      <div class="versioned-endpoint-definition">
8  
-        <h3>Version <%= definition.version %></h3>
9  
-        <hr>
10  
-
11  
-        <%= Interpol::Documentation.html_for_schema(definition.schema) %>
12  
-      </div>
  6
+    <% endpoint.definitions.each do |definitions| %>
  7
+      <% definitions.each do |definition| %>
  8
+        <div class="versioned-endpoint-definition">
  9
+          <h3><%= definition.message_type.capitalize %> - Version <%= definition.version %> - <%= definition.status_codes %></h3>
  10
+          <br/>
  11
+          <%= Interpol::Documentation.html_for_schema(definition.schema) %>
  12
+        </div>
  13
+        <hr/>
  14
+      <% end %>
13 15
     <% end %>
14 16
   </div>
15 17
 </div><!--/span-->
108  lib/interpol/endpoint.rb
@@ -27,23 +27,36 @@ def initialize(endpoint_hash)
27 27
       validate_name!
28 28
     end
29 29
 
30  
-    def find_definition!(version)
31  
-      @definitions.fetch(version) do
  30
+    def find_definition!(version, message_type)
  31
+      @definitions.fetch([message_type, version]) do
32 32
         message = "No definition found for #{name} endpoint for version #{version}"
  33
+        message << " and message_type #{message_type}"
33 34
         raise ArgumentError.new(message)
34 35
       end
35 36
     end
36 37
 
37  
-    def find_example_for!(version)
38  
-      find_definition!(version).examples.first
  38
+    def find_example_for!(version, message_type)
  39
+      find_definition!(version, message_type).first.examples.first
  40
+    end
  41
+
  42
+    def find_example_status_code_for!(version)
  43
+      find_definition!(version, 'response').first.example_status_code
39 44
     end
40 45
 
41 46
     def available_versions
42  
-      definitions.map(&:version)
  47
+      definitions.map { |d| d.first.version }
43 48
     end
44 49
 
45 50
     def definitions
46  
-      @definitions.values.sort_by(&:version)
  51
+      # sort all requests before all responses
  52
+      # sort higher version numbers before lower version numbers
  53
+      @definitions.values.sort do |x,y|
  54
+        if x.first.message_type == y.first.message_type
  55
+          y.first.version <=> x.first.version
  56
+        else
  57
+          x.first.message_type <=> y.first.message_type
  58
+        end
  59
+      end
47 60
     end
48 61
 
49 62
     def route_matches?(path)
@@ -66,12 +79,16 @@ def route_regex
66 79
       end
67 80
     end
68 81
 
  82
+    DEFAULT_MESSAGE_TYPE = 'response'
  83
+
69 84
     def extract_definitions_from(endpoint_hash)
70  
-      definitions = {}
  85
+      definitions = Hash.new { |h, k| h[k] = [] }
71 86
 
72 87
       fetch_from(endpoint_hash, 'definitions').each do |definition|
73 88
         fetch_from(definition, 'versions').each do |version|
74  
-          definitions[version] = EndpointDefinition.new(name, version, definition)
  89
+          message_type = definition.fetch('message_type', DEFAULT_MESSAGE_TYPE)
  90
+          key = [message_type, version]
  91
+          definitions[key] << EndpointDefinition.new(name, version, message_type, definition)
75 92
         end
76 93
       end
77 94
 
@@ -90,13 +107,15 @@ def validate_name!
90 107
   # Provides the means to validate data against that version of the schema.
91 108
   class EndpointDefinition
92 109
     include HashFetcher
93  
-    attr_reader :endpoint_name, :version, :schema, :examples
94  
-
95  
-    def initialize(endpoint_name, version, definition)
96  
-      @endpoint_name = endpoint_name
97  
-      @version       = version
98  
-      @schema        = fetch_from(definition, 'schema')
99  
-      @examples      = fetch_from(definition, 'examples').map { |e| EndpointExample.new(e, self) }
  110
+    attr_reader :endpoint_name, :message_type, :version, :schema, :examples
  111
+
  112
+    def initialize(endpoint_name, version, message_type, definition)
  113
+      @endpoint_name  = endpoint_name
  114
+      @message_type   = message_type
  115
+      @status_codes   = StatusCodeMatcher.new(definition['status_codes'])
  116
+      @version        = version
  117
+      @schema         = fetch_from(definition, 'schema')
  118
+      @examples       = fetch_from(definition, 'examples').map { |e| EndpointExample.new(e, self) }
100 119
       make_schema_strict!(@schema)
101 120
     end
102 121
 
@@ -108,7 +127,19 @@ def validate_data!(data)
108 127
     end
109 128
 
110 129
     def description
111  
-      "#{endpoint_name} (v. #{version})"
  130
+      "#{endpoint_name} (v. #{version}, mt. #{message_type}, sc. #{status_codes})"
  131
+    end
  132
+
  133
+    def status_codes
  134
+      @status_codes.code_strings.join(',')
  135
+    end
  136
+
  137
+    def matches_status_code?(status_code)
  138
+      status_code.nil? || @status_codes.matches?(status_code)
  139
+    end
  140
+
  141
+    def example_status_code
  142
+      @example_status_code ||= @status_codes.example_status_code
112 143
     end
113 144
 
114 145
   private
@@ -127,6 +158,49 @@ def make_schema_strict!(raw_schema, modify_object=true)
127 158
     end
128 159
   end
129 160
 
  161
+  # Holds the acceptable status codes for an enpoint entry
  162
+  # Acceptable status code are either exact status codes (200, 404, etc)
  163
+  # or partial status codes (2xx, 3xx, 4xx, etc). Currently, partial status
  164
+  # codes can only be a digit followed by two lower-case x's.
  165
+  class StatusCodeMatcher
  166
+    attr_reader :code_strings
  167
+
  168
+    def initialize(codes)
  169
+      codes = ["xxx"] if Array(codes).empty?
  170
+      @code_strings = codes
  171
+      validate!
  172
+    end
  173
+
  174
+    def matches?(status_code)
  175
+      code_regexes.any? { |re| re =~ status_code.to_s }
  176
+    end
  177
+
  178
+    def example_status_code
  179
+      example_status_code = "200"
  180
+      code_strings.first.chars.each_with_index do |char, index|
  181
+        example_status_code[index] = char if char != 'x'
  182
+      end
  183
+      example_status_code
  184
+    end
  185
+
  186
+    private
  187
+      def code_regexes
  188
+        @code_regexes ||= code_strings.map do |string|
  189
+          /\A#{string.gsub('x', '\d')}\z/
  190
+        end
  191
+      end
  192
+
  193
+      def validate!
  194
+        code_strings.each do |code|
  195
+          # ensure code is 3 characters and all chars are a number or 'x'
  196
+          # http://rubular.com/r/4sl68Bb4XO
  197
+          unless code =~ /\A[\dx]{3}\Z/
  198
+            raise StatusCodeMatcherArgumentError, "#{code} is not a valid format"
  199
+          end
  200
+        end
  201
+      end
  202
+  end
  203
+
130 204
   # Wraps an example for a particular endpoint entry.
131 205
   class EndpointExample
132 206
     attr_reader :data, :definition
@@ -140,5 +214,3 @@ def validate!
140 214
     end
141 215
   end
142 216
 end
143  
-
144  
-
3  lib/interpol/errors.rb
@@ -31,5 +31,8 @@ def initialize(errors = [], data = nil, endpoint_description = '')
31 31
   # Error raised when the schema validator cannot find a matching
32 32
   # endpoint definition for the request.
33 33
   class NoEndpointDefinitionFoundError < Error; end
  34
+
  35
+  # Raised when an invalid status code is found during validate_codes!
  36
+  class StatusCodeMatcherArgumentError < ArgumentError; end
34 37
 end
35 38
 
3  lib/interpol/response_schema_validator.rb
@@ -67,7 +67,8 @@ def path
67 67
       end
68 68
 
69 69
       def validator
70  
-        @validator ||= @config.endpoints.find_definition(request_method, path) do |endpoint|
  70
+        @validator ||= @config.endpoints.
  71
+            find_definition(request_method, path, 'response', status) do |endpoint|
71 72
           @config.api_version_for(env, endpoint)
72 73
         end
73 74
       end
8  lib/interpol/stub_app.rb
@@ -18,8 +18,8 @@ def interpol_config
18 18
         self.class.interpol_config
19 19
       end
20 20
 
21  
-      def example_for(endpoint, version)
22  
-        endpoint.find_example_for!(version)
  21
+      def example_for(endpoint, version, message_type)
  22
+        endpoint.find_example_for!(version, message_type)
23 23
       rescue ArgumentError
24 24
         interpol_config.request_version_unavailable(self, version, endpoint.available_versions)
25 25
       end
@@ -49,8 +49,10 @@ def build
49 49
       def endpoint_definition(endpoint)
50 50
         lambda do
51 51
           version = interpol_config.api_version_for(request.env, endpoint)
52  
-          example = example_for(endpoint, version)
  52
+          message_type = 'response'
  53
+          example = example_for(endpoint, version, message_type)
53 54
           example.validate!
  55
+          status endpoint.find_example_status_code_for!(version)
54 56
           JSON.dump(example.data)
55 57
         end
56 58
       end
8  lib/interpol/test_helper.rb
@@ -5,9 +5,11 @@ module TestHelper
5 5
     module Common
6 6
       def each_example_from(endpoints)
7 7
         endpoints.each do |endpoint|
8  
-          endpoint.definitions.each do |definition|
9  
-            definition.examples.each_with_index do |example, index|
10  
-              yield endpoint, definition, example, index
  8
+          endpoint.definitions.each do |definitions|
  9
+            definitions.each do |definition|
  10
+              definition.examples.each_with_index do |example, index|
  11
+                yield endpoint, definition, example, index
  12
+              end
11 13
             end
12 14
           end
13 15
         end
66  spec/unit/interpol/configuration_spec.rb
@@ -4,40 +4,59 @@
4 4
 module Interpol
5 5
   describe DefinitionFinder do
6 6
     describe '#find_definition' do
7  
-      def endpoint(name, method, route, *versions)
  7
+      def endpoint_def(message_type, status_codes, *versions)
  8
+        {
  9
+          'versions' => versions,
  10
+          'message_type' => message_type,
  11
+          'status_codes' => status_codes,
  12
+          'schema' => {},
  13
+          'examples' => {}
  14
+        }
  15
+      end
  16
+
  17
+      def endpoint(name, method, route, *endpoint_defs)
8 18
         Endpoint.new \
9  
-          'name' => 'endpoint_name',
  19
+          'name' => name,
10 20
           'route' => route,
11 21
           'method' => method,
12  
-          'definitions' => [{
13  
-            'versions' => versions,
14  
-            'schema' => {},
15  
-            'examples' => {}
16  
-          }]
  22
+          'definitions' => endpoint_defs
17 23
       end
18 24
 
19  
-      let(:endpoint_1)    { endpoint 'e1', 'GET', '/users/:user_id/overview', '1.3' }
20  
-      let(:endpoint_2)    { endpoint 'e2', 'POST', '/foo/bar', '2.3', '2.7' }
  25
+      let(:endpoint_def_1a) { endpoint_def('response', ['2xx'], '1.3') }
  26
+      let(:endpoint_def_1b) { endpoint_def('response', nil, '1.3') }
  27
+      let(:endpoint_def_2a) { endpoint_def('request', nil, '2.3', '2.7') }
  28
+
  29
+      let(:endpoint_1) do
  30
+        endpoint 'e1', 'GET', '/users/:user_id/overview', endpoint_def_1a, endpoint_def_1b
  31
+      end
  32
+      let(:endpoint_2)    { endpoint 'e2', 'POST', '/foo/bar', endpoint_def_2a}
21 33
       let(:all_endpoints) { [endpoint_1, endpoint_2].extend(DefinitionFinder) }
22 34
 
23 35
       def find(options)
24  
-        all_endpoints.find_definition(options[:method], options[:path]) { |e| options[:version] }
  36
+        find_with_status_code(nil, options)
  37
+      end
  38
+
  39
+      def find_with_status_code(status_code, options)
  40
+        all_endpoints.find_definition(options[:method], options[:path],
  41
+          options[:message_type], status_code) { |e| options[:version] }
25 42
       end
26 43
 
27 44
       it 'finds a matching endpoint definition' do
28  
-        found = find(:method => 'POST', :path => '/foo/bar', :version => '2.3')
  45
+        found = find(:method => 'POST', :path => '/foo/bar',
  46
+          :version => '2.3', :message_type => 'request')
29 47
         found.endpoint_name.should eq(endpoint_2.name)
30 48
         found.version.should eq('2.3')
31 49
       end
32 50
 
33 51
       it 'finds the correct versioned definition of the endpoint' do
34  
-        found = find(:method => 'POST', :path => '/foo/bar', :version => '2.7')
  52
+        found = find(:method => 'POST', :path => '/foo/bar',
  53
+          :version => '2.7', :message_type => 'request')
35 54
         found.version.should eq('2.7')
36 55
       end
37 56
 
38 57
       it 'calls the version block with the endpoint' do
39 58
         endpoint = nil
40  
-        all_endpoints.find_definition('POST', '/foo/bar') do |e|
  59
+        all_endpoints.find_definition('POST', '/foo/bar', 'request') do |e|
41 60
           endpoint = e
42 61
         end
43 62
 
@@ -45,18 +64,29 @@ def find(options)
45 64
       end
46 65
 
47 66
       it 'returns NoDefinitionFound if it cannot find a matching route' do
48  
-        result = find(:method => 'POST', :path => '/goo/bar', :version => '2.7')
  67
+        result = find(:method => 'POST', :path => '/goo/bar',
  68
+          :version => '2.7', :message_type => 'request')
49 69
         result.should be(DefinitionFinder::NoDefinitionFound)
50 70
       end
51 71
 
52 72
       it 'returns nil if the endpoint does not have a matching version' do
53  
-        result = find(:method => 'POST', :path => '/foo/bar', :version => '13.7')
  73
+        result = find(:method => 'POST', :path => '/foo/bar',
  74
+          :version => '13.7', :message_type => 'request')
54 75
         result.should be(DefinitionFinder::NoDefinitionFound)
55 76
       end
56 77
 
57 78
       it 'handles route params properly' do
58  
-        found = find(:method => 'GET', :path => '/users/17/overview', :version => '1.3')
  79
+        found = find_with_status_code('200', :method => 'GET', :path => '/users/17/overview',
  80
+          :version => '1.3', :message_type => 'response')
  81
+        found.endpoint_name.should be(endpoint_1.name)
  82
+        found.status_codes.should eq('2xx')
  83
+      end
  84
+
  85
+      it 'handles status code params properly' do
  86
+        found = find_with_status_code('403', :method => 'GET', :path => '/users/17/overview',
  87
+          :version => '1.3', :message_type => 'response')
59 88
         found.endpoint_name.should be(endpoint_1.name)
  89
+        found.status_codes.should eq('xxx')
60 90
       end
61 91
     end
62 92
   end
@@ -138,7 +168,9 @@ def find(options)
138 168
         def assert_expected_endpoint
139 169
           config.endpoints.size.should eq(1)
140 170
           endpoint = config.endpoints.first
141  
-          endpoint.definitions.first.schema.fetch("properties").should have_key("name")
  171
+          endpoint.definitions.first.each do |definitions|
  172
+            definitions.schema.fetch("properties").should have_key("name")
  173
+          end
142 174
         end
143 175
 
144 176
         it 'supports the merge keys when configured before the endpoint definition files' do
124  spec/unit/interpol/endpoint_spec.rb
@@ -54,17 +54,32 @@ def build_hash(hash = {})
54 54
       'examples' => ['e1', 'e2']
55 55
     }] end
56 56
 
  57
+    let(:request_definition_array) do [{
  58
+      'versions'      => ['1.1'],
  59
+      'message_type'  => 'request',
  60
+      'schema'        => {'a' => ' request schema'},
  61
+      'examples'      => ['e1', 'e2']
  62
+    }] end
  63
+
57 64
     describe "#definitions" do
58 65
       it 'returns each definition object, ordered by version' do
59 66
         endpoint = Endpoint.new(build_hash('definitions' => definitions_array))
60  
-        endpoint.definitions.map(&:version).should eq(%w[ 1.2 3.2 ])
  67
+        endpoint.definitions.map{|d|d.first.version}.should eq(%w[ 3.2 1.2 ])
61 68
       end
  69
+
  70
+      it 'returns each definition object, ordered by message type' do
  71
+        full_definitions_array = (definitions_array + request_definition_array)
  72
+        endpoint = Endpoint.new(build_hash('definitions' => full_definitions_array))
  73
+        endpoint.definitions.map{|d|d.first.version}.should eq(%w[ 1.1 3.2 1.2 ])
  74
+        endpoint.definitions.map{|d|d.first.message_type}.should eq(%w[ request response response ])
  75
+      end
  76
+
62 77
     end
63 78
 
64 79
     describe "#available_versions" do
65 80
       it 'returns the list of available version strings, ordered by version' do
66 81
         endpoint = Endpoint.new(build_hash('definitions' => definitions_array))
67  
-        endpoint.available_versions.should eq(%w[ 1.2 3.2 ])
  82
+        endpoint.available_versions.should eq(%w[ 3.2 1.2 ])
68 83
       end
69 84
     end
70 85
 
@@ -72,13 +87,15 @@ def build_hash(hash = {})
72 87
       let(:hash) { build_hash('definitions' => definitions_array) }
73 88
       let(:endpoint) { Endpoint.new(hash) }
74 89
 
75  
-      it 'finds the definition matching the given version' do
76  
-        endpoint.find_definition!('1.2').version.should eq('1.2')
  90
+      it 'finds the definition matching the given version and message_type' do
  91
+        definitions = endpoint.find_definition!('1.2', 'response')
  92
+        definitions.first.version.should eq('1.2')
  93
+        definitions.first.message_type.should eq('response')
77 94
       end
78 95
 
79 96
       it 'raises an error when given a version that matches no definition' do
80 97
         expect {
81  
-          endpoint.find_definition!('2.1')
  98
+          endpoint.find_definition!('2.1', 'response')
82 99
         }.to raise_error(ArgumentError)
83 100
       end
84 101
     end
@@ -88,12 +105,12 @@ def build_hash(hash = {})
88 105
       let(:endpoint) { Endpoint.new(hash) }
89 106
 
90 107
       it 'returns an example for the requested version' do
91  
-        endpoint.find_example_for!('1.2').data.should eq('e1')
  108
+        endpoint.find_example_for!('1.2', 'response').data.should eq('e1')
92 109
       end
93 110
 
94 111
       it 'raises an error when given a version it does not have' do
95 112
         expect {
96  
-          endpoint.find_example_for!('2.1')
  113
+          endpoint.find_example_for!('2.1', 'response')
97 114
         }.to raise_error(ArgumentError)
98 115
       end
99 116
     end
@@ -142,20 +159,35 @@ def build_hash(hash = {})
142 159
     let(:version)  { '1.0' }
143 160
 
144 161
     it 'initializes the endpoint_name' do
145  
-      EndpointDefinition.new("e-name", version, build_hash).endpoint_name.should eq("e-name")
  162
+      endpoint_def = EndpointDefinition.new("e-name", version, 'response', build_hash)
  163
+      endpoint_def.endpoint_name.should eq("e-name")
146 164
     end
147 165
 
148 166
     it 'initializes the version' do
149  
-      EndpointDefinition.new("name", '2.3', build_hash).version.should eq('2.3')
  167
+      endpoint_def = EndpointDefinition.new("name", '2.3', 'response', build_hash)
  168
+      endpoint_def.version.should eq('2.3')
  169
+    end
  170
+
  171
+    it 'default initialized the message type' do
  172
+      endpoint_def = EndpointDefinition.new("name", '2.3', 'response', build_hash)
  173
+      endpoint_def.message_type.should eq('response')
  174
+    end
  175
+
  176
+    it 'initializes the message type' do
  177
+      hash = build_hash('message_type' => 'request')
  178
+      endpoint_def = EndpointDefinition.new("name", '2.3', 'request', hash)
  179
+      endpoint_def.message_type.should eq('request')
150 180
     end
151 181
 
152 182
     it 'initializes the example data' do
153  
-      v = EndpointDefinition.new("name", version, build_hash('examples' => [{'a' => 5}]))
  183
+      hash = build_hash('examples' => [{'a' => 5}])
  184
+      v = EndpointDefinition.new("name", version, 'response', hash)
154 185
       v.examples.map(&:data).should eq([{ 'a' => 5 }])
155 186
     end
156 187
 
157 188
     it 'initializes the schema' do
158  
-      v = EndpointDefinition.new("name", version, build_hash('schema' => {'the' => 'schema'}))
  189
+      hash = build_hash('schema' => {'the' => 'schema'})
  190
+      v = EndpointDefinition.new("name", version, 'response', hash)
159 191
       v.schema['the'].should eq('schema')
160 192
     end
161 193
 
@@ -163,7 +195,7 @@ def build_hash(hash = {})
163 195
       it "raises an error if not initialized with '#{attr}'" do
164 196
         hash = build_hash.reject { |k, v| k == attr }
165 197
         expect {
166  
-          EndpointDefinition.new("name", version, hash)
  198
+          EndpointDefinition.new("name", version, 'response', hash)
167 199
         }.to raise_error(/key not found.*#{attr}/)
168 200
       end
169 201
     end
@@ -174,7 +206,9 @@ def build_hash(hash = {})
174 206
         'properties' => {'foo' => { 'type' => 'integer' } }
175 207
       } end
176 208
 
177  
-      subject { EndpointDefinition.new("e-name", version, build_hash('schema' => schema)) }
  209
+      subject {
  210
+        EndpointDefinition.new("e-name", version, 'response', build_hash('schema' => schema))
  211
+      }
178 212
 
179 213
       it 'raises a validation error when given data of the wrong type' do
180 214
         expect {
@@ -194,7 +228,8 @@ def build_hash(hash = {})
194 228
       end
195 229
 
196 230
       it 'rejects unrecognized data types' do
197  
-        pending "waiting for my json-schema PR to be merged: https://github.com/hoxworth/json-schema/pull/37" do
  231
+        pending "waiting for my json-schema PR to be merged: " +
  232
+           "https://github.com/hoxworth/json-schema/pull/37" do
198 233
           schema['properties']['foo']['type'] = 'sting'
199 234
           expect {
200 235
             subject.validate_data!('foo' => 'bar')
@@ -288,6 +323,67 @@ def build_hash(hash = {})
288 323
     end
289 324
   end
290 325
 
  326
+  describe StatusCodeMatcher do
  327
+    describe "#new" do
  328
+      it 'initializes the codes for nil' do
  329
+        StatusCodeMatcher.new(nil).code_strings.should == ['xxx']
  330
+      end
  331
+
  332
+      it 'initializs the codes for a single code' do
  333
+        StatusCodeMatcher.new(['200']).code_strings.should == ["200"]
  334
+      end
  335
+
  336
+      it 'initializs the codes for a multiple codes' do
  337
+        StatusCodeMatcher.new(['200', '4xx', 'x0x']).code_strings.should == ['200', '4xx', 'x0x']
  338
+      end
  339
+
  340
+      it 'should raise an error for invalid status code formats' do
  341
+        expect {
  342
+          StatusCodeMatcher.new(['200', '4y4'])
  343
+        }.to raise_error(StatusCodeMatcherArgumentError)
  344
+
  345
+        expect {
  346
+          StatusCodeMatcher.new(['2000', '404'])
  347
+        }.to raise_error(StatusCodeMatcherArgumentError)
  348
+      end
  349
+    end
  350
+
  351
+    describe "#matches?" do
  352
+      let(:nil_codes_subject) { StatusCodeMatcher.new(nil) }
  353
+      it 'returns true when codes is nil' do
  354
+        nil_codes_subject.matches?('200').should be_true
  355
+      end
  356
+
  357
+      subject { StatusCodeMatcher.new(['200', '4xx', 'x5x']) }
  358
+      it 'returns true for an exact match' do
  359
+        subject.matches?('200').should be_true
  360
+      end
  361
+
  362
+      it 'returns true for a partial matches' do
  363
+        subject.matches?('401').should be_true
  364
+        subject.matches?('454').should be_true
  365
+      end
  366
+
  367
+      it 'returns false for no matches' do
  368
+        subject.matches?('202').should be_false
  369
+      end
  370
+    end
  371
+
  372
+    describe '#example_status_code' do
  373
+      it 'returns a valid example status code when a specific status code was given' do
  374
+        StatusCodeMatcher.new(['404']).example_status_code.should == '404'
  375
+      end
  376
+
  377
+      it 'returns a valid example status code when no status codes were given' do
  378
+        StatusCodeMatcher.new(nil).example_status_code.should == '200'
  379
+      end
  380
+
  381
+      it 'returns a valid example status code based on the first status code' do
  382
+        StatusCodeMatcher.new(['4xx', 'x0x']).example_status_code.should == '400'
  383
+      end
  384
+    end
  385
+  end
  386
+
291 387
   describe EndpointExample do
292 388
     describe "#validate!" do
293 389
       let(:definition) { fire_double("Interpol::EndpointDefinition") }
2  spec/unit/interpol/response_schema_validator_spec.rb
@@ -80,7 +80,7 @@ def stub_lookup(v = validator)
80 80
       validator.should_receive(:validate_data!).with("a" => "b")
81 81
 
82 82
       default_definition_finder.should_receive(:find_definition).
83  
-        with("GET", "/search/200/overview").
  83
+        with("GET", "/search/200/overview", "response", 200).
84 84
         and_return(validator)
85 85
 
86 86
       get '/search/200/overview'
2  spec/unit/interpol/stub_app_spec.rb
@@ -101,7 +101,7 @@ def parsed_body
101 101
     end
102 102
 
103 103
     let(:endpoint_example) do
104  
-      endpoint.find_example_for!('1.0')
  104
+      endpoint.find_example_for!('1.0', 'response')
105 105
     end
106 106
 
107 107
     it 'performs validations by default' do
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.