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,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