interpol 0.0.8 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -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
data/Rakefile CHANGED
@@ -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
@@ -8,14 +8,21 @@ module Interpol
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
@@ -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-->
@@ -27,23 +27,36 @@ module Interpol
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 @@ module Interpol
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 @@ module Interpol
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 @@ module Interpol
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 @@ module Interpol
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 @@ module Interpol
140
214
  end
141
215
  end
142
216
  end
143
-
144
-
@@ -31,5 +31,8 @@ module Interpol
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
 
@@ -67,7 +67,8 @@ module Interpol
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
@@ -18,8 +18,8 @@ module Interpol
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 @@ module Interpol
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
@@ -5,9 +5,11 @@ module Interpol
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
@@ -1,3 +1,3 @@
1
1
  module Interpol
2
- VERSION = "0.0.8"
2
+ VERSION = "0.1.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: interpol
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-05-29 00:00:00.000000000 Z
12
+ date: 2012-06-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: json-schema
@@ -218,7 +218,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
218
218
  version: '0'
219
219
  segments:
220
220
  - 0
221
- hash: -3251393236495982186
221
+ hash: -430941373413258822
222
222
  required_rubygems_version: !ruby/object:Gem::Requirement
223
223
  none: false
224
224
  requirements:
@@ -227,7 +227,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
227
227
  version: '0'
228
228
  segments:
229
229
  - 0
230
- hash: -3251393236495982186
230
+ hash: -430941373413258822
231
231
  requirements: []
232
232
  rubyforge_project:
233
233
  rubygems_version: 1.8.24