endpoint-flux 1.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +36 -0
- data/CONTRIBUTING.md +18 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +37 -0
- data/README.md +607 -0
- data/circle.yml +14 -0
- data/endpoint_flux.gemspec +18 -0
- data/lib/endpoint_flux.rb +20 -0
- data/lib/endpoint_flux/class_loader.rb +58 -0
- data/lib/endpoint_flux/config.rb +85 -0
- data/lib/endpoint_flux/config/interceptor.rb +23 -0
- data/lib/endpoint_flux/config/middleware.rb +38 -0
- data/lib/endpoint_flux/config/rescue_from.rb +28 -0
- data/lib/endpoint_flux/endpoint.rb +81 -0
- data/lib/endpoint_flux/exceptions.rb +10 -0
- data/lib/endpoint_flux/exceptions/base.rb +21 -0
- data/lib/endpoint_flux/exceptions/forbidden.rb +12 -0
- data/lib/endpoint_flux/exceptions/not_found.rb +12 -0
- data/lib/endpoint_flux/exceptions/service_unavailable.rb +12 -0
- data/lib/endpoint_flux/exceptions/unauthorized.rb +12 -0
- data/lib/endpoint_flux/exceptions/validation.rb +13 -0
- data/lib/endpoint_flux/middlewares.rb +8 -0
- data/lib/endpoint_flux/middlewares/authenticator/skip.rb +11 -0
- data/lib/endpoint_flux/middlewares/authorizator/skip.rb +11 -0
- data/lib/endpoint_flux/middlewares/decorator/add_status.rb +12 -0
- data/lib/endpoint_flux/middlewares/decorator/skip.rb +11 -0
- data/lib/endpoint_flux/middlewares/policy/skip.rb +11 -0
- data/lib/endpoint_flux/middlewares/validator/empty.rb +12 -0
- data/lib/endpoint_flux/rails/concerns/endpoint_controller.rb +43 -0
- data/lib/endpoint_flux/request.rb +17 -0
- data/lib/endpoint_flux/response.rb +30 -0
- data/lib/endpoint_flux/version.rb +3 -0
- data/spec/lib/class_loader_spec.rb +31 -0
- data/spec/lib/config/default_middlewares_spec.rb +21 -0
- data/spec/lib/config/endpoints_namespace_spec.rb +13 -0
- data/spec/lib/config/flow_spec.rb +8 -0
- data/spec/lib/config/interceptor_spec.rb +34 -0
- data/spec/lib/config/middleware_spec.rb +62 -0
- data/spec/lib/config/rescue_from_spec.rb +45 -0
- data/spec/lib/endpoint/flow_spec.rb +43 -0
- data/spec/lib/endpoint/middlewares_spec.rb +110 -0
- data/spec/lib/endpoint/perform_spec.rb +61 -0
- data/spec/lib/endpoint/rescue_from_spec.rb +61 -0
- data/spec/lib/endpoint_flux/rails/concerns/endpoint_controller_spec.rb +75 -0
- data/spec/lib/endpoint_flux/request_spec.rb +44 -0
- data/spec/lib/exceptions/forbidden_spec.rb +12 -0
- data/spec/lib/exceptions/not_found_spec.rb +12 -0
- data/spec/lib/exceptions/service_unavailable_spec.rb +12 -0
- data/spec/lib/exceptions/unauthorized_spec.rb +12 -0
- data/spec/lib/exceptions/validation_spec.rb +14 -0
- data/spec/lib/middlewares/authenticator/skip_spec.rb +5 -0
- data/spec/lib/middlewares/authorizator/skip_spec.rb +5 -0
- data/spec/lib/middlewares/decorator/add_status_spec.rb +17 -0
- data/spec/lib/middlewares/decorator/skip_spec.rb +5 -0
- data/spec/lib/middlewares/policy/skip_spec.rb +5 -0
- data/spec/lib/middlewares/shared_examples.rb +19 -0
- data/spec/lib/middlewares/validator/empty_spec.rb +15 -0
- data/spec/lib/response_spec.rb +131 -0
- data/spec/spec_helper.rb +52 -0
- metadata +157 -0
data/circle.yml
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
version: 2
|
2
|
+
jobs:
|
3
|
+
build:
|
4
|
+
docker:
|
5
|
+
- image: circleci/ruby:2.5.3-node
|
6
|
+
steps:
|
7
|
+
- checkout
|
8
|
+
- run: gem install bundle
|
9
|
+
- run:
|
10
|
+
name: Bundle Install
|
11
|
+
command: bundle check || bundle install
|
12
|
+
- run:
|
13
|
+
name: Run Specs
|
14
|
+
command: bundle exec rspec --format progress
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative 'lib/endpoint_flux/version' # frozen_string_literal: true
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'endpoint-flux'
|
5
|
+
s.version = EndpointFlux::VERSION
|
6
|
+
s.summary = 'EndpointFlux!'
|
7
|
+
s.description = 'A simple way to organise API endpoints'
|
8
|
+
s.authors = ['Arturs Kreipans https://github.com/fragallia']
|
9
|
+
s.files = `git ls-files`.split($\)
|
10
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
11
|
+
s.require_paths = ['lib']
|
12
|
+
s.homepage = 'https://github.com/resolving/endpoint-flux'
|
13
|
+
s.license = 'MIT'
|
14
|
+
s.required_ruby_version = '>= 2.5.3'
|
15
|
+
|
16
|
+
s.add_development_dependency 'byebug', '>= 9.0'
|
17
|
+
s.add_development_dependency 'rspec', '>= 3.5.0'
|
18
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'endpoint_flux/class_loader'
|
2
|
+
|
3
|
+
require 'endpoint_flux/config'
|
4
|
+
require 'endpoint_flux/endpoint'
|
5
|
+
require 'endpoint_flux/exceptions'
|
6
|
+
require 'endpoint_flux/middlewares'
|
7
|
+
|
8
|
+
module EndpointFlux
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def config(handler = nil)
|
12
|
+
@config = handler if handler
|
13
|
+
|
14
|
+
@config ||= EndpointFlux::Config.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def clear
|
18
|
+
@config = nil
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module EndpointFlux
|
2
|
+
module ClassLoader
|
3
|
+
module_function
|
4
|
+
|
5
|
+
def load_class(klass_name)
|
6
|
+
return klass_name if klass_name.is_a?(Class)
|
7
|
+
|
8
|
+
constantize('::' + string_to_class_name(klass_name.to_s))
|
9
|
+
rescue NameError
|
10
|
+
nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def load_class!(klass_name)
|
14
|
+
load_class(klass_name) ||
|
15
|
+
raise("The [#{klass_name}] should be a string representing a class")
|
16
|
+
end
|
17
|
+
|
18
|
+
def string_to_class_name(klass_name)
|
19
|
+
klass_name
|
20
|
+
.sub(%r{^[a-z\d]*}) { $&.capitalize }
|
21
|
+
.gsub(%r{(?:_|(\/))([a-z\d]*)}) do
|
22
|
+
"#{Regexp.last_match[1]}#{Regexp.last_match[2].capitalize}"
|
23
|
+
end
|
24
|
+
.gsub('/', '::')
|
25
|
+
end
|
26
|
+
|
27
|
+
def constantize(camel_cased_word)
|
28
|
+
names = camel_cased_word.split("::".freeze)
|
29
|
+
|
30
|
+
# Trigger a built-in NameError exception including the ill-formed constant in the message.
|
31
|
+
Object.const_get(camel_cased_word) if names.empty?
|
32
|
+
|
33
|
+
# Remove the first blank element in case of '::ClassName' notation.
|
34
|
+
names.shift if names.size > 1 && names.first.empty?
|
35
|
+
|
36
|
+
names.inject(Object) do |constant, name|
|
37
|
+
if constant == Object
|
38
|
+
constant.const_get(name)
|
39
|
+
else
|
40
|
+
candidate = constant.const_get(name)
|
41
|
+
next candidate if constant.const_defined?(name, false)
|
42
|
+
next candidate unless Object.const_defined?(name)
|
43
|
+
|
44
|
+
# Go down the ancestors to check if it is owned directly. The check
|
45
|
+
# stops when we reach Object or the end of ancestors tree.
|
46
|
+
constant = constant.ancestors.inject(constant) do |const, ancestor|
|
47
|
+
break const if ancestor == Object
|
48
|
+
break ancestor if ancestor.const_defined?(name, false)
|
49
|
+
const
|
50
|
+
end
|
51
|
+
|
52
|
+
# owner is in Object, so raise
|
53
|
+
constant.const_get(name, false)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module EndpointFlux
|
2
|
+
class Config
|
3
|
+
require 'endpoint_flux/config/interceptor'
|
4
|
+
require 'endpoint_flux/config/middleware'
|
5
|
+
require 'endpoint_flux/config/rescue_from'
|
6
|
+
|
7
|
+
attr_accessor :middlewares_namespaces
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@flow = %i[authenticator authorizator validator policy process decorator]
|
11
|
+
@default_middlewares = {}
|
12
|
+
@rescue_from = EndpointFlux::Config::RescueFrom.new
|
13
|
+
@endpoints_namespace = 'endpoints'
|
14
|
+
@middlewares_namespaces = ['endpoint_flux/middlewares']
|
15
|
+
end
|
16
|
+
|
17
|
+
def interceptor(&block)
|
18
|
+
@interceptor ||= EndpointFlux::Config::Interceptor.new
|
19
|
+
@interceptor.add(&block) if block_given?
|
20
|
+
@interceptor
|
21
|
+
end
|
22
|
+
|
23
|
+
def flow(new_flow = nil)
|
24
|
+
@flow = new_flow if new_flow
|
25
|
+
|
26
|
+
@flow
|
27
|
+
end
|
28
|
+
|
29
|
+
def default_middlewares(name = nil, klass_name = nil)
|
30
|
+
if name
|
31
|
+
@default_middlewares[name] ||= []
|
32
|
+
|
33
|
+
if klass_name
|
34
|
+
klass = fetch_middleware_class!(name.to_s, klass_name.to_s)
|
35
|
+
@default_middlewares[name] << EndpointFlux::Config::Middleware.new(klass)
|
36
|
+
end
|
37
|
+
|
38
|
+
return @default_middlewares[name]
|
39
|
+
end
|
40
|
+
|
41
|
+
@default_middlewares
|
42
|
+
end
|
43
|
+
|
44
|
+
def rescue_from(klass_names = nil, &block)
|
45
|
+
if klass_names
|
46
|
+
if klass_names.respond_to?(:to_ary)
|
47
|
+
klass_names.to_ary || [klass_names]
|
48
|
+
else
|
49
|
+
[klass_names]
|
50
|
+
end.each do |klass_name|
|
51
|
+
klass = EndpointFlux::ClassLoader.load_class!(klass_name)
|
52
|
+
@rescue_from.add(klass, &block)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
@rescue_from
|
57
|
+
end
|
58
|
+
|
59
|
+
def endpoints_namespace(name = nil)
|
60
|
+
@endpoints_namespace = name if name
|
61
|
+
|
62
|
+
@endpoints_namespace
|
63
|
+
end
|
64
|
+
|
65
|
+
def fetch_middleware_class!(name, klass_name)
|
66
|
+
klass = fetch_middleware_class(name, klass_name)
|
67
|
+
|
68
|
+
raise "The [#{name}][#{klass_name}] should be a string representing a class" unless klass
|
69
|
+
|
70
|
+
klass
|
71
|
+
end
|
72
|
+
|
73
|
+
def fetch_middleware_class(name, klass_name)
|
74
|
+
middlewares_namespaces.each do |namespace|
|
75
|
+
klass = EndpointFlux::ClassLoader.load_class(
|
76
|
+
[namespace, name.to_s, klass_name.to_s].compact.join('/')
|
77
|
+
)
|
78
|
+
|
79
|
+
return klass if klass
|
80
|
+
end
|
81
|
+
|
82
|
+
nil
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module EndpointFlux
|
2
|
+
class Config
|
3
|
+
class Interceptor
|
4
|
+
attr_accessor :handlers
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@handlers = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def run(attrs)
|
11
|
+
handlers.each { |handler| handler.call(attrs) }
|
12
|
+
|
13
|
+
attrs
|
14
|
+
end
|
15
|
+
|
16
|
+
def add(&block)
|
17
|
+
raise 'Block not given' unless block_given?
|
18
|
+
|
19
|
+
handlers << block
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module EndpointFlux
|
2
|
+
class Config
|
3
|
+
class Middleware
|
4
|
+
attr_accessor :klass, :options, :handler
|
5
|
+
|
6
|
+
def initialize(klass = nil, options = {}, &block)
|
7
|
+
if klass
|
8
|
+
unless klass.methods.include?(:perform)
|
9
|
+
raise "The [#{klass}] class should define perform class method"
|
10
|
+
end
|
11
|
+
else
|
12
|
+
raise 'You must provide block or existing klass' unless block_given?
|
13
|
+
end
|
14
|
+
|
15
|
+
@klass = klass
|
16
|
+
@options = options
|
17
|
+
@handler = block
|
18
|
+
end
|
19
|
+
|
20
|
+
def run(attrs)
|
21
|
+
if klass
|
22
|
+
klass.perform(*attrs, options, &handler)
|
23
|
+
elsif handler
|
24
|
+
handler.call(*attrs, options)
|
25
|
+
else
|
26
|
+
raise "Unknown middleware type [#{inspect}]"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def ==(other)
|
31
|
+
return true if klass && klass == other.klass
|
32
|
+
return true if handler && handler == other.handler
|
33
|
+
|
34
|
+
false
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module EndpointFlux
|
2
|
+
class Config
|
3
|
+
class RescueFrom
|
4
|
+
attr_accessor :handlers, :exceptions
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@handlers = {}
|
8
|
+
@exceptions = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def run(name, attrs, e)
|
12
|
+
raise 'No handler given' unless exceptions.include?(e.class)
|
13
|
+
|
14
|
+
handlers[e.class.name].call(name, attrs, e)
|
15
|
+
end
|
16
|
+
|
17
|
+
def add(klass, &block)
|
18
|
+
raise 'Block not given' unless block_given?
|
19
|
+
|
20
|
+
handlers[klass.to_s] = block
|
21
|
+
|
22
|
+
exceptions << klass unless exceptions.include?(klass)
|
23
|
+
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'endpoint_flux/config'
|
2
|
+
require 'endpoint_flux/request'
|
3
|
+
require 'endpoint_flux/response'
|
4
|
+
|
5
|
+
module EndpointFlux
|
6
|
+
module Endpoint
|
7
|
+
def self.included(includer)
|
8
|
+
includer.extend self
|
9
|
+
includer.class_eval %( @middlewares ||= {} )
|
10
|
+
end
|
11
|
+
|
12
|
+
def perform(request)
|
13
|
+
attrs = [request, EndpointFlux::Response.new]
|
14
|
+
attrs = EndpointFlux.config.interceptor.run(attrs)
|
15
|
+
|
16
|
+
request.endpoint = to_s
|
17
|
+
|
18
|
+
flow.inject(attrs) do |res, middleware_name|
|
19
|
+
fetch_middleware(middleware_name).inject(res) do |middleware_res, middleware|
|
20
|
+
middleware.run(middleware_res)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
rescue *EndpointFlux.config.rescue_from.exceptions => e
|
24
|
+
EndpointFlux.config.rescue_from.run(name, attrs, e)
|
25
|
+
end
|
26
|
+
|
27
|
+
def flow(array = nil)
|
28
|
+
@flow = array.map(&:to_sym) if array
|
29
|
+
@flow || EndpointFlux.config.flow
|
30
|
+
end
|
31
|
+
|
32
|
+
def method_missing(method_id, *attrs, &block)
|
33
|
+
method_sym = method_id.to_sym
|
34
|
+
if flow.include?(method_sym)
|
35
|
+
configure_middleware(method_sym, attrs, &block)
|
36
|
+
else
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def respond_to_missing?(method_name)
|
42
|
+
flow.include?(method_name.to_sym)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def configure_middleware(middleware_name, attrs, &block)
|
48
|
+
@middlewares[middleware_name] ||= []
|
49
|
+
|
50
|
+
middleware = build_middleware(middleware_name, *attrs, &block)
|
51
|
+
@middlewares[middleware_name] << middleware if middleware
|
52
|
+
|
53
|
+
@middlewares[middleware_name]
|
54
|
+
end
|
55
|
+
|
56
|
+
def fetch_middleware(name)
|
57
|
+
@middlewares[name] ||
|
58
|
+
EndpointFlux.config.default_middlewares[name] ||
|
59
|
+
raise("No middleware registred for [#{name.inspect}]")
|
60
|
+
end
|
61
|
+
|
62
|
+
def build_middleware(middleware_name, *options, &block)
|
63
|
+
attrs = []
|
64
|
+
unless options.empty?
|
65
|
+
klass = EndpointFlux.config.fetch_middleware_class(middleware_name, options.first)
|
66
|
+
|
67
|
+
if klass
|
68
|
+
attrs << klass
|
69
|
+
attrs << options[1..-1]
|
70
|
+
else
|
71
|
+
attrs << nil
|
72
|
+
attrs << options
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
return if attrs.empty? && !block_given?
|
77
|
+
|
78
|
+
EndpointFlux::Config::Middleware.new(*attrs.flatten, &block)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module EndpointFlux
|
2
|
+
module Exceptions
|
3
|
+
require 'endpoint_flux/exceptions/base'
|
4
|
+
require 'endpoint_flux/exceptions/forbidden'
|
5
|
+
require 'endpoint_flux/exceptions/not_found'
|
6
|
+
require 'endpoint_flux/exceptions/service_unavailable'
|
7
|
+
require 'endpoint_flux/exceptions/unauthorized'
|
8
|
+
require 'endpoint_flux/exceptions/validation'
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module EndpointFlux
|
2
|
+
module Exceptions
|
3
|
+
class Base < StandardError
|
4
|
+
attr_accessor :messages
|
5
|
+
|
6
|
+
def initialize(messages = nil)
|
7
|
+
@messages = messages
|
8
|
+
|
9
|
+
super("#{self.class.name}: [ #{messages.inspect}]")
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_hash
|
13
|
+
raise NotImplementedError
|
14
|
+
end
|
15
|
+
|
16
|
+
def inspect
|
17
|
+
"#{self.class.name}: [ #{to_hash.inspect} ]"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|