rack-service_api_versioning 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ require 'prolog/dry_types'
6
+
7
+ require_relative './encoded_api_version_data/input_data'
8
+ require_relative './encoded_api_version_data/return_data'
9
+ require_relative './encoded_api_version_data/invalid_base_url_error'
10
+
11
+ # All(?) Rack code is namespaced within this module.
12
+ module Rack
13
+ # Module includes our middleware components for managing service API versions.
14
+ module ServiceApiVersioning
15
+ # Builds an API Version data hash and encodes as JSON, for injection into a
16
+ # Rack environment, normally with the key "COMPONENT_API_VERSION_DATA".
17
+ class EncodedApiVersionData
18
+ def self.call(api_version:, data:)
19
+ new(api_version, data).call
20
+ end
21
+
22
+ def call
23
+ JSON.dump(version_data.to_hash)
24
+ end
25
+
26
+ protected
27
+
28
+ def initialize(api_version, input_data)
29
+ @api_version = api_version.to_sym
30
+ @data_obj = InputData.new api_version: api_version,
31
+ input_data: input_data
32
+ self
33
+ end
34
+
35
+ private
36
+
37
+ extend Forwardable
38
+ def_delegators :@data_obj, :base_url, :name, :vendor_org
39
+
40
+ attr_reader :api_version
41
+
42
+ def raise_invalid_base_url_error(original_error)
43
+ raise InvalidBaseUrlError.new base_url, original_error
44
+ end
45
+
46
+ def return_data_params
47
+ { api_version: api_version, base_url: base_url, name: name,
48
+ vendor_org: vendor_org }
49
+ end
50
+
51
+ def version_data
52
+ ReturnData.new return_data_params
53
+ rescue Dry::Struct::Error => original_error
54
+ raise_invalid_base_url_error original_error
55
+ end
56
+
57
+ private_constant :InputData
58
+ private_constant :ReturnData
59
+ end # class EncodedApiVersionData
60
+ end
61
+ end
@@ -0,0 +1,29 @@
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
+ # Builds Rack::Response with specified status code and body message.
11
+ class HttpErrorResponse
12
+ def call
13
+ Rack::Response.new(message, code).finish
14
+ end
15
+
16
+ protected
17
+
18
+ def initialize(code, message)
19
+ @code = code.to_i
20
+ @message = Array(message)
21
+ self
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :code, :message
27
+ end # class Rack::ServiceApiVersioning::HttpErrorResponse
28
+ end
29
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # All(?) Rack code is namespaced within this module.
4
+ module Rack
5
+ # Module includes our middleware components for managing service API versions.
6
+ module ServiceApiVersioning
7
+ # Wrapper around JSON encoding of object in environment with defaulted key.
8
+ class InputEnv
9
+ DEFAULT_INPUT_KEY = 'COMPONENT_DESCRIPTION'
10
+
11
+ def initialize(env, input_key = DEFAULT_INPUT_KEY)
12
+ @env = env
13
+ @key = input_key
14
+ self
15
+ end
16
+
17
+ def any?
18
+ !input_str.empty?
19
+ end
20
+
21
+ def data
22
+ JSON.parse input_str, symbolize_names: true
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :env, :key
28
+
29
+ def input_str
30
+ input_value.strip
31
+ end
32
+
33
+ def input_value
34
+ env[key].to_s
35
+ end
36
+ end # class Rack::ServiceApiVersioning::InputEnv
37
+ end
38
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './report_invalid_description'
4
+ require_relative './report_not_found'
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
+ # Validates input for AcceptContentTypeSelector. If input passes checks,
11
+ # returns `nil`. Returns a Rack::Response instance if any validation step
12
+ # failed.
13
+ class InputIsInvalid
14
+ def self.call(input)
15
+ Internals.verify_input(input) || Internals.verify_api_versions(input)
16
+ end
17
+
18
+ # Stateless methods
19
+ module Internals
20
+ def self._api_versions?(input)
21
+ input.data[:api_versions].any?
22
+ end
23
+
24
+ def self.verify_api_versions(input)
25
+ return ReportNotFound.call unless _api_versions?(input)
26
+ nil
27
+ end
28
+
29
+ def self.verify_input(input)
30
+ return ReportInvalidDescription.call unless input.any?
31
+ nil
32
+ end
33
+ end
34
+ end # class Rack::ServiceApiVersioning::InputIsInvalid
35
+ end
36
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack'
4
+ require 'rack/utils'
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
+ # Matches content of HTTP Accept header against presently-available API
11
+ # Versions. Returns either a symbolic value (e.g., `:v2`) on success or
12
+ # `nil` on failure.
13
+ class MatchHeaderAgainstApiVersions
14
+ def self.call(accept_header:, api_versions:)
15
+ new(accept_header, api_versions).call
16
+ end
17
+
18
+ def call
19
+ best_match
20
+ end
21
+
22
+ protected
23
+
24
+ def initialize(accept_header, api_versions)
25
+ @accept_header = accept_header
26
+ @api_versions = api_versions
27
+ self
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :accept_header, :api_versions
33
+
34
+ def all_matches
35
+ api_versions.select { |_, version| best_type?(version) }
36
+ end
37
+
38
+ def all_types
39
+ api_versions.values.map { |version| version[:content_type] }
40
+ end
41
+
42
+ def best_match
43
+ all_matches.keys.first
44
+ end
45
+
46
+ def best_type
47
+ Rack::Utils.best_q_match(accept_header, all_types)
48
+ end
49
+
50
+ def best_type?(version)
51
+ version[:content_type] == best_type
52
+ end
53
+ end # class Rack::ServiceApiVersioning::MatchHeaderAgainstApiVersions
54
+ end
55
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './http_error_response'
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 Rack::Response to halt request execution with an HTTP status code
10
+ # of 400 ("Bad Request").
11
+ class ReportInvalidDescription < HttpErrorResponse
12
+ DEFAULT_MESSAGE = 'Invalid value for COMPONENT_DESCRIPTION'
13
+
14
+ def self.call(code: 400, message: DEFAULT_MESSAGE)
15
+ new(code, message).call
16
+ end
17
+ end # class Rack::ServiceApiVersioning::ReportInvalidDescription
18
+ end
19
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './http_error_response'
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 Rack::Response to halt request execution with an HTTP status code
10
+ # of 406 ("Not Acceptable") when no presently available API Version has been
11
+ # specified in an HTTP `Accept` header.
12
+ class ReportNoMatchingVersion < HttpErrorResponse
13
+ def self.call(api_versions:, code: 406)
14
+ new(code, Internals.message_data(api_versions)).call
15
+ end
16
+
17
+ # Stateless methods.
18
+ module Internals
19
+ def self.all_types_as_string(api_versions, separator = ', ')
20
+ all_types = api_versions.values.map do |version|
21
+ version[:content_type]
22
+ end
23
+ all_types.join(separator)
24
+ end
25
+
26
+ def self.message_data(api_versions)
27
+ types = all_types_as_string(api_versions)
28
+ JSON.dump('supported-media-types': types)
29
+ end
30
+ end
31
+ private_constant :Internals
32
+ end # class Rack::ServiceApiVersioning::ReportNoMatchingVersion
33
+ end
34
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './http_error_response'
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 Rack::Response to halt request execution, responding with a 404.
10
+ class ReportNotFound < HttpErrorResponse
11
+ DEFAULT_MESSAGE = 'Invalid value for COMPONENT_DESCRIPTION'
12
+
13
+ def self.call(code: 404, message: DEFAULT_MESSAGE)
14
+ new(code, message).call
15
+ end
16
+ end # class Rack::ServiceApiVersioning::ReportNotFound
17
+ end
18
+ end
@@ -0,0 +1,44 @@
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
+ # Middlware to query and encode data on a named service, making it available
11
+ # to later entries in the middleware chain via an environment variable.
12
+ class ServiceComponentDescriber
13
+ # Builds Rack::Result to halt request execution, responding with a 404.
14
+ class ReportServiceNotFound
15
+ def self.call(service_name)
16
+ new(service_name).call
17
+ end
18
+
19
+ def call
20
+ new_response.finish
21
+ end
22
+
23
+ protected
24
+
25
+ def initialize(service_name)
26
+ @service_name = service_name
27
+ self
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :service_name
33
+
34
+ def body
35
+ %(Service not found: "#{service_name}")
36
+ end
37
+
38
+ def new_response
39
+ Rack::Response.new(Array(body), 404)
40
+ end
41
+ end # class ServiceComponentDescriber::ReportServiceNotFound
42
+ end # class ServiceComponentDescriber
43
+ end
44
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack'
4
+ require 'rack/response'
5
+
6
+ require_relative './service_component_describer/report_service_not_found'
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
+ # Middleware that, given a "service name" parameter, will read descriptive
13
+ # data from a supplied repository regarding the named service, format that
14
+ # data as a JSON string, assign it to the `COMPONENT_DESCRIPTION`
15
+ # environment variable, and pass the updated environment along to the next
16
+ # link in the Rack call chain (app or more middleware). If there is no data
17
+ # for the named service in the repository, this middleware will halt
18
+ # execution with a 404 (Not Found).
19
+ #
20
+ # Also bear in mind that both usual parameters (`repository` and
21
+ # `service_name`) are supplied *by the AVIDA,* which should know what those
22
+ # "ought" to be.
23
+ class ServiceComponentDescriber
24
+ DEFAULT_ENV_KEYS = { result: 'COMPONENT_DESCRIPTION' }.freeze
25
+
26
+ def initialize(app, repository:, service_name:,
27
+ env_keys: DEFAULT_ENV_KEYS)
28
+ @app = app
29
+ @env_keys = env_keys
30
+ @repository = repository
31
+ @service_name = service_name
32
+ self
33
+ end
34
+
35
+ def call(env)
36
+ env = update_env(env)
37
+ verify_datum_set(env) { |app_env| app.call app_env }
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :app, :env_keys, :repository, :service_name
43
+
44
+ def first_record
45
+ repository.find(name: service_name).first
46
+ end
47
+
48
+ def result_key
49
+ env_keys[:result]
50
+ end
51
+
52
+ def update_env(env)
53
+ datum = first_record
54
+ env[result_key] = JSON.dump(datum) if datum
55
+ env
56
+ end
57
+
58
+ def verify_datum_set(env)
59
+ verify_found(env) || yield(env)
60
+ end
61
+
62
+ def verify_found(env)
63
+ return nil if env[result_key]
64
+ ReportServiceNotFound.call(service_name)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ module ServiceApiVersioning
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack/service_api_versioning/version'
4
+ require 'rack/service_api_versioning/accept_content_type_selector'
5
+ require 'rack/service_api_versioning/api_version_redirector'
6
+ require 'rack/service_api_versioning/service_component_describer'
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
+ # Your code goes here...
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake/tasklib'
4
+ require 'flog'
5
+ require 'flog_task'
6
+
7
+ # Redefinition of standard task's Rake invocation. Because we don't like
8
+ # inconsistency in option settings.
9
+ class FlogTask < Rake::TaskLib
10
+ # Reek bitches that this is a :reek:Attribute (writable). That's the *point*.
11
+ attr_accessor :methods_only
12
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'rack/service_api_versioning/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "rack-service_api_versioning"
9
+ spec.version = Rack::ServiceApiVersioning::VERSION
10
+ spec.authors = ["Jeff Dickey"]
11
+ spec.email = ["jdickey@seven-sigma.com"]
12
+
13
+ spec.summary = %q{Rack middleware for API Version-specific Component Service redirection.}
14
+ # spec.description = %q{TODO: Write a longer description or delete this line.}
15
+ spec.homepage = "https://github.com/jdickey/rack-service_api_versioning"
16
+ spec.required_ruby_version = ">= 2.3.0"
17
+ spec.license = "MIT"
18
+
19
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
20
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
21
+ # if spec.respond_to?(:metadata)
22
+ # spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
23
+ # else
24
+ # raise "RubyGems 2.0 or newer is required to protect against " \
25
+ # "public gem pushes."
26
+ # end
27
+
28
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
29
+ f.match(%r{^(test|spec|features)/})
30
+ end
31
+ spec.bindir = "exe"
32
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
33
+ spec.require_paths = ["lib"]
34
+
35
+ spec.add_dependency "prolog-dry_types", '0.3.3'
36
+ spec.add_dependency "rack", '2.0.1'
37
+
38
+ spec.add_development_dependency "bundler", '1.14.6'
39
+ spec.add_development_dependency "rake", '12.0.0'
40
+ spec.add_development_dependency "minitest", '5.10.1'
41
+
42
+ spec.add_development_dependency "minitest-matchers", '1.4.1'
43
+ spec.add_development_dependency "minitest-reporters", '1.1.14'
44
+ spec.add_development_dependency "minitest-tagz", '1.5.2'
45
+ spec.add_development_dependency "flay", '2.8.1'
46
+ spec.add_development_dependency "flog", '4.6.1'
47
+ spec.add_development_dependency "reek", '4.5.6'
48
+ spec.add_development_dependency "rubocop", '0.47.1'
49
+ spec.add_development_dependency "simplecov", '0.14.1'
50
+ spec.add_development_dependency "pry-byebug", '3.4.2'
51
+ spec.add_development_dependency "pry-doc", '0.10.0'
52
+ spec.add_development_dependency "awesome_print", '1.7.0'
53
+ spec.add_development_dependency "colorator", '1.1.0'
54
+
55
+ spec.add_development_dependency 'guard', '2.14.1'
56
+ spec.add_development_dependency 'guard-livereload', '2.5.2'
57
+ spec.add_development_dependency 'guard-minitest', '2.4.6'
58
+ spec.add_development_dependency 'guard-rake', '1.0.0'
59
+ spec.add_development_dependency 'guard-reek', '1.0.2'
60
+ spec.add_development_dependency 'guard-rubocop', '1.2.0'
61
+ spec.add_development_dependency 'guard-rubycritic', '2.9.3'
62
+ end
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env zsh
2
+
3
+ RBVER=`rbenv version | cut -f 1 -d ' '`
4
+ GEMSETBASE="./tmp/gemsets/$RBVER"
5
+ rm -rf .rbenv-gemsets $GEMSETBASE/{dev,extras,runtime}
6
+ mkdir -p $RBENV_ROOT/versions/$RBVER/gemsets/.project-gemsets/rack-service_api_versioning-dummy
7
+ rm -rf $RBENV_ROOT/versions/$RBVER/gemsets/.project-gemsets/*rack-service_api_versioning*
8
+
9
+ mkdir -p $GEMSETBASE
10
+ rbenv gemset init $GEMSETBASE/runtime
11
+ gem install prolog-dry_types -v 0.3.3
12
+ gem install rack -v 2.0.1
13
+
14
+ rbenv gemset init $GEMSETBASE/dev
15
+ echo "$GEMSETBASE/dev\n$GEMSETBASE/runtime" > .rbenv-gemsets
16
+ gem install rake -v 12.0.0
17
+ gem install minitest -v 5.10.1
18
+ gem install minitest-matchers -v 1.4.1
19
+ gem install minitest-reporters -v 1.1.14
20
+ gem install minitest-tagz -v 1.5.2
21
+
22
+ gem install flay -v 2.8.1
23
+ gem install flog -v 4.6.1
24
+ gem install pry-byebug -v 3.4.2
25
+ gem install pry-doc -v 0.10.0
26
+ gem install reek -v 4.5.6
27
+ gem install rubocop -v 0.47.1
28
+ gem install simplecov -v 0.14.1
29
+
30
+ gem install awesome_print -v 1.7.0
31
+ gem install colorator -v 1.1.0
32
+
33
+ gem install guard -v 2.14.1
34
+ gem install guard-livereload -v 2.5.2
35
+ gem install guard-minitest -v 2.4.6
36
+ gem install guard-rake -v 1.0.0
37
+ gem install guard-reek -v 1.0.2
38
+ gem install guard-rubocop -v 1.2.0
39
+ gem install guard-rubycritic -v 2.9.3
40
+ # gem install guard-shell -v 0.7.1
41
+
42
+ # gem install fury -v 0.0.5
43
+ rbenv gemset init $GEMSETBASE/extras
44
+ echo "$GEMSETBASE/extras\n$GEMSETBASE/dev\n$GEMSETBASE/runtime" > .rbenv-gemsets
45
+ unset RBVER
46
+ rbenv rehash
47
+
48
+ bundle install --local