interpol 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []