interpol 0.0.1

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.
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in interpol.gemspec
4
+ gemspec
5
+
6
+ group :extras do
7
+ gem 'debugger' if RUBY_ENGINE == 'ruby' && RUBY_VERSION == '1.9.3'
8
+ end
9
+
10
+ gem 'json-jruby', platform: 'jruby'
data/License ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 SEOmoz
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,263 @@
1
+ # Interpol
2
+
3
+ [![Build Status](https://secure.travis-ci.org/seomoz/interpol.png)](http://travis-ci.org/seomoz/interpol)
4
+
5
+ Interpol is a toolkit for policing your HTTP JSON interface. To use it,
6
+ define the endpoints of your HTTP API in simple YAML files.
7
+ Interpol provides multiple tools to work with these endpoint
8
+ definitions:
9
+
10
+ * `Interpol::TestHelper::RSpec` and `Interpol::TestHelper::TestUnit` are
11
+ modules that you can mix in to your test context. They provide a means
12
+ to generate tests from your endpoint definitions that validate example
13
+ data against your JSON schema definition.
14
+ * `Interpol::StubApp` builds a stub implementation of your API from
15
+ the endpoint definitions. This can be distributed with your API's
16
+ client gem so that API users have something local to hit that
17
+ generates data that is valid according to your schema definition.
18
+ * `Interpol::ResponseSchemaValidator` is a rack middleware that
19
+ validates your API responses against the JSON schema in your endpoint
20
+ definition files. This is useful in test/development environments to
21
+ ensure that your real API returns valid responses.
22
+ * A documentation browser for your API is planned as well (but not yet
23
+ implemented).
24
+
25
+ You can use any of these tools individually or some combination of all
26
+ of them.
27
+
28
+ ## Installation
29
+
30
+ Add this line to your application's Gemfile:
31
+
32
+ gem 'interpol'
33
+
34
+ And then execute:
35
+
36
+ $ bundle
37
+
38
+ Or install it yourself as:
39
+
40
+ $ gem install interpol
41
+
42
+ ## Endpoint Definition
43
+
44
+ Endpoints are defined in YAML files, using a separate
45
+ file per endpoint. Here's an example:
46
+
47
+ ``` yaml
48
+ ---
49
+ name: user_projects
50
+ route: /users/:user_id/projects
51
+ method: GET
52
+ definitions:
53
+ - versions: ["1.0"]
54
+ schema:
55
+ description: Returns a list of projects for the given user.
56
+ type: object
57
+ properties:
58
+ projects:
59
+ description: List of projects.
60
+ type: array
61
+ items:
62
+ type: object
63
+ properties:
64
+ name:
65
+ description: The name of the project.
66
+ type: string
67
+ importance:
68
+ description: The importance of the project, on a scale of 1 to 10.
69
+ type: integer
70
+ minimum: 1
71
+ maximum: 10
72
+
73
+ examples:
74
+ - projects:
75
+ - name: iPhone App
76
+ importance: 5
77
+ - name: Rails App
78
+ importance: 7
79
+ ```
80
+
81
+ Let's look at this YAML file, point-by-point:
82
+
83
+ * `name` can be anything you want. Each endpoint should have a different name. Interpol uses
84
+ it in schema validation error messages.
85
+ * `route` defines the sinatra route for this endpoint. Note that while
86
+ Interpol::StubApp supports any sinatra route, Interpol::ResponseSchemaValidator
87
+ (which has to find a matching endpoint definition from the request path), only
88
+ supports a subset of Sinatra's routing syntax. Specifically, it supports static
89
+ segments (`users` and `projects` in the example above) and named
90
+ parameter segments (`:user_id` in the example above).
91
+ * `method` defines the HTTP method for this endpoint. The method should be in uppercase.
92
+ * The `definitions` array contains a list of versioned schema definitions, with
93
+ corresponding examples. Everytime you modify your schema and change the version,
94
+ you should add a new entry here.
95
+ * The `versions` array lists the endpoint versions that should be associated with a
96
+ particular schema definition.
97
+ * The `schema` contains a [JSON schema](http://tools.ietf.org/html/draft-zyp-json-schema-03)
98
+ description of the contents of the endpoint. This schema definition is used by the
99
+ `SchemaValidation` middleware to ensure that your implementation of the endpoint
100
+ matches the definition.
101
+ * `examples` contains a list of valid example data. It is used by the stub app as example data.
102
+
103
+ ## Configuration
104
+
105
+ Interpol provides two levels of configuration: global default
106
+ configuration, and one-off configuration, set on a particular
107
+ instance of one of the provided tools. Each of the tools accepts
108
+ a configuration block that provides an identical API to the
109
+ global configuration API shown below.
110
+
111
+ ``` ruby
112
+ require 'interpol'
113
+
114
+ Interpol.default_configuration do |config|
115
+ # Tells Interpol where to find your endpoint definition files.
116
+ #
117
+ # Needed by all tools.
118
+ config.endpoint_definition_files = Dir["config/endpoints/*.yml"]
119
+
120
+ # Determines which versioned endpoint definition Interpol uses
121
+ # for a request. You can also use a block form, which yields
122
+ # the rack env hash and the endpoint object as arguments.
123
+ # This is useful when you need to extract the version from a
124
+ # request header (e.g. Accept) or from the request URI.
125
+ #
126
+ # Needed by Interpol::StubApp and Interpol::ResponseSchemaValidator.
127
+ config.api_version '1.0'
128
+
129
+ # Determines the stub app response when the requested version is not
130
+ # available. This block will be eval'd in the context of the stub app
131
+ # sinatra application, so you can use sinatra helpers like `halt` here.
132
+ #
133
+ # Needed by Interpol::StubApp.
134
+ config.on_unavailable_request_version do |requested_version, available_versions|
135
+ message = JSON.dump(
136
+ "message" => "Not Acceptable",
137
+ "requested_version" => requested_version,
138
+ "available_versions" => available_versions
139
+ )
140
+
141
+ halt 406, message
142
+ end
143
+
144
+ # Determines which responses will be validated against the endpoint
145
+ # definition when you use Interpol::ResponseSchemaValidator. The
146
+ # validation is meant to run against the "happy path" response.
147
+ # For responses like "404 Not Found", you probably don't want any
148
+ # validation performed. The default validate_if hook will cause
149
+ # validation to run against any 2xx response except 204 ("No Content").
150
+ #
151
+ # Used by Interpol::ResponseSchemaValidator.
152
+ config.validate_if do |env, status, headers, body|
153
+ headers['Content-Type'] == my_custom_mime_type
154
+ end
155
+
156
+ # Determines how Interpol::ResponseSchemaValidator handles
157
+ # invalid data. By default it will raise an error, but you can
158
+ # make it print a warning instead.
159
+ #
160
+ # Used by Interpol::ResponseSchemaValidator.
161
+ config.validation_mode = :error # or :warn
162
+ end
163
+
164
+ ```
165
+
166
+ ## Tool Usage
167
+
168
+ ### Interpol::TestHelper::RSpec and Interpol::TestHelper::TestUnit
169
+
170
+ These are modules that you can extend onto an RSpec example group
171
+ or a `Test::Unit::TestCase` subclass, respectively.
172
+ They provide a `define_interpol_example_tests` macro that will define
173
+ a test for each example for each schema definition in your endpoint
174
+ definition files. The tests will validate that your schema is a valid
175
+ JSON schema definition and will validate that the examples are valid
176
+ according to that schema.
177
+
178
+ RSpec example:
179
+
180
+ ``` ruby
181
+ require 'interpol/test_helper'
182
+
183
+ describe "My API endpoints" do
184
+ extend Interpol::TestHelper::RSpec
185
+
186
+ # the block is only necessary if you want to override the default
187
+ # config or if you have not set a default config.
188
+ define_interpol_example_tests do |ipol|
189
+ ipol.endpoint_definition_files = Dir["config/endpoints_definitions/*.yml"]
190
+ end
191
+ end
192
+ ```
193
+
194
+ Test::Unit example:
195
+
196
+ ``` ruby
197
+ require 'interpol/test_helper'
198
+
199
+ class MyAPIEndpointsTest < Test::Unit::TestCase
200
+ extend Interpol::TestHelper::TestUnit
201
+ define_interpol_example_tests
202
+ end
203
+ ```
204
+
205
+ ### Interpol::StubApp
206
+
207
+ This will build a little sinatra app that returns example data from
208
+ your endpoint definition files.
209
+
210
+ Example:
211
+
212
+ ``` ruby
213
+ # config.ru
214
+
215
+ require 'interpol/stub_app'
216
+
217
+ # the block is only necessary if you want to override the default
218
+ # config or if you have not set a default config.
219
+ stub_app = Interpol::StubApp.build do |app|
220
+ app.endpoint_definition_files = Dir["config/endpoints_definitions/*.yml"]
221
+ app.api_version do |env|
222
+ RequestVersion.extract_from(env['HTTP_ACCEPT'])
223
+ end
224
+ end
225
+
226
+ run stub_app
227
+ ```
228
+
229
+ ### Interpol::ResponseSchemaValidator
230
+
231
+ This rack middleware validates the responses from your app
232
+ against the schema definition. Here's an example of how you
233
+ might use it with a class-style sinatra app:
234
+
235
+ ``` ruby
236
+ require 'sinatra'
237
+
238
+ # You probably only want to validate the schema in local development.
239
+ unless ENV['RACK_ENV'] == 'production'
240
+ require 'interpol/response_schema_validator'
241
+
242
+ # the block is only necessary if you want to override the default
243
+ # config or if you have not set a default config.
244
+ use Interpol::ResponseSchemaValidator do |config|
245
+ config.endpoint_definition_files = Dir["config/endpoints_definitions/*.yml"]
246
+ config.api_version do |env|
247
+ RequestVersion.extract_from(env['HTTP_ACCEPT'])
248
+ end
249
+ end
250
+ end
251
+
252
+ get '/users/:user_id/projects' do
253
+ JSON.dump(User.find(params[:user_id]).projects)
254
+ end
255
+ ```
256
+
257
+ ## Contributing
258
+
259
+ 1. Fork it
260
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
261
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
262
+ 4. Push to the branch (`git push origin my-new-feature`)
263
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec) do |t|
6
+ t.rspec_opts = %w[--profile --format progress]
7
+ t.ruby_opts = "-Ispec -rsimplecov_setup"
8
+ end
9
+
10
+ if RUBY_ENGINE == 'ruby' # MRI only
11
+ require 'cane/rake_task'
12
+
13
+ desc "Run cane to check quality metrics"
14
+ Cane::RakeTask.new(:quality) do |cane|
15
+ cane.abc_max = 12
16
+ cane.add_threshold 'coverage/coverage_percent.txt', :==, 100
17
+ cane.style_measure = 100
18
+ end
19
+ else
20
+ task(:quality) { } # no-op
21
+ end
22
+
23
+ task default: [:spec, :quality]
@@ -0,0 +1,101 @@
1
+ require 'interpol/endpoint'
2
+ require 'interpol/errors'
3
+ require 'yaml'
4
+
5
+ module Interpol
6
+ module DefinitionFinder
7
+ include HashFetcher
8
+ NoDefinitionFound = Class.new
9
+
10
+ def find_definition(method, path)
11
+ with_endpoint_matching(method, path) do |endpoint|
12
+ version = yield endpoint
13
+ endpoint.definitions.find { |d| d.version == version }
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def with_endpoint_matching(method, path)
20
+ method = method.downcase.to_sym
21
+ endpoint = find { |e| e.method == method && e.route_matches?(path) }
22
+ (yield endpoint if endpoint) || NoDefinitionFound
23
+ end
24
+ end
25
+
26
+ # Public: Defines interpol configuration.
27
+ class Configuration
28
+ attr_reader :endpoint_definition_files, :endpoints
29
+ attr_accessor :validation_mode
30
+
31
+ def initialize
32
+ api_version do
33
+ raise ConfigurationError, "api_version has not been configured"
34
+ end
35
+
36
+ validate_if do |env, status, headers, body|
37
+ (200..299).cover?(status) && status != 204 # No Content
38
+ end
39
+
40
+ on_unavailable_request_version do |requested, available|
41
+ message = "The requested API version is invalid. " +
42
+ "Requested: #{requested}. " +
43
+ "Available: #{available}"
44
+ halt 406, JSON.dump(error: message)
45
+ end
46
+
47
+ self.endpoint_definition_files = []
48
+ yield self if block_given?
49
+ end
50
+
51
+ def endpoint_definition_files=(files)
52
+ self.endpoints = files.map do |file|
53
+ Endpoint.new(YAML.load_file file)
54
+ end
55
+ @endpoint_definition_files = files
56
+ end
57
+
58
+ def endpoints=(endpoints)
59
+ @endpoints = endpoints.extend(DefinitionFinder)
60
+ end
61
+
62
+ def api_version(version=nil, &block)
63
+ if [version, block].compact.size.even?
64
+ raise ConfigurationError.new("api_version requires a static version " +
65
+ "or a dynamic block, but not both")
66
+ end
67
+
68
+ @api_version_block = block || lambda { |*a| version }
69
+ end
70
+
71
+ def api_version_for(rack_env, endpoint=nil)
72
+ @api_version_block.call(rack_env, endpoint).to_s
73
+ end
74
+
75
+ def validate_if(&block)
76
+ @validate_if_block = block
77
+ end
78
+
79
+ def validate?(*args)
80
+ @validate_if_block.call(*args)
81
+ end
82
+
83
+ def on_unavailable_request_version(&block)
84
+ @unavailable_request_version_block = block
85
+ end
86
+
87
+ def request_version_unavailable(execution_context, *args)
88
+ execution_context.instance_exec(*args, &@unavailable_request_version_block)
89
+ end
90
+
91
+ def self.default
92
+ @default ||= Configuration.new
93
+ end
94
+
95
+ def customized_duplicate(&block)
96
+ block ||= lambda { |c| }
97
+ dup.tap(&block)
98
+ end
99
+ end
100
+ end
101
+
@@ -0,0 +1,133 @@
1
+ require 'json-schema'
2
+ require 'interpol/errors'
3
+
4
+ module Interpol
5
+ module HashFetcher
6
+ # Unfortunately, on JRuby 1.9, the error raised from Hash#fetch when
7
+ # the key is not found does not include the key itself :(. So we work
8
+ # around it here.
9
+ def fetch_from(hash, key)
10
+ hash.fetch(key) do
11
+ raise ArgumentError.new("key not found: #{key.inspect}")
12
+ end
13
+ end
14
+ end
15
+
16
+ # Represents an endpoint. Instances of this class are constructed
17
+ # based on the endpoint definitions in the YAML files.
18
+ class Endpoint
19
+ include HashFetcher
20
+ attr_reader :name, :route, :method
21
+
22
+ def initialize(endpoint_hash)
23
+ @name = fetch_from(endpoint_hash, 'name')
24
+ @route = fetch_from(endpoint_hash, 'route')
25
+ @method = fetch_from(endpoint_hash, 'method').downcase.to_sym
26
+ @definitions = extract_definitions_from(endpoint_hash)
27
+ end
28
+
29
+ def find_example_for!(version)
30
+ d = @definitions.fetch(version) do
31
+ message = "No definition found for #{name} endpoint for version #{version}"
32
+ raise ArgumentError.new(message)
33
+ end
34
+ d.examples.first
35
+ end
36
+
37
+ def available_versions
38
+ definitions.map(&:version)
39
+ end
40
+
41
+ def definitions
42
+ @definitions.values.sort_by(&:version)
43
+ end
44
+
45
+ def route_matches?(path)
46
+ path =~ route_regex
47
+ end
48
+
49
+ private
50
+
51
+ def route_regex
52
+ @route_regex ||= begin
53
+ regex_string = route.split('/').map do |path_part|
54
+ if path_part.start_with?(':')
55
+ '[^\/]+' # it's a parameter; match anything
56
+ else
57
+ Regexp.escape(path_part)
58
+ end
59
+ end.join('\/')
60
+
61
+ /\A#{regex_string}\z/
62
+ end
63
+ end
64
+
65
+ def extract_definitions_from(endpoint_hash)
66
+ definitions = {}
67
+
68
+ fetch_from(endpoint_hash, 'definitions').each do |definition|
69
+ fetch_from(definition, 'versions').each do |version|
70
+ definitions[version] = EndpointDefinition.new(self, version, definition)
71
+ end
72
+ end
73
+
74
+ definitions
75
+ end
76
+ end
77
+
78
+ # Wraps a single versioned definition for an endpoint.
79
+ # Provides the means to validate data against that version of the schema.
80
+ class EndpointDefinition
81
+ include HashFetcher
82
+ attr_reader :endpoint, :version, :schema, :examples
83
+
84
+ def initialize(endpoint, version, definition)
85
+ @endpoint = endpoint
86
+ @version = version
87
+ @schema = fetch_from(definition, 'schema')
88
+ @examples = fetch_from(definition, 'examples').map { |e| EndpointExample.new(e, self) }
89
+ make_schema_strict!(@schema)
90
+ end
91
+
92
+ def validate_data!(data)
93
+ errors = ::JSON::Validator.fully_validate_schema(schema)
94
+ raise ValidationError.new(errors, nil, description) if errors.any?
95
+ errors = ::JSON::Validator.fully_validate(schema, data)
96
+ raise ValidationError.new(errors, data, description) if errors.any?
97
+ end
98
+
99
+ def description
100
+ "#{endpoint.name} (v. #{version})"
101
+ end
102
+
103
+ private
104
+
105
+ def make_schema_strict!(raw_schema, modify_object=true)
106
+ return unless Hash === raw_schema
107
+
108
+ raw_schema.each do |key, value|
109
+ make_schema_strict!(value, key != 'properties')
110
+ end
111
+
112
+ return unless modify_object
113
+
114
+ raw_schema['additionalProperties'] = false
115
+ raw_schema['required'] = !raw_schema.delete('optional')
116
+ end
117
+ end
118
+
119
+ # Wraps an example for a particular endpoint entry.
120
+ class EndpointExample
121
+ attr_reader :data, :definition
122
+
123
+ def initialize(data, definition)
124
+ @data, @definition = data, definition
125
+ end
126
+
127
+ def validate!
128
+ definition.validate_data!(data)
129
+ end
130
+ end
131
+ end
132
+
133
+
@@ -0,0 +1,28 @@
1
+ module Interpol
2
+ # Base class that interpol errors should subclass.
3
+ class Error < StandardError; end
4
+
5
+ # Error raised when the configuration is invalid.
6
+ class ConfigurationError < Error; end
7
+
8
+ # Error raised when data fails to validate against the schema
9
+ # for an endpoint.
10
+ class ValidationError < Error
11
+ attr_reader :errors
12
+
13
+ def initialize(errors = [], data = nil, endpoint_description = '')
14
+ @errors = errors
15
+ error_bullet_points = errors.map { |e| "\n - #{e}" }.join
16
+ message = "Found #{errors.size} error(s) when validating " +
17
+ "against endpoint #{endpoint_description}. " +
18
+ "Errors: #{error_bullet_points}.\n\nData:\n#{data.inspect}"
19
+
20
+ super(message)
21
+ end
22
+ end
23
+
24
+ # Error raised when the schema validator cannot find a matching
25
+ # endpoint definition for the request.
26
+ class NoEndpointDefinitionFoundError < Error; end
27
+ end
28
+
@@ -0,0 +1,87 @@
1
+ require 'interpol'
2
+ require 'json'
3
+
4
+ module Interpol
5
+ # Rack middleware that validates response data against the schema
6
+ # definition for the endpoint. Can be configured to raise an
7
+ # error or warn in this condition. Intended for development and
8
+ # test use.
9
+ class ResponseSchemaValidator
10
+ def initialize(app, &block)
11
+ @config = Configuration.default.customized_duplicate(&block)
12
+ @app = app
13
+ @handler_class = @config.validation_mode == :warn ? HandlerWithWarnings : Handler
14
+ end
15
+
16
+ def call(env)
17
+ status, headers, body = @app.call(env)
18
+ return status, headers, body unless @config.validate?(env, status, headers, body)
19
+
20
+ handler = @handler_class.new(status, headers, body, env, @config)
21
+ handler.validate!
22
+
23
+ return status, headers, handler.extracted_body
24
+ end
25
+
26
+ # Private: handles a responses and validates it. Validation
27
+ # errors will result in an error.
28
+ class Handler
29
+ attr_reader :status, :headers, :body, :env, :config
30
+
31
+ def initialize(status, headers, body, env, config)
32
+ @status, @headers, @body, @env, @config = status, headers, body, env, config
33
+ end
34
+
35
+ def validate!
36
+ unless validator == Interpol::DefinitionFinder::NoDefinitionFound
37
+ return validator.validate_data!(data)
38
+ end
39
+
40
+ raise NoEndpointDefinitionFoundError,
41
+ "No endpoint definition could be found for: #{request_method} '#{path}'"
42
+ end
43
+
44
+ # The only interface we can count on from the body is that it
45
+ # implements #each. It may not be re-windable. To preserve this
46
+ # interface while reading the whole thing, we need to extract
47
+ # it into our own array.
48
+ def extracted_body
49
+ @extracted_body ||= [].tap do |extracted_body|
50
+ body.each { |str| extracted_body << str }
51
+ body.close if body.respond_to?(:close)
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def request_method
58
+ env.fetch('REQUEST_METHOD')
59
+ end
60
+
61
+ def data
62
+ @data ||= JSON.parse(extracted_body.join)
63
+ end
64
+
65
+ def path
66
+ env.fetch('PATH_INFO')
67
+ end
68
+
69
+ def validator
70
+ @validator ||= @config.endpoints.find_definition(request_method, path) do |endpoint|
71
+ @config.api_version_for(env, endpoint)
72
+ end
73
+ end
74
+ end
75
+
76
+ # Private: Subclasses Handler in order to convert validation errors
77
+ # to warnings instead.
78
+ class HandlerWithWarnings < Handler
79
+ def validate!
80
+ super
81
+ rescue ValidationError, NoEndpointDefinitionFoundError => e
82
+ Kernel.warn e.message
83
+ end
84
+ end
85
+ end
86
+ end
87
+
@@ -0,0 +1,60 @@
1
+ require 'interpol'
2
+ require 'sinatra/base'
3
+ require 'json'
4
+
5
+ module Interpol
6
+ module StubApp
7
+ extend self
8
+
9
+ def build(&block)
10
+ config = Configuration.default.customized_duplicate(&block)
11
+ builder = Builder.new(config)
12
+ builder.build
13
+ builder.app
14
+ end
15
+
16
+ module Helpers
17
+ def interpol_config
18
+ self.class.interpol_config
19
+ end
20
+
21
+ def example_for(endpoint, version)
22
+ endpoint.find_example_for!(version)
23
+ rescue ArgumentError
24
+ interpol_config.request_version_unavailable(self, version, endpoint.available_versions)
25
+ end
26
+ end
27
+
28
+ # Private: Builds a stub sinatra app for the given interpol
29
+ # configuration.
30
+ class Builder
31
+ attr_reader :app
32
+
33
+ def initialize(config)
34
+ @app = Sinatra.new do
35
+ set :interpol_config, config
36
+ helpers Helpers
37
+ not_found { JSON.dump(error: "The requested resource could not be found") }
38
+ before { content_type "application/json;charset=utf-8" }
39
+ get('/__ping') { JSON.dump(message: "Interpol stub app running.") }
40
+ end
41
+ end
42
+
43
+ def build
44
+ @app.interpol_config.endpoints.each do |endpoint|
45
+ app.send(endpoint.method, endpoint.route, &endpoint_definition(endpoint))
46
+ end
47
+ end
48
+
49
+ def endpoint_definition(endpoint)
50
+ lambda do
51
+ version = interpol_config.api_version_for(request.env, endpoint)
52
+ example = example_for(endpoint, version)
53
+ example.validate!
54
+ JSON.dump(example.data)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+
@@ -0,0 +1,44 @@
1
+ require 'interpol'
2
+
3
+ module Interpol
4
+ module TestHelper
5
+ module Common
6
+ def each_example_from(endpoints)
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
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ def define_interpol_example_tests(&block)
17
+ config = Configuration.default.customized_duplicate(&block)
18
+
19
+ each_example_from(config.endpoints) do |endpoint, definition, example, example_index|
20
+ description = "#{endpoint.name} (v #{definition.version}) has " +
21
+ "valid data for example #{example_index + 1}"
22
+ define_test(description) { example.validate! }
23
+ end
24
+ end
25
+ end
26
+
27
+ module RSpec
28
+ include Common
29
+
30
+ def define_test(name, &block)
31
+ it(name, &block)
32
+ end
33
+ end
34
+
35
+ module TestUnit
36
+ include Common
37
+
38
+ def define_test(name, &block)
39
+ define_method("test_#{name.gsub(/\W+/, '_')}", &block)
40
+ end
41
+ end
42
+ end
43
+ end
44
+
@@ -0,0 +1,3 @@
1
+ module Interpol
2
+ VERSION = "0.0.1"
3
+ end
data/lib/interpol.rb ADDED
@@ -0,0 +1,12 @@
1
+ require "interpol/configuration"
2
+ require "interpol/version"
3
+
4
+ module Interpol
5
+ extend self
6
+
7
+ def default_configuration(&block)
8
+ block ||= lambda { |c| }
9
+ Configuration.default.tap(&block)
10
+ end
11
+ end
12
+
metadata ADDED
@@ -0,0 +1,149 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: interpol
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Myron Marston
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-04-04 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: sinatra
16
+ requirement: &2152119280 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 1.3.2
22
+ - - <
23
+ - !ruby/object:Gem::Version
24
+ version: 2.0.0
25
+ type: :runtime
26
+ prerelease: false
27
+ version_requirements: *2152119280
28
+ - !ruby/object:Gem::Dependency
29
+ name: json-schema
30
+ requirement: &2152116920 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ~>
34
+ - !ruby/object:Gem::Version
35
+ version: 1.0.5
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: *2152116920
39
+ - !ruby/object:Gem::Dependency
40
+ name: rspec
41
+ requirement: &2152114860 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ~>
45
+ - !ruby/object:Gem::Version
46
+ version: '2.9'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: *2152114860
50
+ - !ruby/object:Gem::Dependency
51
+ name: rspec-fire
52
+ requirement: &2152112940 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ~>
56
+ - !ruby/object:Gem::Version
57
+ version: '0.4'
58
+ type: :development
59
+ prerelease: false
60
+ version_requirements: *2152112940
61
+ - !ruby/object:Gem::Dependency
62
+ name: simplecov
63
+ requirement: &2152112120 !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '0.6'
69
+ type: :development
70
+ prerelease: false
71
+ version_requirements: *2152112120
72
+ - !ruby/object:Gem::Dependency
73
+ name: cane
74
+ requirement: &2152111100 !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ~>
78
+ - !ruby/object:Gem::Version
79
+ version: '1.2'
80
+ type: :development
81
+ prerelease: false
82
+ version_requirements: *2152111100
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: &2151941140 !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ~>
89
+ - !ruby/object:Gem::Version
90
+ version: 0.9.2.2
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: *2151941140
94
+ - !ruby/object:Gem::Dependency
95
+ name: rack-test
96
+ requirement: &2151939360 !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - =
100
+ - !ruby/object:Gem::Version
101
+ version: 0.6.1
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: *2151939360
105
+ description: Interpol is a toolkit for working with API endpoint definition files,
106
+ giving you a stub app, a schema validation middleware, and browsable documentation.
107
+ email:
108
+ - myron.marston@gmail.com
109
+ executables: []
110
+ extensions: []
111
+ extra_rdoc_files: []
112
+ files:
113
+ - README.md
114
+ - License
115
+ - Gemfile
116
+ - Rakefile
117
+ - lib/interpol/configuration.rb
118
+ - lib/interpol/endpoint.rb
119
+ - lib/interpol/errors.rb
120
+ - lib/interpol/response_schema_validator.rb
121
+ - lib/interpol/stub_app.rb
122
+ - lib/interpol/test_helper.rb
123
+ - lib/interpol/version.rb
124
+ - lib/interpol.rb
125
+ homepage: ''
126
+ licenses: []
127
+ post_install_message:
128
+ rdoc_options: []
129
+ require_paths:
130
+ - lib
131
+ required_ruby_version: !ruby/object:Gem::Requirement
132
+ none: false
133
+ requirements:
134
+ - - ! '>='
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ required_rubygems_version: !ruby/object:Gem::Requirement
138
+ none: false
139
+ requirements:
140
+ - - ! '>='
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ requirements: []
144
+ rubyforge_project:
145
+ rubygems_version: 1.8.15
146
+ signing_key:
147
+ specification_version: 3
148
+ summary: Interpol is a toolkit for policing your HTTP JSON interface.
149
+ test_files: []