rack-service_api_versioning 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rubocop.yml +14 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +105 -0
- data/Rakefile +60 -0
- data/bin/console +14 -0
- data/bin/setup +12 -0
- data/config.reek +21 -0
- data/doc/API-DOCUMENTATION.md +166 -0
- data/doc/CODE_OF_CONDUCT.md +74 -0
- data/doc/UBIQUITOUS-LANGUAGE.md +286 -0
- data/lib/rack/service_api_versioning/accept_content_type_selector.rb +78 -0
- data/lib/rack/service_api_versioning/api_version_redirector.rb +60 -0
- data/lib/rack/service_api_versioning/build_redirect_location_uri.rb +55 -0
- data/lib/rack/service_api_versioning/build_redirect_uri_from_env.rb +54 -0
- data/lib/rack/service_api_versioning/encoded_api_version_data/input_data.rb +45 -0
- data/lib/rack/service_api_versioning/encoded_api_version_data/invalid_base_url_error.rb +22 -0
- data/lib/rack/service_api_versioning/encoded_api_version_data/return_data.rb +44 -0
- data/lib/rack/service_api_versioning/encoded_api_version_data.rb +61 -0
- data/lib/rack/service_api_versioning/http_error_response.rb +29 -0
- data/lib/rack/service_api_versioning/input_env.rb +38 -0
- data/lib/rack/service_api_versioning/input_is_invalid.rb +36 -0
- data/lib/rack/service_api_versioning/match_header_against_api_versions.rb +55 -0
- data/lib/rack/service_api_versioning/report_invalid_description.rb +19 -0
- data/lib/rack/service_api_versioning/report_no_matching_version.rb +34 -0
- data/lib/rack/service_api_versioning/report_not_found.rb +18 -0
- data/lib/rack/service_api_versioning/service_component_describer/report_service_not_found.rb +44 -0
- data/lib/rack/service_api_versioning/service_component_describer.rb +68 -0
- data/lib/rack/service_api_versioning/version.rb +7 -0
- data/lib/rack/service_api_versioning.rb +14 -0
- data/lib/tasks/flog_task_patch.rb +12 -0
- data/rack-service_api_versioning.gemspec +62 -0
- data/tmp/gemsets/setup-and-bundle.sh +48 -0
- metadata +415 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: cd275478211642acf09b98289aeedc3ce4a2b646
|
4
|
+
data.tar.gz: 4677eda8a6576e3a9446b67ac9433d54cdcb95dd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a71438c1f5917dcbb8667365510454d3b6957652ba3cc08206737965e7a37dbac015b7a5aab0454b42a6db4227993d60cb12c95bbb39c800b4a3f90b6e0c2275
|
7
|
+
data.tar.gz: 303b94ad05bc34ab76f085fcaf3e3806c4871977db7dfab1f3b1549094a05bf894b534d7423d6eb668a06f0a404b2bd2e7549fc06d78b85532888ebe8601408a
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Jeff Dickey
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
<h1>Rack::ServiceApiVersioning</h1>
|
2
|
+
|
3
|
+
[![Join the chat at https://gitter.im/rack-service_api_versioning/Lobby](https://badges.gitter.im/rack-service_api_versioning/Lobby.svg)](https://gitter.im/rack-service_api_versioning/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
4
|
+
|
5
|
+
This Gem implements three Rack middleware components that, together, enable possibly multiple API Versions of one or more Component Services to be active at the same time. Incoming requests for a service specify their version requirements, if any, with an `Accept` HTTP header.
|
6
|
+
|
7
|
+
----
|
8
|
+
|
9
|
+
## Contents
|
10
|
+
|
11
|
+
- [A Note on Terminology](#a-note-on-terminology)
|
12
|
+
* [Ubiquitous Language](#ubiquitous-language)
|
13
|
+
* [Requirement-Level Keywords](#requirement-level-keywords)
|
14
|
+
- [Installation](#installation)
|
15
|
+
- [Usage](#usage)
|
16
|
+
* [An Overview of the Protocol](#an-overview-of-the-protocol)
|
17
|
+
* [API Documentation](#api-documentation)
|
18
|
+
- [Development](#development)
|
19
|
+
* [Prerequisites](#prerequisites)
|
20
|
+
* [Running Tests](#running-tests)
|
21
|
+
- [Contributing](#contributing)
|
22
|
+
- [License](#license)
|
23
|
+
|
24
|
+
## A Note on Terminology
|
25
|
+
|
26
|
+
### Ubiquitous Language
|
27
|
+
|
28
|
+
This Gem was developed to support a larger project involving a collection of separately packaged, independent Component Services communicating via HTTP, with any data transfer objects encoded as JSON. As such, these middleware components use a subset of that project's [Ubiquitous Langauge](https://martinfowler.com/bliki/UbiquitousLanguage.html), which is documented in the file [`UBIQUITOUS_LANGUAGE.md`](https://github.com/jdickey/rack-service_api_versioning/blob/master/UBIQUITOUS_LANGUAGE.md) in the `/doc` directory.
|
29
|
+
|
30
|
+
These terms, when used in this or other documents, can be identified as probable Ubiquitous Language terms by their use of initial capital letters, as demonstrated *by* the usage of *Ubiquitous Language* itself.
|
31
|
+
|
32
|
+
### Requirement-Level Keywords
|
33
|
+
|
34
|
+
Additionally, the keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://tools.ietf.org/html/rfc2119). These keywords **must** be styled in a **strong** ("bold") font face when used in this or other related documents; rendering them in grammatically-appropriate case rather than in ALL CAPS is a **recommended** variance from the RFC *unless* the author is certain that the audience will be viewing the content only as raw text, in which case the ALL CAPS styling is strongly **recommended.**
|
35
|
+
|
36
|
+
## Installation
|
37
|
+
|
38
|
+
The middleware components in this Gem are intended for use in an API Version-independent Delivery Application, or AVIDA. They will not normally be used by API Version implementations or by other applications not developed using this protocol for API Version disambiguation. Therefore, this Gem will ordinarily be added to the Gemfile of such an AVIDA, rather than installed in the system Gem repository.
|
39
|
+
|
40
|
+
Add this line to the Gemfile for an API Version-independent Delivery Application:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
gem 'rack-service_api_versioning'
|
44
|
+
```
|
45
|
+
|
46
|
+
And then execute:
|
47
|
+
|
48
|
+
$ bundle
|
49
|
+
|
50
|
+
## Usage
|
51
|
+
|
52
|
+
### An Overview of the Protocol
|
53
|
+
|
54
|
+
An application platform may be constructed of a number of separately-maintained components, including [use case or use story](https://martinfowler.com/bliki/UseCasesAndStories.html) implementations running as separate Primary Delivery Applications, or *PDAs.* Each of these is invoked by and interacts with other Component Services via HTTP, with data objects encoded using JSON. Each of these also is ordinarily versioned independently of others, which presents challenges when a Component Service and its PDA (collectively, a *Target Service*) are updated:
|
55
|
+
|
56
|
+
* How do its collaborators, which may be implemented and maintained by different teams, ensure that they collaborate only with a known-good version of the Target Service when the possibility exists that new versions may introduce breaking changes?
|
57
|
+
* How do new API Versions of the Target Service evolve and implement functionality, or even simple API changes, that introduce breaking changes without being hobbled by fealty to backwards compatibility?
|
58
|
+
* Given the above, how can multiple API Versions of a given Target Service be deployed *in the same system* to meet the needs of different clients which have not all updated to the latest version due to API changes?
|
59
|
+
* From an operational perspective, how can the system maintain adequate resilience if a newly-deployed API Version's PDA of a Service proves unreliable, yet all clients will happily work with previous API Versions if the new one is unavailable?
|
60
|
+
* How can network- and server-related issues such as failover or migration be dealt with while maintaining continuous availability of the larger system?
|
61
|
+
|
62
|
+
One solution is to define a single Service Base URL for each Component Service, with the AVIDA application accessible via that URL existing solely to generate HTTP redirects to the Service Base URL for the Primary Delivery Application of a given API Version. The AVIDA **must not** implement code to serve Service Endpoints itself, as they will never be accessed when using the middleware correctly. The middleware components get information about the currently-available API Versions by querying a [Repository](#the-repository); maintaining the correctness and currency of that data is outside the scope of this document (or this Gem).
|
63
|
+
|
64
|
+
In the accompanying [API Documentation](https://github.com/jdickey/rack-service_api_versioning/doc/API-DOCUMENTATION.md), we discuss the three artefacts directly involved with the use of the Rack middleware components in this Gem: the AVIDA (API Version-Independent Primary Delivery Application); the Repository containing information about currently available API Versions; and the API Implementation Primary Delivery Application (PDA).
|
65
|
+
|
66
|
+
### API Documentation
|
67
|
+
|
68
|
+
As noted above, the [API Documentation](https://github.com/jdickey/rack-service_api_versioning/doc/API-DOCUMENTATION.md) for the Rack middleware components implemented in this Gem, including overview and usage, is in a separate document.
|
69
|
+
|
70
|
+
## Development
|
71
|
+
|
72
|
+
After checking out the repo, run `bin/setup` to install dependencies (which as of now must already be installed on your local system). Then, run `bin/rake test` to run the tests, or `bin/rake` to run tests and, if tests are successful, further static-analysis tools ([RuboCop](https://github.com/bbatsov/rubocop), [Flay](https://github.com/seattlerb/flay), [Flog](https://github.com/seattlerb/flog), and [RubyCritic](https://github.com/whitesmith/rubycritic)).
|
73
|
+
|
74
|
+
To install *your build* of this Gem onto your local machine, run `bin/rake install`. We recommend that you uninstall any previously-installed "official" Gem to increase your confidence that your tests are running against *your* build. You should then be able to either run tests or test the middleware components from within your set of applications (AVIDA and PDA).
|
75
|
+
|
76
|
+
### Prerequisites
|
77
|
+
|
78
|
+
The development setup as automated by `bin/setup` assumes that
|
79
|
+
|
80
|
+
1. you're using [`rbenv`](https://github.com/rbenv/rbenv) for Ruby version management;
|
81
|
+
2. you have the [`rbenv-gemset`](https://github.com/jf/rbenv-gemset) plugin installed (see [here](https://gist.github.com/MicahElliott/2407918) for a quick setup HOWTO).
|
82
|
+
|
83
|
+
Gemsets make life easier, both by maintaining a pristine system Gem repository and by guaranteeing that a program can be rebuilt with the *exact same versions* of Gems as was used to build a specific commit. Our use of Gemsets, as shown in the [`gemsets/setup_and_bundle.sh`](https://github.com/jdickey/rack-service_api_versioning/tree/master/gemsets/setup_and_bundle.sh) file and the [gemspec](https://github.com/jdickey/rack-service_api_versioning/tree/master/rack-service_api_versioning.gemspec), can be seen as "imposing a burden" on maintainence by requiring that Gem version updates be made consistently in both files, but it more than compensates for that by ensuring that each Gem directly used by *our* Gem doesn't have any "stealth updates" applied against it that risk changing functionality.
|
84
|
+
|
85
|
+
### Running Tests
|
86
|
+
|
87
|
+
Running tests works just as you would expect for individual MiniTest::Spec test scripts; you can run a command line such as `ruby test/rack/service_api_versioning/service_component_describer_test.rb` to run a single test-spec file. Also, running `rake` and `rake test` works just as you'd expect for running the complete set of tests.
|
88
|
+
|
89
|
+
## Contributing
|
90
|
+
|
91
|
+
1. [Fork it](https://github.com/jdickey/rack-service_api_versioning/fork);
|
92
|
+
1. *Please* open an issue on this repo so we can discuss your feature. Features which reflect a consensus reached are much more likely to be merged quickly;
|
93
|
+
1. Create your feature branch (`git checkout -b NNN-my-new-feature`) where `NNN` is the issue number for the aforementioned discussion;
|
94
|
+
1. Ensure that your changes are completely covered by *passing* specs, and comply with the [Ruby Style Guide](https://github.com/bbatsov/ruby-style-guide) as enforced by [RuboCop](https://github.com/bbatsov/rubocop). To verify this, run `bundle exec rake`, noting and repairing any lapses in coverage or style violations;
|
95
|
+
1. Commit your changes (`git add .; git commit`). Please *do not* use a single-line commit message (`git commit -am "some message"`). A good commit message notes what was changed and why in sufficient detail that a relative newcomer to the code can understand your reasoning and your code. We **recommend** (but do not yet enforce) commit messages conforming to [these conventions](http://karma-runner.github.io/1.0/dev/git-commit-msg.html);
|
96
|
+
1. Push to the branch (`git push origin NNN-my-new-feature`). Remember that the first time pushing a branch to a remote requires an "unconditional" push (`git push -u origin NNN-my-new-feature`);
|
97
|
+
1. Create a new Pull Request. In the initial message, reference the open issue where your feature has been discussed; if no such issue exists (why?), then describe at some length the rationale for your new feature; your implementation strategy at a higher level than each individual commit message; anything future maintainers should be aware of; and so on. Modifications to existing code *must* have been discussed in an issue for PRs containing them to be accepted and merged;
|
98
|
+
1. Don't be discouraged if the PR generates further discussion leading to further refinement of your PR through additional commits. These should *generally* be discussed in comments on the relevant issue; discussion in the Gitter room (see below) may also be useful;
|
99
|
+
1. If you've comments, questions, or just want to talk through your ideas, come hang out in the project's [room on Gitter](https://gitter.im/rack-service_api_versioning). Ask away!
|
100
|
+
|
101
|
+
## License
|
102
|
+
|
103
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
104
|
+
|
105
|
+
Copyright © 2017, Jeff Dickey and Prolog Systems (Singapore) Private Limited.
|
data/Rakefile
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
require "rake/testtask"
|
4
|
+
|
5
|
+
require 'rake/tasklib'
|
6
|
+
require 'flay'
|
7
|
+
require 'flay_task'
|
8
|
+
require 'tasks/flog_task_patch'
|
9
|
+
require 'reek/rake/task'
|
10
|
+
require 'rubocop/rake_task'
|
11
|
+
# require 'rubycritic/rake_task'
|
12
|
+
|
13
|
+
Rake::TestTask.new(:test) do |t|
|
14
|
+
# t.libs << "test"
|
15
|
+
t.libs << "lib"
|
16
|
+
t.test_files = FileList['test/**/*_test.rb']
|
17
|
+
# NOTE: Silences "loading in progress, circular require considered harmful"
|
18
|
+
# (and any other warnings -- not spec failures -- from MiniTest). Try
|
19
|
+
# removing/uncommenting this after a minitest update from 5.10.1.
|
20
|
+
t.warning = false
|
21
|
+
end
|
22
|
+
|
23
|
+
RuboCop::RakeTask.new(:rubocop) do |task|
|
24
|
+
task.patterns = [
|
25
|
+
'lib/**/*.rb',
|
26
|
+
'test/**/*.rb'
|
27
|
+
]
|
28
|
+
task.formatters = ['simple', 'd']
|
29
|
+
task.fail_on_error = true
|
30
|
+
# task.options << '--rails'
|
31
|
+
task.options << '--display-cop-names'
|
32
|
+
end
|
33
|
+
|
34
|
+
Reek::Rake::Task.new do |t|
|
35
|
+
t.config_file = 'config.reek'
|
36
|
+
t.source_files = 'lib/**/*.rb'
|
37
|
+
t.reek_opts = '--sort-by smelliness --no-progress -s'
|
38
|
+
end
|
39
|
+
|
40
|
+
FlayTask.new do |t|
|
41
|
+
t.verbose = true
|
42
|
+
t.dirs = %w(lib)
|
43
|
+
end
|
44
|
+
|
45
|
+
FlogTask.new do |t|
|
46
|
+
t.verbose = true
|
47
|
+
t.threshold = 300 # default is 200
|
48
|
+
t.methods_only = true
|
49
|
+
t.dirs = %w(lib) # Look, Ma; no tests! Run the tool manually every so often for those.
|
50
|
+
end
|
51
|
+
|
52
|
+
# # NOTE: We still want to keep the `config.reek` file, since RubyCritic uses Reek.
|
53
|
+
# # Also note that tests give craptastic scores, hence now skipped. :grimacing:
|
54
|
+
# RubyCritic::RakeTask.new do |t|
|
55
|
+
# t.options = '-f console'
|
56
|
+
# t.paths = %w(lib)
|
57
|
+
# end
|
58
|
+
|
59
|
+
task(:default).clear
|
60
|
+
task default: [:test, :rubocop, :flay, :flog, :reek]
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "rack/service_api_versioning"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/config.reek
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
# NOTE: Even if we're using RubyCritic "instead of" directly invoking Reek, be
|
3
|
+
# aware that RubyCritic *invokes* Reek. Hence, this file should remain
|
4
|
+
# where it is.
|
5
|
+
|
6
|
+
# This tells Reek not to complain about module names such as `FooCsV42`, which
|
7
|
+
# our conventions say implements the Foo Component Service, API Version 42.
|
8
|
+
UncommunicativeModuleName:
|
9
|
+
accept:
|
10
|
+
- !ruby/regexp /CsV\d+?$/
|
11
|
+
- !ruby/regexp /UcV\d+?$/
|
12
|
+
|
13
|
+
# Usual suspects copied from previous projects. Commented out until needed.
|
14
|
+
|
15
|
+
# LongParameterList:
|
16
|
+
# max_params: 4 # If it's good enough for Sandi, it's good enough for us.
|
17
|
+
|
18
|
+
# NestedIterators:
|
19
|
+
# max_allowed_nesting: 2
|
20
|
+
# ignore_iterators:
|
21
|
+
# - lambda
|
@@ -0,0 +1,166 @@
|
|
1
|
+
<h1>Contents</h1>
|
2
|
+
|
3
|
+
# Contents
|
4
|
+
|
5
|
+
- [Before We Get Started](#before-we-get-started)
|
6
|
+
* [A Note on Terminology](#a-note-on-terminology)
|
7
|
+
* [FAQ for This Document](#faq-for-this-document)
|
8
|
+
- [Setup in the AVIDA](#setup-in-the-avida)
|
9
|
+
- [The `ServiceComponentDescriber` Middleware Component](#the-servicecomponentdescriber-middleware-component)
|
10
|
+
* [The Repository](#the-repository)
|
11
|
+
+ [Repository Value Schemata](#repository-value-schemata)
|
12
|
+
* [Error Reporting](#error-reporting)
|
13
|
+
- [The `AcceptContentTypeSelector` Middleware Component](#the-acceptcontenttypeselector-middleware-component)
|
14
|
+
* [Inputs from Rack Environment](#inputs-from-rack-environment)
|
15
|
+
* [Error Reporting](#error-reporting-1)
|
16
|
+
- [The `ApiVersionRedirector` Middleware Component](#the-apiversionredirector-middleware-component)
|
17
|
+
* [Error Reporting](#error-reporting-2)
|
18
|
+
- [Feasible Future Features](#feasible-future-features)
|
19
|
+
* [Other Ideas?](#other-ideas)
|
20
|
+
|
21
|
+
# Before We Get Started
|
22
|
+
|
23
|
+
## A Note on Terminology
|
24
|
+
|
25
|
+
The section [*A Note on Terminology*](https://github.com/jdickey/rack-service_api_versioning/README.md#a-note-on-terminology), including description of this project's [Ubiquitous Langauge](https://github.com/jdickey/rack-service_api_versioning/doc/UBIQUITOUS-LANGUAGE.md) and [Requirement-Level Keywords](https://github.com/jdickey/rack-service_api_versioning/README.md#requirement-level-keywords), is incorporated herein by reference.
|
26
|
+
|
27
|
+
## FAQ for This Document
|
28
|
+
|
29
|
+
<dl>
|
30
|
+
<dt>Why the Funky Header for "Contents"?</dt>
|
31
|
+
<dd>We use the [`markdown-toc`](https://github.com/nok/markdown-toc) Node (and Atom) package to generate the table of contents. That package understands Markdown syntax for headers; it does not fully comprehend that Markdown is a proper superset of HTML, and so HTML headers are valid, too. Since we don't want the "Contents" header itself to appear in the TOC, using the HTML markup gives the desired result.
|
32
|
+
</dd>
|
33
|
+
</dl>
|
34
|
+
|
35
|
+
# Setup in the AVIDA
|
36
|
+
|
37
|
+
A typical AVIDA might read something like the following:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
|
41
|
+
require 'awesome_print'
|
42
|
+
|
43
|
+
require_relative './repository'
|
44
|
+
|
45
|
+
# Code for Acme Apidemo Service Component, API Version `v1`, namespaced in this module.
|
46
|
+
module AcmeApiDemoV1
|
47
|
+
# Roda/Rack app to serve as API Demo Component Service delivery mechanism.
|
48
|
+
# Remember that Roda's convention is to set `response.status` to 200 by
|
49
|
+
# default, which is just fine for most cases.
|
50
|
+
# Reek complains about a :reek:UncommunicativeVariableName. Ah, convention.
|
51
|
+
class ServiceApp < Roda
|
52
|
+
use Rack::Session::Cookie, secret: ENV['ACME_APIDEMO_SESSION_COOKIE_SECRET']
|
53
|
+
plugin :default_headers, 'Content-Type' => 'application/json'
|
54
|
+
use ServiceComponentDescriber, repository: DummyRepository.new,
|
55
|
+
service_name: 'apidemo'
|
56
|
+
use AcceptContentTypeSelector
|
57
|
+
use ApiVersionRedirector
|
58
|
+
|
59
|
+
route do |r|
|
60
|
+
r.post 'register' do
|
61
|
+
'Hello from #register. I MUST NOT be shown. params: ' + r.params.ai
|
62
|
+
end
|
63
|
+
end # route
|
64
|
+
end # class ServiceCatalogueUcV1::ServiceApp
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
Note that, although the demo code above uses (the underappreciated, awesome) [Roda](http://roda.jeremyevans.net) framework, the middleware works with any framework built on [Rack](http://rack.github.io); this includes Rails, Sinatra, [Brooklyn](https://github.com/luislavena/brooklyn), or [anything else](http://codecondo.com/12-small-ruby-frameworks/) that runs on top of Rack.
|
69
|
+
|
70
|
+
What's important is the use of the three middleware components `ServiceComponentDescriber`, `AcceptContentTypeSelector`, and `ApiVersionRedirector`; their ordering in the main application module *prior to* any routing or other application logic; and the parameters (for `ServiceComponentDescriber`). Each will be discussed in turn below.
|
71
|
+
|
72
|
+
# The `ServiceComponentDescriber` Middleware Component
|
73
|
+
|
74
|
+
The `ServiceComponentDescriber` middleware component is the first of our three middleware components to be used in an AVIDA for a Component Service. It **must** be invoked with parameters for `repository` and `service_name`. These parameters **may** be in either order.
|
75
|
+
|
76
|
+
The component retrieves information concerning the implementation(s) of a specific named Component Service from a Repository whose data has been provided by an external service, formats that data into a JSON string which it uses to set a value in the Rack environment (using the key `'COMPONENT_DESCRIPTION'`), and then passes that environment on to the next link in the Rack call chain (which in practice **should** be the next middleware component).
|
77
|
+
|
78
|
+
## The Repository
|
79
|
+
|
80
|
+
The Repository is an object which returns an array of zero or more entities describing Component Services and their API Versions asserted to be presently available for use by external clients via HTTP.
|
81
|
+
|
82
|
+
The Repository is queried via its `#find` method. The method takes as its parameter a Hash of entity attribute/value pairs, the only one of which that is guaranteed to be significant here being `:name`, which is matched against the short `:name` of available entities (see the next paragraph). The `#find` method returns an array of entities, which will be empty if no matches were found.
|
83
|
+
|
84
|
+
### Repository Value Schemata
|
85
|
+
|
86
|
+
Each entity returned from `#find` **must** have the following attributes:
|
87
|
+
|
88
|
+
| Attribute | Type | Description | Example |
|
89
|
+
| --------- | ---- | ----------- | ------- |
|
90
|
+
| `name` | string | Short, unique name of a single Component Service | `apidemo` |
|
91
|
+
| `description` | string | Non-empty descriptive text for the Component Service | `The API Demo Component Service` |
|
92
|
+
| `api_versions` | array of object | Array of one or more API Version descriptor entities (see table below) |
|
93
|
+
|
94
|
+
The Repository **must** return one or more API Version descriptor entities in the `api_versions` attribute above. In the event that a Component Service with the requested name nominally exists but has no API Versions preferably available, the Repository's `#find` method **must** return an **empty* array result.
|
95
|
+
|
96
|
+
Each API Version descriptor entities **must** have attributes as follows:
|
97
|
+
|
98
|
+
| Attribute | Type | Description | Example |
|
99
|
+
| --------- | ---- | ----------- | ------- |
|
100
|
+
| `base_url` | string | Service Base URL for this specific API Version of this specific Component Service. | `http://example.com:9876/api/` |
|
101
|
+
| `content_type` | string | MIME content type sent in `Accept` header to explicitly specify this specific API Version of this specific Component Service. | `application/vnd.examplecorp.apidemo.v1+json` |
|
102
|
+
| `restricted` | Boolean | Reserved for future use. Must be `false`. | `false` |
|
103
|
+
| `deprecated` | Boolean | Reserved for future use. Must be `false`. | `false` |
|
104
|
+
|
105
|
+
## Error Reporting
|
106
|
+
|
107
|
+
If the attempt to retrieve information from the Repository using the value passed in the `service_name` parameter is unsuccessful, then the `ServiceComponentDescriber` will abort the Rack request processing, returning an HTTP status code [404](https://httpstatuses.com/404) (*Not Found*), with a response body that simply contains the text, *Service not found: "bad-service-name"*.
|
108
|
+
|
109
|
+
# The `AcceptContentTypeSelector` Middleware Component
|
110
|
+
|
111
|
+
The `AcceptContentTypeSelector` middleware component
|
112
|
+
|
113
|
+
1. parses the JSON encoded into the `COMPONENT_DESCRIPTION` value by the `ServiceComponentDescriber` middleware component;
|
114
|
+
2. parses and interprets the requested API Version as specified by the Content Type specified in the `Accept` HTTP header (available in the Rack environment at `HTTP_ACCEPT`);
|
115
|
+
1. if a suitable API Version is identified, encodes that API Version's details into a new `COMPONENT_API_VERSION_DATA` entry in the Rack environment;
|
116
|
+
2. if no suitable API Version is identified, returns a Rack response with status code [406](https://httpstatuses.com/406) (*Not Acceptable*), and a message body enumerating the Content Type values which would have resulted in a successful request for the Component Service in question;
|
117
|
+
3. unless aborted with an error, proceeds on to the next component in the Rack middleware chain.
|
118
|
+
|
119
|
+
## Inputs from Rack Environment
|
120
|
+
|
121
|
+
The `AcceptContentTypeSelector` middleware component requires two entries to be set in the Rack environment (the `env` passed into its `#call` method).
|
122
|
+
|
123
|
+
The `COMPONENT_DESCRIPTION` value contains a JSON-serialised object describing a Component Service and the API Versions presently operational and available for that Service. This is ordinarily set by the [`ServiceComponentDescriber`](#the-servicecomponentdescriber-middleware-component) component described above.
|
124
|
+
|
125
|
+
The `HTTP_ACCEPT` value represents the standard `Accept` header used for HTTP content negotiation. It will normally have one or more segments of the format
|
126
|
+
|
127
|
+
> `application/vnd.COMPANYORORG.APINAME.vSTR+json`
|
128
|
+
|
129
|
+
where
|
130
|
+
|
131
|
+
* `vnd` is a conventional abbreviation for "vendor"; i.e., for an application content type that is not part of the HTTP or related IETF Standards;
|
132
|
+
* `COMPANYORORG` is the name of the company or organisation responsible for maintaining the application on whose behalf the Content Type is used. In our documentation for this gem, we have been using the example `acme`, for [Acme Corporation](https://en.wikipedia.org/wiki/Acme_Corporation);
|
133
|
+
* `APINAME` is the name of the application programming interface (or *API*) which these middleware components are being used to support. In the documentation for this Gem, we have been using the example `apidemo`, for the *API Demo Component Service*; and
|
134
|
+
* `STR` is an API-unique version identifier. Conventionally, and as demonstrated in this documentation, this has been an integer (which would presumably increment for each successive API Version release), giving an example such as `v1` or `v472`. In practice, it can be virtually *any* string-representable application-unique identifier; for those using [Semantic Versioning](http://semver.org), you might have an example such as `v1.0.0` or `v42.6.4-pre71`. As long as the version identifier is meaningful to you and your development team, it should be useable here.
|
135
|
+
|
136
|
+
|
137
|
+
## Error Reporting
|
138
|
+
|
139
|
+
The middleware component will abort processing of the request and return an HTTP error under any of the following conditions:
|
140
|
+
|
141
|
+
* An HTTP [400](https://httpstatuses.com/400) (*Bad Request*) will be returned if there is no defined `COMPONENT_DESCRIPTION` value or if that value does not contain valid [Repository](#the-repository) data in JSON format with at least one API Version defined; or
|
142
|
+
* An HTTP [406](https://httpstatuses.com/406) (*Not Acceptable*) will be returned if the API Version specifier in the `HTTP_ACCEPT` environment value does not match any API Versions reported as supported by parsing the `COMPONENT_DESCRIPTION` environment value.
|
143
|
+
|
144
|
+
# The `ApiVersionRedirector` Middleware Component
|
145
|
+
|
146
|
+
The `ApiVersionRedirector` middleware component parses the JSON encoded in the `COMPONENT_API_VERSION_DATA` value by the `AcceptContentTypeSelector` middleware component. It then builds a Rack response with
|
147
|
+
|
148
|
+
* the status code [307](https://httpstatuses.com/307) (*Temporary Redirect*);
|
149
|
+
* body content containing the markup `Please resend the request to <a href="LOCATION">LOCATION</a> without caching it.`, where `LOCATION` is replaced by the value of the `Location` header (see the next item); and
|
150
|
+
* headers for
|
151
|
+
* `API-Version`, with a value of the API Version used to match the request (e.g., `v1` or `v2.14.6`); and
|
152
|
+
* `Location`, with a value of the full URL for the API Version-specific request as supplied to the AVIDA, including path information and query parameters, if any.
|
153
|
+
|
154
|
+
## Error Reporting
|
155
|
+
|
156
|
+
**None.** If the `COMPONENT_DESCRIPTION` entry in the Rack environment is missing, or is invalidly formatted, then this middleware component *will* fail. Adding error detection and reporting similar to that of [`AcceptContentTypeSelector`](#the-servicecomponentdescriber-middleware-component), above, *but* the question may reasonably be asked, *how useful would that be in practice?* If these three components are always used together in the correct sequence, then there should be no possible error path for this middleware component; if an error is encountered in operation, that is a strong indication that the *use* of the middleware in that particular AVIDA is incorrect.
|
157
|
+
|
158
|
+
# Feasible Future Features
|
159
|
+
|
160
|
+
1. The initial release of this Gem itself uses no encryption; if HTTPS rather than HTTP is used between Component Services, that would provide an increased level of security. HTTPS, however, is not presently **required,** but is **recommended.** An imminent future release is being considered which would use the [RbNaCl](https://github.com/cryptosphere/rbnacl) library's support for [public-key encryption](https://github.com/cryptosphere/rbnacl/wiki/SimpleBox#public-key-encryption-with-simplebox) to secure and authenticate HTTP payloads and, where practical, message data.
|
161
|
+
2. Despite the explanation given for the deliberate omission of error reporting in the `ApiVersionRedirector` middleware component (immediately above), some intrepid soul may choose to implement it anyway. (It's open source; it's a platform for learning experiences.)
|
162
|
+
3. Some misadventurous developer may choose to implement the three existing middleware components *as a single, unified component.* We considered that approach during initial development, and abandoned it because we strongly feel that the "boilerplate" of including two "extra" middleware components is *far* outweighed by the inner complexity that such a unified component would contain, and the likelihood that any future change would have effects beyond the intended change. ([SOLID](https://en.wikipedia.org/wiki/SOLID_(object-oriented_design)) *is* a thing, you know.)
|
163
|
+
|
164
|
+
## Other Ideas?
|
165
|
+
|
166
|
+
Do you see something we missed that you'd find useful? Open an [issue and PR](https://github.com/jdickey/rack-service_api_versioning/#contributing) and let's have a chat about it!
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
10
|
+
orientation.
|
11
|
+
|
12
|
+
## Our Standards
|
13
|
+
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
15
|
+
include:
|
16
|
+
|
17
|
+
* Using welcoming and inclusive language
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
19
|
+
* Gracefully accepting constructive criticism
|
20
|
+
* Focusing on what is best for the community
|
21
|
+
* Showing empathy towards other community members
|
22
|
+
|
23
|
+
Examples of unacceptable behavior by participants include:
|
24
|
+
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
26
|
+
advances
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
28
|
+
* Public or private harassment
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
30
|
+
address, without explicit permission
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
32
|
+
professional setting
|
33
|
+
|
34
|
+
## Our Responsibilities
|
35
|
+
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
38
|
+
response to any instances of unacceptable behavior.
|
39
|
+
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
44
|
+
threatening, offensive, or harmful.
|
45
|
+
|
46
|
+
## Scope
|
47
|
+
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
49
|
+
when an individual is representing the project or its community. Examples of
|
50
|
+
representing a project or community include using an official project e-mail
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
53
|
+
further defined and clarified by project maintainers.
|
54
|
+
|
55
|
+
## Enforcement
|
56
|
+
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
58
|
+
reported by contacting the project team at jdickey@seven-sigma.com. All
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
63
|
+
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
66
|
+
members of the project's leadership.
|
67
|
+
|
68
|
+
## Attribution
|
69
|
+
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
71
|
+
available at [http://contributor-covenant.org/version/1/4][version]
|
72
|
+
|
73
|
+
[homepage]: http://contributor-covenant.org
|
74
|
+
[version]: http://contributor-covenant.org/version/1/4/
|