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
@@ -0,0 +1,286 @@
1
+ # Ubiquitous Language
2
+
3
+ Every application and system incorporates some amount and type of *domain knowledge*, originally expressed in terminology familiar to domain experts as having specific meanings that are often meant to be an unambiguous shorthand for broader concepts. Too often, development teams implementing these software artefacts use different, less precise and/or specific terms, the variations between which introduce confusion between [Stakeholders](#stakeholder). Having an evolving but precisely-defined, uniformly-shared, yet evolving [ubiquitous language](http://blog.carbonfive.com/2016/10/04/ubiquitous-language-the-joy-of-naming/) eases development and improves communication by ensuring that "everybody means what they say they mean", preventing miscommunication and ambiguity from introducing defects and delays into what is already an imperfect, probably late, system.
4
+
5
+ The Ubiquitous Language (UL) for `Rack::ServiceApiVersioning` **must** define *all* terms used in specification, design, code comments, or other communication between [Stakeholders](#stakeholder) (possibly between a given Stakeholder and his or her future self) which lack, or differ from, a single, specific definition in standard technical written English. *This includes* terms that are primarily used in a technical context where the meaning of those terms is ordinarily open to multiple interpretations or meanings, as demonstrated by perusing the industry technical literature.
6
+
7
+ The Ubiquitous Language for `Rack::ServiceApiVersioning` **must** be compatible with, and is presumed to be a proper subset of, the UL for any application and/or [Platform](#platform) that uses it. Deviations from this are highly likely to cause miscommunication and misunderstanding due to ambiguity during the development of that larger application/Platform.
8
+
9
+ ----
10
+
11
+ ## Contents
12
+
13
+ - [The Rules](#the-rules)
14
+ - [Capitalisation](#capitalisation)
15
+ - [Exception for Requirement Level Keywords (RFC 2119)](#exception-for-requirement-level-keywords-rfc-2119)
16
+ - [Additional Considerations for Requirement Level Keywords (RFC 2119)](#additional-considerations-for-requirement-level-keywords-rfc-2119)
17
+ - [Common Words in Technical English](#common-words-in-technical-english)
18
+ - [Non-Normative Supplemental Information](#non-normative-supplemental-information)
19
+ - [Use of Temporary Placeholders](#use-of-temporary-placeholders)
20
+ - [Term Context to be Reflected in Definition](#term-context-to-be-reflected-in-definition)
21
+ - [Basic Policy](#basic-policy)
22
+ - [Terms that Differ Between Contexts](#terms-that-differ-between-contexts)
23
+ - [Highlighting Context with Multiple Definitions](#highlighting-context-with-multiple-definitions)
24
+ - [Ordering of Multiple Contexts](#ordering-of-multiple-contexts)
25
+ - [Meaning of Term Affected By Other Contexts](#meaning-of-term-affected-by-other-contexts)
26
+ - [Term Closely Tied to Other Term(s)](#term-closely-tied-to-other-terms)
27
+ - [Transitional Meaning of a Term](#transitional-meaning-of-a-term)
28
+ - [Use of Term as Synonym (Cross-Reference)](#use-of-term-as-synonym-cross-reference)
29
+ - [Link Formation](#link-formation)
30
+ - [Link on First Use in a Paragraph](#link-on-first-use-in-a-paragraph)
31
+ - [Optional Abbreviation After First Use in a Paragraph](#optional-abbreviation-after-first-use-in-a-paragraph)
32
+ - [Use Non-Abbreviated Form on First Use in Subsequent Paragraph](#use-non-abbreviated-form-on-first-use-in-subsequent-paragraph)
33
+ - [The List](#the-list)
34
+ - [Accept](#accept)
35
+ - [API Version](#api-version)
36
+ - [AVIDA](#avida)
37
+ - [API Version-Independent Delivery Application](#api-version-independent-delivery-application)
38
+ - [Component Service](#component-service)
39
+ - [Content Negotiation](#content-negotiation)
40
+ - [Deprecate](#deprecate)
41
+ - [Endpoint](#endpoint)
42
+ - [Entity](#entity)
43
+ - [Glossary](#glossary)
44
+ - [PDA](#pda)
45
+ - [Platform](#platform)
46
+ - [Primary Delivery Application](#primary-delivery-application)
47
+ - [Register](#register)
48
+ - [Request](#request)
49
+ - [Repository](#request)
50
+ - [Service](#service)
51
+ - [Service Base URL](#service-base-url)
52
+ - [Service Description](#service-description)
53
+ - [Service Endpoint](#service-endpoint)
54
+ - [Specification](#specification)
55
+ - [Stakeholder](#stakeholder)
56
+ - [Target Service](#target-service)
57
+ - [Value Object](#value-object)
58
+ - [Yank](#yank)
59
+
60
+ ----
61
+
62
+ ## The Rules
63
+
64
+ ### Capitalisation
65
+
66
+ Ubiquitous Language terms **must** always have each word capitalised (e.g., Member Name). The definition **must** be in the singular for nouns, and third person simple present usage for verbs. Usage may be modified according to the rules of standard English as is normal for nouns (e.g., [Stakeholders](#stakeholder) is a collection of possibly multiple individuals, each of whom is a Stakeholder), verbs (e.g., [Requested](#request) as the simple past tense of Request), and so on. Such usage highlights that a Ubiquitous Language term is being discussed which has a meaning distinct from any it may have in standard English.
67
+
68
+ #### Exception for Requirement Level Keywords (RFC 2119)
69
+
70
+ Words defined in [RFC 2119](http://www.faqs.org/rfcs/rfc2119.html) as specifying interpretation of requirement levels, such as **must**, **may**, and **should**, **should not** be capitalised but instead rendered in **boldface**, following industry custom.
71
+
72
+ ### Additional Considerations for Requirement Level Keywords (RFC 2119)
73
+
74
+ Words defined in [RFC 2119](http://www.faqs.org/rfcs/rfc2119.html) as specifying interpretation of requirement levels within a specification **should not** be linked to entries in this [Glossary](#glossary). Rather, the opening sections of the Specification or other document in question should incorporate the phrase recommended by the RFC, as follows (reformatted from the original):
75
+
76
+ > The key words **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](http://www.faqs.org/rfcs/rfc2119.html).
77
+
78
+ ### Common Words in Technical English
79
+
80
+ Words that occur and are used in the same sense as in the general technical literature, such as *device* or *text,* are not part of the Ubiquitous Language. If a specific context would be served by applying a more specific or variant definition of a term, it should be consistently distinguished from the generic term. Hence, *device* as opposed to a hypothetical *Frobulation Device*.
81
+
82
+ ### Non-Normative Supplemental Information
83
+
84
+ Paragraphs in the definition of a term **may** be added that convey (primarily out-of-domain) additional supplemental information relevant to a term. These paragraphs **must** be added to the end of the discussion of a term, and **must** be preceded by a paragraph consisting solely of the text *Non-normative supplemental information follows.*
85
+
86
+ Such non-normative supplemental information **may** omit links to definitions of terms used that were linked to in the earlier,l normative section of the term definition.
87
+
88
+ ### Use of Temporary Placeholders
89
+
90
+ As the Ubiquitous Language evolves and grows, and is recorded in this [Glossary](#glossary), a new definition may be added which references other terms not yet having Glossary entries. When circulating the draft Glossary to others for review and comment, those referenced entries **should** be created and linked to in the new entry. The Glossary maintainer **should** complete those definitions as well, but **may** instead choose to create an entry with the text "TBD." as the complete content. This is an indication to other reviewers that the term so listed is known to be relevant but has not yet been defined in the Glossary.
91
+
92
+ All [Glossary](#glossary) definitions relevant to a given artefact (and to those terms' definitions, and so on) **must** be defined in the Glossary before a Specification for that artefact is completed or meaningful code developed.
93
+
94
+ ### Term Context to be Reflected in Definition
95
+
96
+ #### Basic Policy
97
+
98
+ Since the Ubiquitous Language includes terms that are defined in a domain context, a technical context, or both, each term **must** indicate at the beginning of its definition, the phrase *Domain term.* for domain terms or *Technical term.* for terms which are not relevant to a domain expert but have a specific meaning to technical [Stakeholders](#stakeholder).
99
+
100
+ #### Terms that Differ Between Contexts
101
+
102
+ Terms that have importantly different meanings between contexts should be defined sequentially under the same term, with the domain context first.
103
+
104
+ ##### Highlighting Context with Multiple Definitions
105
+
106
+ When a term has definitions supplied for both contexts, *and* one of the contexts includes definitions for both a noun and a verb, then that context **must** be listed on a line by itself, as in the above example. If the domain context has definitions of both a noun and a verb, then both contexts **must** be listed on separate lines.
107
+
108
+ ##### Ordering of Multiple Contexts
109
+
110
+ When domain-context and technical-context definitions are supplied for the same term, the domain-context definition **must** always be first.
111
+
112
+ ##### Meaning of Term Affected By Other Contexts
113
+
114
+ When a term is *primarily* relevant in one context, but changes in the understanding of another may affect that meaning, then the context phrase definition **must** include the word *Primarily*.
115
+
116
+ If more than two contexts have been defined within this glossary, then the other affecting contexts **must** also be listed, e.g., "*Primarily technical term; affected by pseudo-financial context.*" This serves as a warning that changes in the listed secondary context(s) could in future affect the primary definition of the term.
117
+
118
+ ### Term Closely Tied to Other Term(s)
119
+
120
+ When a [Glossary](#glossary) term is closely conceptually dependent on one or more others, then that linkage **must** be indicated by a phrase equivalent to "*With reference to (other term),*" at the start of the dependent term's definition. For an example of this, see [Restricted](#restricted).
121
+
122
+ ### Transitional Meaning of a Term
123
+
124
+ The Ubiquitous Language *will* evolve as the application it defines evolves over its lifetime. However, some terms are considered more likely to change than others, often because their definition and use are tightly bound to business policies and rules more than to intrinsic domain terminology. The definition of these terms **must** include the *either* word **currently** *or* the word **initially**, modified for correct grammar.
125
+
126
+ For an example of how this is used, see the definition of [Restricted](#restricted).
127
+
128
+ ### Use of Term as Synonym (Cross-Reference)
129
+
130
+ When a term has a commonly-used synonym that has identical meaning to the original in all relevant contexts, then the full definition **must** be listed under what is, or is expected to be, the most commonly-used form of the term. Synonymous entries are permitted, as with the example shown below for [Authorisation Role](#authorisation-role), a presently-punctilious more specific term for [Role](#role).
131
+
132
+ ### Link Formation
133
+
134
+ A Ubiquitous Language term definition, when linked to within hypertext markup such as [Component Service](#component-service) or email **must** be formed using a normal link (e.g., HTML `<a></a>` tag pair) with the properly-capitalised Ubiquitous Language term as the link text and a URL which references the specific definition on this page. For example, a reference on this page to the term [Component Services](#component-service) would be rendered in Markdown as `[Component Services](#component-service)`. Note that, as mentioned previously, the term *in the URL* is in the singular, and words are separated within the URL by hyphen (`-`) characters rather than underscores.
135
+
136
+ ### Link on First Use in a Paragraph
137
+
138
+ The first use of a Ubiquitous Language term in a paragraph within hypertext markup such as specifications or email **should** link to its definition in this [Glossary](#glossary) **unless** that definition has been linked to in a "recent" paragraph within the markup *and* the term may reasonably be presumed to be understood in specific detail by the reader. An example of such a term might be Member Name. Document authors **should** exercise judicious restraint in considering exceptions, and readers who feel that unwarranted exceptions have been made **should** so inform the author.
139
+
140
+ ### Optional Abbreviation After First Use in a Paragraph
141
+
142
+ Ubiquitous Language terms **may** be abbreviated after their first use in a paragraph. where the first use calls attention to the fact that future abbreviations are being used informally.
143
+
144
+ ### Use Non-Abbreviated Form on First Use in Subsequent Paragraph
145
+
146
+ The Ubiquitous Language term previously abbreviated is normally fully expanded for its first use in any subsequent paragraph.
147
+
148
+ ------
149
+
150
+ ## The List
151
+
152
+ ### Accept
153
+
154
+ *Primarily technical term.* The [`Accept` header](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html) in the standard HTTP request headers is the basis of standard HTTP [content negotiation](#content-negotiiation). In our usage, [Clients](#client) making [Requests](#request) to a [Component Service](#component-service) **must** specify an `Accept` header with at least one content type specifying *either* that a specific [API Version](#api-version) of a specific Component Service is required (e.g., with a value of `vnd.example.apidemo.v4`) *or* that the latest available API Version of that Component Service is acceptable (with a comparable example of `vnd.example.apidemo`).
155
+
156
+ ### API Version
157
+
158
+ *Technical term.* A sequential identifier, formed by the lower-case letter `v` and a positive ascending integer. Thus, `v1` would be the first version of a [Component Service](#component-service), `v2` would be a change (in API form or underlying implementation) from `v1`, and so on. Successive [API Versions](#api-version) increase in sequence, omitting only any API Versions which have been [Yanked](#yank).
159
+
160
+ In theory, any string can be used as an API Version identifier, though it traditionally revolves around numbers (e.g., '1', '2.7182.818.24'); a common convention is to prepend the lower-case letter `v` to such a number. All that is really required of an API Version identifier is that when two are compared lexically (as strings), the higher/later API Version has a higher/later sort order than the identifier it is being compared to.f
161
+
162
+
163
+ ### API Version-Independent Delivery Application
164
+
165
+ *Technical term.* An executable application whose only purpose is to provide a non-API-Version-specific [Service Base URL](#service-base-url) for use by API clients, invoking the [Content Negotiation](#content-negotiation) middleware or equivalent which redirects (via HTTP 302, 307 or equivalent) to the [Primary Delivery Application](#primary-delivery-application) for a specific [API Version](#api-version).
166
+
167
+ ### AVIDA
168
+
169
+ See [API Version-Independent Delivery Application](#api-version-independent-delivery-application).
170
+
171
+ ### Client
172
+
173
+ An externally-developed and -maintained application or [Component Service](#component-service) which makes [Requests](#request) against the Component Service using this middleware in its [AVIDA](#avida).
174
+
175
+ ### Component Service
176
+
177
+ *Technical term.* A Component Service is a self-contained, independently provisioned software construct that supplies part of the domain or supporting logic for a larger [Platform](#platform). Its only interface to or from other Component Services is via HTTP endpoints and JSON. Therefore, its definition, from the standpoint of any other component in the [System](#system), is expressed entirely in its interface, and no assumptions about implementation can or should be made. Evolution of a component, whether modifications to the API *or refactoring of the implementing code,* causes the [API Version](#api-version) to be incremented.
178
+
179
+ For a service to be a Component Service, it **must** meet each of the following criteria:
180
+
181
+ 1. It **must** use HTTP as the sole means of initiating actions (via [Service Endpoints](#service-endpoint)) from outside code;
182
+ 2. It **must** consist of an [API Version-Independent Delivery Application](#api-version-independent-delivery-application) and *one or more* [API Version](#api-version)-specific [Primary Delivery Application](#primary-delivery-application)s;
183
+ 3. Its [AVIDA](#api-version-independent-delivery-application) **must** invoke [Content Negotiation](#content-negotiation) on each request for a [Service Endpoint](#service-endpoint);
184
+ 4. It **must** [Register](#register) each API Version implemented with the service maintaining the [Repository](#repository) of currently-available Component Services, ordinarily using its [AVIDA](#api-version-independent-delivery-application)'s [Service Base URL](#service-base-url).
185
+
186
+ ### Content Negotiation
187
+
188
+ *Technical term.* A standard feature of HTTP, normally used to select among multiple representations of the data being presented. This is commonly used for agreeing on the response's format (HTML, JSON, etc) and internationalisation (`en-gb`, `pt-br`, etc) to use.
189
+
190
+ Used by an application/[Platform](#platform) to select among (possibly) multiple supported [API Versions](#api-version) of a specific [Component Service](#component-service), with sensible default handling. For example, given a Platform named `acme` and an `ApiDemo` Component Service that supports API Versions `1` and `2`, a request which specifies a value for the `Accept` HTTP header of `application/vnd.acme.apidemo.v1` will be served by the implementation of API Version `1`; a value of `application/vnd.acme.apidemo`, `application/*`, or `*/*` will be handled by the most recent API Version `2`, exactly as if it had been requested by a value of `application/vnd.acme.apidemo.v2`.
191
+
192
+ ### Deprecate
193
+
194
+ *Technical term.* To serve notice that a specific [API Version](api-version) of a specific [Component Service](#component-service) **should not** be used since it will be removed from the [Platform](#platform)'s ecosystem at some future time. Initiating operation of a Component Service with one or more Deprecated API Versions, and/or accessing a [Service Endpoint](#service-endpoint) using a Deprecated API Version, **must** generate diagnostic messages using the customary means for operational monitoring, e.g., logging.
195
+
196
+ ### Endpoint
197
+
198
+ See [Service Endpoint](#service-endpoint).
199
+
200
+ ### Entity
201
+
202
+ *Technical term.* An Entity, is an object that is not defined by its attributes, but each instance has a distinct identity and thread of continuity (flow of internal state over its lifecycle). Contrast [Value Object](#value-object).
203
+
204
+ The term Entity is sometimes casually used for an immutable piece of data that is more properly classed as a Value Object. If it is exclusively used for directly-read attribute values or simple derivations there from ("full name" as a combination of "given name" and "family name" is the classic example), then it *is* a Value Object and **must** be classed as such. Conversely, an Entity should modify its state through actions performed by methods, and minimise "raw" access to data (see *[Tell, Don't Ask](https://pragprog.com/articles/tell-dont-ask)*). A collaborator *asks* a Value Object for something; it *tells* an Entity to do something with itself. (An object that does something with objects *other than* itself is performing a service and is not, strictly speaking, an entity.)
205
+
206
+ *Non-normative supplemental information follows.*
207
+
208
+ The non-normative supplemental information for [Value Object](#value-object) is relevant here. *Unless* an object delivers significant value by having a mutable state over time, *and* is constantly maintained as a single Entity instance throughout by collaborating code, a Value Object **should** be used instead.
209
+
210
+ ### Glossary
211
+
212
+ *Primarily domain term.* The document defining the [Ubiquitous Language](http://blog.carbonfive.com/2016/10/04/ubiquitous-language-the-joy-of-naming/) in use for and by a particular application or [Platform](#platform).
213
+
214
+ ### PDA
215
+
216
+ See [Primary Delivery Application](#primary-delivery-application).
217
+
218
+ ### Platform
219
+
220
+ *Technical term.* Refers to an aggregate of multiple applications and/or [Component Services](#component-service) which, collectively, provide a conceptually unified service or product to users (as opposed to [Clients](#client)). For example, Facebook is a platform; it is not a single, monolithic application.
221
+
222
+ ### Primary Delivery Application
223
+
224
+ *Technical term.* The executable program/process implementing a specific [API Version](#api-version) of a specific [Component Service](#component-service), which **should** be redirected to by the [API Version-Independent Delivery Application](#api-version-independent-delivery-application). It **must** provide the API implemented by its [Service Endpoints](#service-endpoint) via HTTP.
225
+
226
+ ### Register
227
+
228
+ *Technical term.* The process of making another [Component Service](#component-service) known to and addressable based upon information retrieved from the [Repository](#repository) used by this Gem's middleware.
229
+
230
+ ### Repository
231
+
232
+ *Technical term.* A Repository, in general, intermediates between application-level logic and what is presumed to be (and generally treated as) persistent storage of structured data. Our middleware, specifically `ServiceComponentDescriber`, queries a Repository for information about the [Component Service](#component-service) implemented by the application hosting the middleware. How and whether that data is actually persisted, or how and how often that data is updated to match then-current conditions, are details unknown to and, practically speaking, irrelevant to, the middleware and its hosting application.
233
+
234
+ ### Request
235
+
236
+ *Technical term.* Making an HTTP request, and acting upon its response, is the sole means of interacting with a [Component Service](#component-service) as that concept is defined here. The Request **must** specify the Component Service addressed by the request, and **must** *either* specify a specific [API Version](#api-version) of that Service *or* specify that the latest (most recent, highest version identifier) API Version is acceptable.
237
+
238
+ ### Service
239
+
240
+ *Technical term.* See [Component Service](#component-service).
241
+
242
+ ### Service Base URL
243
+
244
+ *Technical term.* The common base of all [Service Endpoints](#service-endpoint) for a given [Component Service](#component-service). An [AVIDA](#avida) and a corresponding [API Version](#api-version)-specific [Primary Delivery Application](#primary-delivery-application) each have unique Service Base URLs.
245
+
246
+ The URL to access a specific [Service Endpoint](#service-endpoint) is formed by appending the path information and query parameters, if any, to the Service Base URL for the Component Service being addressed.
247
+
248
+ ### Service Description
249
+
250
+ *Primarily technical term.* A string which describes the purpose and/or function of a [Service Component](#service-component) with greater semantic meaning than a [Service Name](#service-name). Like a Service Name, this **must** also be unique within the [System](#system). It is primarily for the benefit of UI mechanisms which describe Service Component information in an operationally relevant context.
251
+
252
+ ### Service Endpoint
253
+
254
+ *Technical term.* Uniquely identified by combining a [Service Base URL](#service-base-url) and [Action Name](#action-name), this, after making use of the Content Negotiation [Utility Component](#utility-component), invokes a function or method in a specific API Version of the implementing code responsible for performing a useful action or accessing a useful resource as part of a [Component Service](#component-service).
255
+
256
+ ### Service Name
257
+
258
+ *Technical term.* A short string used to uniquely identify a [Component Service](#component-service) internally within the Conversagence ecosystem. This is distinct from the [Service Description](#service-description). It **must** be unique within the [System](#system).
259
+
260
+ ### Specification
261
+
262
+ *Primarily technical term.* In the Ubiquitous Language, specifically refers to the specification of the API and associated functionality of a specific [Component Service](#component-service), typically defined in a single hypertext document similar to this one.
263
+
264
+ Specifications are expected to converge on a common set of formatting and content standards that **must** be documented here after multiple source samples establish those standards through usage and review between Stakeholders.
265
+
266
+ ### Stakeholder
267
+
268
+ *Domain term.* An individual or cohesive collection of individuals participating in the continuing development, including supporting business operations, of the Conversagence Project. These include, but are not limited to, [Members](#member), developers, operational specialists, domain experts, and investors. Each of these, and others, have a "stake" in the success of the Project and the organisation, and each are essential contributors to that success.
269
+
270
+ ### Target Service
271
+
272
+ *Technical term.* Used by a Utility Component's code or Specification to refer to the Component Service on whose behalf it is performing work. This term's first use in a specific context **must** make clear whether this term refers to *any* API Version of the Component Service being targeted rather than to a *single* API Version as is the intended default usage of the term.
273
+
274
+ ### Value Object
275
+
276
+ *Technical term.* Value Objects have no identity or flow of internal state (they are immutable), and are defined by their attribute values. Examples are colours (which are expressed using values such as sRGB and CMYK), geolocations (latitude and longitude values), and so on. Two value objects may be compared on the basis of their attributes (the location for Springfield is not the same as the location for Singapore), but assignment normally does not make sense in context (e.g., changing the values for the "location of Singapore" object to those of the "location of Springfield" object and expecting to continue to see the two as having distinct meaning). Contrast [Entity](#entity).
277
+
278
+ *Non-normative supplemental information follows.*
279
+
280
+ *In general,* immutable value objects are to be preferred in use to mutable entities; when an "updated" object is needed, instantiate a new object and delete (or garbage-collect) the old one. Maintaining and preserving mutable state is a fraught enterprise that sucks resources and is a traditionally rich source of defects.
281
+
282
+ ### Yank
283
+
284
+ *Technical term.* To Yank a specific [API Version](#api-version) of a specific [Specification](#specification) is to remove it from possible use by any [Component Service](#component-service). This **should** normally be done after the API Version in question has been [Deprecated](#deprecate) for a period of time, generally due to later changes in the API obsoleting the to-be-Yanked API Version.
285
+
286
+ Yanking a specific [API Version](#api-version) **may** also be necessary as part of an urgent security response, when vulnerabilities or other critical failures have been discovered in the version in question.
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './encoded_api_version_data'
4
+ require_relative './input_env'
5
+ require_relative './input_is_invalid'
6
+ require_relative './match_header_against_api_versions'
7
+ require_relative './report_no_matching_version'
8
+
9
+ # All(?) Rack code is namespaced within this module.
10
+ module Rack
11
+ # Module includes our middleware components for managing service API versions.
12
+ module ServiceApiVersioning
13
+ # Select API Version of Component Service based on HTTP `Accept` header.
14
+ class AcceptContentTypeSelector
15
+ def initialize(app)
16
+ @app = app
17
+ @env = nil
18
+ @input = nil
19
+ self
20
+ end
21
+
22
+ def call(env)
23
+ @env = env
24
+ @input = InputEnv.new env
25
+ match_request_from_header
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :app, :env, :input
31
+
32
+ def add_component_api_version_data(version)
33
+ env['COMPONENT_API_VERSION_DATA'] = encoded_data_for(version)
34
+ self
35
+ end
36
+
37
+ def api_versions
38
+ input.data[:api_versions]
39
+ end
40
+
41
+ def encoded_data_for(api_version)
42
+ EncodedApiVersionData.call(api_version: api_version, data: input.data)
43
+ end
44
+
45
+ def if_component_apis_defined(&_)
46
+ InputIsInvalid.call(input) || yield
47
+ end
48
+
49
+ def if_requested_api_version_found
50
+ best_version = specified_api_version
51
+ return report_no_matching_api_versions unless best_version
52
+ yield best_version
53
+ end
54
+
55
+ def match_request_from_header
56
+ if_component_apis_defined do
57
+ if_requested_api_version_found do |matching_version|
58
+ use_api_version(matching_version)
59
+ end
60
+ end
61
+ end
62
+
63
+ def report_no_matching_api_versions
64
+ ReportNoMatchingVersion.call api_versions: api_versions
65
+ end
66
+
67
+ def specified_api_version
68
+ MatchHeaderAgainstApiVersions.call(accept_header: env['HTTP_ACCEPT'],
69
+ api_versions: api_versions)
70
+ end
71
+
72
+ def use_api_version(matching_version)
73
+ add_component_api_version_data(matching_version)
74
+ app.call env
75
+ end
76
+ end # class AcceptContentTypeSelector
77
+ end
78
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack'
4
+ require 'rack/response'
5
+
6
+ require_relative './build_redirect_uri_from_env'
7
+
8
+ # All(?) Rack code is namespaced within this module.
9
+ module Rack
10
+ # Module includes our middleware components for managing service API versions.
11
+ module ServiceApiVersioning
12
+ # Returns an HTTP 302 response with cleaned-up environment and `Location`
13
+ # header.
14
+ class ApiVersionRedirector
15
+ def initialize(app)
16
+ @app = app
17
+ @env = nil
18
+ self
19
+ end
20
+
21
+ def call(env)
22
+ @env = env
23
+ response
24
+ end
25
+
26
+ private
27
+
28
+ DEFAULT_STATUS = 307
29
+ private_constant :DEFAULT_STATUS
30
+
31
+ attr_reader :app, :env
32
+
33
+ def api_version
34
+ api_version_data[:api_version]
35
+ end
36
+
37
+ def api_version_data
38
+ JSON.parse(env['COMPONENT_API_VERSION_DATA'], symbolize_names: true)
39
+ end
40
+
41
+ def body
42
+ 'Please resend the request to ' \
43
+ "<a href=\"#{location}\">#{location}</a>" \
44
+ ' without caching it.'
45
+ end
46
+
47
+ def headers
48
+ { 'API-Version' => api_version, 'Location' => location }
49
+ end
50
+
51
+ def location
52
+ BuildRedirectUriFromEnv.call(env)
53
+ end
54
+
55
+ def response
56
+ Rack::Response.new(body, DEFAULT_STATUS, headers).finish
57
+ end
58
+ end # class Rack::ServiceApiVersioning::ApiVersionRedirector
59
+ end
60
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack'
4
+ require 'rack/response'
5
+
6
+ # All(?) Rack code is namespaced within this module.
7
+ module Rack
8
+ # Module includes our middleware components for managing service API versions.
9
+ module ServiceApiVersioning
10
+ # Build redirect URI from original request URI and API Version SBU.
11
+ class BuildRedirectLocationUri
12
+ def self.call(api_version_base_uri:, request_uri:)
13
+ new(api_version_base_uri, request_uri).call
14
+ end
15
+
16
+ def call
17
+ update_path
18
+ update_query
19
+ uri_for_redirect
20
+ end
21
+
22
+ protected
23
+
24
+ def initialize(api_version_base_uri, request_uri)
25
+ @new_base_uri_parts = api_version_base_uri.to_hash
26
+ @request_uri = request_uri
27
+ self
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :new_base_uri_parts, :request_uri
33
+
34
+ def combined_path_str
35
+ new_base_uri_parts[:path] + request_uri.path
36
+ end
37
+
38
+ def path_str
39
+ combined_path_str.sub('//', '/')
40
+ end
41
+
42
+ def update_path
43
+ new_base_uri_parts[:path] = path_str
44
+ end
45
+
46
+ def update_query
47
+ new_base_uri_parts[:query] = request_uri.query
48
+ end
49
+
50
+ def uri_for_redirect
51
+ Addressable::URI.new(new_base_uri_parts)
52
+ end
53
+ end # class Rack::ServiceApiVersioning::BuildRedirectLocationUri
54
+ end
55
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'addressable'
4
+ require 'rack'
5
+ require 'rack/response'
6
+
7
+ require_relative './build_redirect_location_uri'
8
+
9
+ # All(?) Rack code is namespaced within this module.
10
+ module Rack
11
+ # Module includes our middleware components for managing service API versions.
12
+ module ServiceApiVersioning
13
+ # Build redirect URI from data in `env`
14
+ class BuildRedirectUriFromEnv
15
+ def self.call(env)
16
+ new(env).call
17
+ end
18
+
19
+ def call
20
+ location_uri_from(request_uri).to_s
21
+ end
22
+
23
+ protected
24
+
25
+ def initialize(env)
26
+ @env = env
27
+ self
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :env
33
+
34
+ def api_version_base_uri
35
+ Addressable::URI.parse(api_version_data[:base_url])
36
+ end
37
+
38
+ def api_version_data
39
+ JSON.parse(env['COMPONENT_API_VERSION_DATA'], symbolize_names: true)
40
+ end
41
+
42
+ def location_uri_from(request_uri)
43
+ params = { api_version_base_uri: api_version_base_uri,
44
+ request_uri: request_uri }
45
+ BuildRedirectLocationUri.call params
46
+ end
47
+
48
+ def request_uri
49
+ request_str = Rack::Request.new(env).url
50
+ Addressable::URI.parse(request_str)
51
+ end
52
+ end # class BuildRedirectUriFromEnv
53
+ end
54
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'prolog/dry_types'
4
+
5
+ # All(?) Rack code is namespaced within this module.
6
+ module Rack
7
+ # Module includes our middleware components for managing service API versions.
8
+ module ServiceApiVersioning
9
+ # Builds an API Version data hash and encodes as JSON, for injection into a
10
+ # Rack environment, normally with the key "COMPONENT_API_VERSION_DATA".
11
+ class EncodedApiVersionData
12
+ # Unpacks relevant attributes from passed-in input data
13
+ class InputData < Dry::Struct::Value
14
+ attribute :api_version, Types::Strict::Symbol
15
+ attribute :input_data, Types::Hash
16
+
17
+ def base_url
18
+ version_data[:base_url]
19
+ end
20
+
21
+ def name
22
+ input_data[:name]
23
+ end
24
+
25
+ def vendor_org
26
+ content_type_parts[VENDOR_ORG_INDEX]
27
+ end
28
+
29
+ private
30
+
31
+ # Index 0 will be `application/vnd`; index 1 the org name (eg, `acme`)
32
+ VENDOR_ORG_INDEX = 1
33
+ private_constant :VENDOR_ORG_INDEX
34
+
35
+ def content_type_parts
36
+ version_data[:content_type].split('.')
37
+ end
38
+
39
+ def version_data
40
+ input_data[:api_versions][api_version]
41
+ end
42
+ end # class EncodedApiVersionData::InputData
43
+ end # class EncodedApiVersionData
44
+ end
45
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'prolog/dry_types'
4
+
5
+ # All(?) Rack code is namespaced within this module.
6
+ module Rack
7
+ # Module includes our middleware components for managing service API versions.
8
+ module ServiceApiVersioning
9
+ # Exception wrapper class for invalid return data from
10
+ # `EncodedApiVersionData#version_data` due to a bad SBU being specified.
11
+ # Not nested within that class to avoid leaking unnecessary implementation
12
+ # detail, *even though* this class is (presently) only used by that class.
13
+ class InvalidBaseUrlError < RuntimeError
14
+ def initialize(base_url, original_error)
15
+ @original_error = original_error
16
+ super "Invalidly formatted base URL: #{base_url}"
17
+ end
18
+
19
+ attr_reader :original_error
20
+ end # class InvalidBaseUrlError
21
+ end
22
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'prolog/dry_types'
4
+
5
+ # All(?) Rack code is namespaced within this module.
6
+ module Rack
7
+ # Module includes our middleware components for managing service API versions.
8
+ module ServiceApiVersioning
9
+ # Builds an API Version data hash and encodes as JSON, for injection into a
10
+ # Rack environment, normally with the key "COMPONENT_API_VERSION_DATA".
11
+ class EncodedApiVersionData
12
+ # Immutable, structured data type for returned version data.
13
+ class ReturnData < Dry::Struct::Value
14
+ SBU_FMT = %r{\A\w+?://.+?/\z}
15
+ private_constant :SBU_FMT
16
+
17
+ constructor_type :strict_with_defaults
18
+
19
+ attribute :api_version, Types::Coercible::String
20
+ attribute :base_url, Types::Strict::String.constrained(format: SBU_FMT)
21
+ attribute :name, Types::Strict::String
22
+ attribute :deprecated, Types::Strict::Bool.default(false)
23
+ attribute :restricted, Types::Strict::Bool.default(false)
24
+ attribute :vendor_org, Types::Strict::String
25
+
26
+ def content_type
27
+ content_parts.join('.') + '+json'
28
+ end
29
+
30
+ def to_hash
31
+ super.merge(content_type: content_type)
32
+ .reject { |key, _| key == :vendor_org }
33
+ end
34
+ alias to_h to_hash
35
+
36
+ private
37
+
38
+ def content_parts
39
+ ['application/vnd', vendor_org, name, api_version.to_s]
40
+ end
41
+ end # class EncodedApiVersionData::ReturnData
42
+ end # class EncodedApiVersionData
43
+ end
44
+ end