rack-service_api_versioning 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.rubocop.yml +14 -0
  4. data/.travis.yml +5 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +105 -0
  8. data/Rakefile +60 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +12 -0
  11. data/config.reek +21 -0
  12. data/doc/API-DOCUMENTATION.md +166 -0
  13. data/doc/CODE_OF_CONDUCT.md +74 -0
  14. data/doc/UBIQUITOUS-LANGUAGE.md +286 -0
  15. data/lib/rack/service_api_versioning/accept_content_type_selector.rb +78 -0
  16. data/lib/rack/service_api_versioning/api_version_redirector.rb +60 -0
  17. data/lib/rack/service_api_versioning/build_redirect_location_uri.rb +55 -0
  18. data/lib/rack/service_api_versioning/build_redirect_uri_from_env.rb +54 -0
  19. data/lib/rack/service_api_versioning/encoded_api_version_data/input_data.rb +45 -0
  20. data/lib/rack/service_api_versioning/encoded_api_version_data/invalid_base_url_error.rb +22 -0
  21. data/lib/rack/service_api_versioning/encoded_api_version_data/return_data.rb +44 -0
  22. data/lib/rack/service_api_versioning/encoded_api_version_data.rb +61 -0
  23. data/lib/rack/service_api_versioning/http_error_response.rb +29 -0
  24. data/lib/rack/service_api_versioning/input_env.rb +38 -0
  25. data/lib/rack/service_api_versioning/input_is_invalid.rb +36 -0
  26. data/lib/rack/service_api_versioning/match_header_against_api_versions.rb +55 -0
  27. data/lib/rack/service_api_versioning/report_invalid_description.rb +19 -0
  28. data/lib/rack/service_api_versioning/report_no_matching_version.rb +34 -0
  29. data/lib/rack/service_api_versioning/report_not_found.rb +18 -0
  30. data/lib/rack/service_api_versioning/service_component_describer/report_service_not_found.rb +44 -0
  31. data/lib/rack/service_api_versioning/service_component_describer.rb +68 -0
  32. data/lib/rack/service_api_versioning/version.rb +7 -0
  33. data/lib/rack/service_api_versioning.rb +14 -0
  34. data/lib/tasks/flog_task_patch.rb +12 -0
  35. data/rack-service_api_versioning.gemspec +62 -0
  36. data/tmp/gemsets/setup-and-bundle.sh +48 -0
  37. 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
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.rbenv-gemsets
3
+ /.yardoc
4
+ /Gemfile.lock
5
+ /_yardoc/
6
+ /coverage/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /o-rdoc/
11
+ /bin/
12
+ !/bin/setup
13
+ .ruby-version
data/.rubocop.yml ADDED
@@ -0,0 +1,14 @@
1
+
2
+ AllCops:
3
+ TargetRubyVersion: 2.3
4
+ Exclude:
5
+ - 'gemsets/**/*'
6
+ - 'bin/**/*'
7
+ - '*.gemspec'
8
+ - 'Rakefile'
9
+ - 'Guardfile'
10
+
11
+ Metrics/BlockLength:
12
+ Max: 800
13
+ Include:
14
+ - 'test/**/*_test.rb'
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.3
5
+ before_install: gem install bundler -v 1.14.4
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rack-service_api_versioning.gemspec
4
+ gemspec
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 in­stalled 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 recom­mend that you uninstall any previously-installed "official" Gem to increase your confi­dence 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 &copy; 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
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env zsh
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ # set -vx
5
+
6
+ rm -f Gemfile.lock
7
+
8
+ # bundle install
9
+ source tmp/gemsets/setup-and-bundle.sh
10
+
11
+ bundle binstub --force flay flog guard pry rake reek rubocop rubycritic
12
+ echo "Setup completed."
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/