kraftwerk 0.1.1
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.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.gitlab-ci.yml +20 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +3 -0
- data/Rakefile +10 -0
- data/kraftwerk.gemspec +41 -0
- data/lib/kraftwerk.rb +6 -0
- data/lib/kraftwerk/app.rb +66 -0
- data/lib/kraftwerk/app/components.rb +56 -0
- data/lib/kraftwerk/app/configuration.rb +25 -0
- data/lib/kraftwerk/app/dependencies.rb +16 -0
- data/lib/kraftwerk/endpoint.rb +20 -0
- data/lib/kraftwerk/endpoint/callable.rb +21 -0
- data/lib/kraftwerk/endpoint/error_handling.rb +38 -0
- data/lib/kraftwerk/endpoint/validatable.rb +45 -0
- data/lib/kraftwerk/logger/dev_logger.rb +53 -0
- data/lib/kraftwerk/middleware/reloader.rb +18 -0
- data/lib/kraftwerk/middleware/request_telemetry.rb +68 -0
- data/lib/kraftwerk/response.rb +13 -0
- data/lib/kraftwerk/response_formatter.rb +37 -0
- data/lib/kraftwerk/router.rb +36 -0
- data/lib/kraftwerk/telemetry.rb +56 -0
- data/lib/kraftwerk/version.rb +3 -0
- metadata +291 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 297caa3f44b655c645d35c4a8334eaac79b1c0c2908a7efe961307ce9a48eae2
|
4
|
+
data.tar.gz: 2dee1765d8923066f6f4ee18f14d056dc49b7f30527fe3da044a0d0d76817510
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9c5587a92d8288571cd30c7e00b8f26e8bcf04e4862ac40b1576c5dd888a87f819a867edb7bf47a0440084b3c85ac58fb6d89ce3000f76ffeffcbb89d24c5250
|
7
|
+
data.tar.gz: 86cdc0ff303e3dec5e9b69419603d274aafe840f0eb6098e33c8f1fd6a7ff49f6ec8c0564d8564cdc3314d902289e4bc87e8eb7bd67a6a3570a826407b7e77b2
|
data/.gitignore
ADDED
data/.gitlab-ci.yml
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
|
2
|
+
default:
|
3
|
+
before_script:
|
4
|
+
- ruby -v
|
5
|
+
- gem install bundler --no-document
|
6
|
+
- bundle install --jobs $(nproc) "${FLAGS[@]}"
|
7
|
+
|
8
|
+
test2.7:
|
9
|
+
image: "ruby:2.7"
|
10
|
+
script:
|
11
|
+
- bundle exec rake test
|
12
|
+
test3.0:
|
13
|
+
image: "ruby:3.0"
|
14
|
+
script:
|
15
|
+
- bundle exec rake test
|
16
|
+
|
17
|
+
lint:
|
18
|
+
image: "ruby:2.7"
|
19
|
+
script:
|
20
|
+
- bundle exec rubycritic -f lint -s 85
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Paweł Świątkowski
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
data/Rakefile
ADDED
data/kraftwerk.gemspec
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
lib = File.expand_path('lib', __dir__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'kraftwerk/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'kraftwerk'
|
7
|
+
spec.version = Kraftwerk::VERSION
|
8
|
+
spec.authors = ['Paweł Świątkowski']
|
9
|
+
spec.email = ['katafrakt@vivaldi.net']
|
10
|
+
|
11
|
+
spec.summary = 'Framework for crafting JSON APIs with style'
|
12
|
+
spec.homepage = 'https://gitlab.com/katafrakt/kraftwerk'
|
13
|
+
spec.license = 'MIT'
|
14
|
+
|
15
|
+
# Specify which files should be added to the gem when it is released.
|
16
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
17
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
18
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|example)/}) }
|
19
|
+
end
|
20
|
+
spec.bindir = 'exe'
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ['lib']
|
23
|
+
|
24
|
+
spec.add_dependency 'hanami-controller', '~> 2.0.0.alpha'
|
25
|
+
spec.add_dependency 'hanami-router', '~> 2.0.0.alpha4'
|
26
|
+
spec.add_dependency 'hanami-utils', '~> 2.0.0.alpha'
|
27
|
+
spec.add_dependency 'dry-validation', '~> 1.5'
|
28
|
+
spec.add_dependency 'dry-auto_inject'
|
29
|
+
spec.add_dependency 'dry-container', '~> 0.7'
|
30
|
+
spec.add_dependency 'dry-core', '~> 0.5'
|
31
|
+
spec.add_dependency 'zeitwerk'
|
32
|
+
|
33
|
+
spec.add_development_dependency 'bundler', '>= 1.16'
|
34
|
+
spec.add_development_dependency 'minitest', '~> 5.0'
|
35
|
+
spec.add_development_dependency 'rack-test', '~> 1.1'
|
36
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
37
|
+
spec.add_development_dependency 'rubocop'
|
38
|
+
spec.add_development_dependency 'rubycritic'
|
39
|
+
spec.add_development_dependency 'sequel'
|
40
|
+
spec.add_development_dependency 'sqlite3'
|
41
|
+
end
|
data/lib/kraftwerk.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'kraftwerk/router'
|
2
|
+
require 'kraftwerk/app/configuration'
|
3
|
+
require 'kraftwerk/app/components'
|
4
|
+
require 'kraftwerk/app/dependencies'
|
5
|
+
require 'hanami/middleware/body_parser'
|
6
|
+
require 'kraftwerk/middleware/reloader'
|
7
|
+
require 'hanami/utils/class_attribute'
|
8
|
+
require 'dry/container'
|
9
|
+
require 'dry/auto_inject'
|
10
|
+
|
11
|
+
module Kraftwerk
|
12
|
+
class App
|
13
|
+
RoutingAlreadyDefined = Class.new(StandardError)
|
14
|
+
DependenciesAlreadyDefined = Class.new(StandardError)
|
15
|
+
AlreadyConfigured = Class.new(StandardError)
|
16
|
+
|
17
|
+
include Hanami::Utils::ClassAttribute
|
18
|
+
class_attribute :_dependencies, :_routing, :_zeitwerk_loader
|
19
|
+
|
20
|
+
# configuration
|
21
|
+
class_attribute :autoload_paths, :reloading_enabled, :configuration
|
22
|
+
|
23
|
+
def self.inherited(base)
|
24
|
+
create_configuration!(base)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Initializes all the machinery
|
28
|
+
# Should only be called once
|
29
|
+
def self.initialize!
|
30
|
+
self::Configuration.freeze
|
31
|
+
create_components!
|
32
|
+
|
33
|
+
require 'kraftwerk/logger/dev_logger'
|
34
|
+
logger = Kraftwerk::Logger::DevLogger.new($stdout)
|
35
|
+
|
36
|
+
self::Components['telemetry'].attach(
|
37
|
+
'kraftwerk-request-start-log',
|
38
|
+
[:kraftwerk, :request, :start],
|
39
|
+
&logger.method(:handle_request_start)
|
40
|
+
)
|
41
|
+
self::Components['telemetry'].attach(
|
42
|
+
'kraftwerk-request-finish-log',
|
43
|
+
[:kraftwerk, :request, :finish],
|
44
|
+
&logger.method(:handle_request_finish)
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
class << self
|
49
|
+
def routing(&block)
|
50
|
+
raise RoutingAlreadyDefined unless self::Configuration.routing.nil?
|
51
|
+
self::Configuration.routing Kraftwerk::Router.new(&Proc.new(&block))
|
52
|
+
end
|
53
|
+
|
54
|
+
def dependencies(&block)
|
55
|
+
return self.create_dependencies!(&Proc.new(&block)) unless defined?(self::Dependencies)
|
56
|
+
|
57
|
+
raise DependenciesAlreadyDefined if block_given?
|
58
|
+
self::Dependencies
|
59
|
+
end
|
60
|
+
|
61
|
+
def rack_app
|
62
|
+
self::Components[:rack_app]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'dry/container'
|
2
|
+
require 'kraftwerk/telemetry'
|
3
|
+
require 'kraftwerk/middleware/request_telemetry'
|
4
|
+
|
5
|
+
module Kraftwerk
|
6
|
+
class App
|
7
|
+
# Creates components
|
8
|
+
# Configuration must be present and final at this point
|
9
|
+
def self.create_components!
|
10
|
+
kraftwerk_app = self
|
11
|
+
config = self::Configuration
|
12
|
+
klass = Class.new do
|
13
|
+
extend Dry::Container::Mixin
|
14
|
+
|
15
|
+
# Rack application for use in config.ru
|
16
|
+
register(:rack_app, memoize: true) do
|
17
|
+
router = resolve(:router)
|
18
|
+
reloader = resolve(:reloader)
|
19
|
+
|
20
|
+
Rack::Builder.new do
|
21
|
+
use Kraftwerk::Middleware::Reloader.new(reloader)
|
22
|
+
use Hanami::Middleware::BodyParser, :json
|
23
|
+
use Kraftwerk::Middleware::RequestTelemetry, kraftwerk_app
|
24
|
+
run router
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
register(:router, memoize: true) do
|
29
|
+
kraftwerk_app::Configuration.routing.routes
|
30
|
+
end
|
31
|
+
|
32
|
+
register(:reloader, memoize: true) do
|
33
|
+
if config.reloading_enabled
|
34
|
+
require 'zeitwerk'
|
35
|
+
loader = Zeitwerk::Loader.new
|
36
|
+
config.autoload_paths.each { |path| loader.push_dir(path) }
|
37
|
+
loader.enable_reloading
|
38
|
+
loader.setup
|
39
|
+
loader
|
40
|
+
else
|
41
|
+
# fake reloader
|
42
|
+
Class.new do
|
43
|
+
def reload; end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
register(:telemetry, memoize: true) do
|
49
|
+
Kraftwerk::Telemetry.new
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
self.const_set('Components', klass)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'dry/core/class_builder'
|
2
|
+
|
3
|
+
module Kraftwerk
|
4
|
+
class App
|
5
|
+
# Creates class holding app configuration
|
6
|
+
def self.create_configuration!(namespace)
|
7
|
+
klass = Class.new do
|
8
|
+
extend Dry::Core::ClassAttributes
|
9
|
+
|
10
|
+
defines :logger
|
11
|
+
logger ::Logger.new($stdout)
|
12
|
+
|
13
|
+
defines :reloading_enabled
|
14
|
+
reloading_enabled ENV['RACK_ENV'] != 'production'
|
15
|
+
|
16
|
+
defines :autoload_paths
|
17
|
+
autoload_paths File.directory?('endpoints') ? ['endpoints'] : []
|
18
|
+
|
19
|
+
defines :routing
|
20
|
+
end
|
21
|
+
|
22
|
+
namespace.const_set('Configuration', klass)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/core/class_builder'
|
4
|
+
|
5
|
+
module Kraftwerk
|
6
|
+
class App
|
7
|
+
def self.create_dependencies!(&block)
|
8
|
+
klass = Class.new do
|
9
|
+
extend Dry::Container::Mixin
|
10
|
+
instance_exec(&block)
|
11
|
+
end
|
12
|
+
|
13
|
+
self.const_set('Dependencies', Dry::AutoInject(klass))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'hanami/controller'
|
2
|
+
require 'kraftwerk/response_formatter'
|
3
|
+
require 'kraftwerk/endpoint/validatable'
|
4
|
+
require 'kraftwerk/endpoint/callable'
|
5
|
+
require 'kraftwerk/endpoint/error_handling'
|
6
|
+
|
7
|
+
module Kraftwerk
|
8
|
+
module Endpoint
|
9
|
+
def self.prepended(base_class)
|
10
|
+
base_class.class_eval do
|
11
|
+
include Hanami::Utils::ClassAttribute
|
12
|
+
include Hanami::Action::Rack
|
13
|
+
include Hanami::Action::Mime
|
14
|
+
prepend Validatable
|
15
|
+
prepend ErrorHandling
|
16
|
+
prepend Callable
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Kraftwerk
|
2
|
+
module Endpoint
|
3
|
+
# Replacement for Hanami::Action::Callable, which does not care about return value of the call method,
|
4
|
+
# instead requiring us to assign response via self.body= or via view.
|
5
|
+
# We don't want that in Kraftwerk. Return value from #call should be interpreted and returned by the framework.
|
6
|
+
module Callable
|
7
|
+
def call(env)
|
8
|
+
params = Hanami::Action::BaseParams.new(env)
|
9
|
+
begin
|
10
|
+
response = super(params)
|
11
|
+
rescue StandardError => e
|
12
|
+
puts e
|
13
|
+
puts e.backtrace.join("\n")
|
14
|
+
response = Kraftwerk::Response.new(code: 500, body: { error: 'Internal server error' })
|
15
|
+
end
|
16
|
+
|
17
|
+
ResponseFormatter.new.call(response: response, params: params)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Kraftwerk
|
2
|
+
module Endpoint
|
3
|
+
module ErrorHandling
|
4
|
+
def call(params)
|
5
|
+
begin
|
6
|
+
super
|
7
|
+
rescue => exception
|
8
|
+
if self.class.exception_handlers.key?(exception.class)
|
9
|
+
handler = self.class.exception_handlers[exception.class]
|
10
|
+
message = handler[:message].respond_to?(:call) ? handler[:message].call(exception) : handler[:message]
|
11
|
+
unless message.is_a?(Kraftwerk::Response)
|
12
|
+
message = Kraftwerk::Response.new(code: handler[:code], body: { error: message })
|
13
|
+
end
|
14
|
+
message
|
15
|
+
# todo call app expection handler somehow
|
16
|
+
else
|
17
|
+
raise exception
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.prepended(base)
|
23
|
+
base.class_eval do
|
24
|
+
class_attribute :exception_handlers
|
25
|
+
self.exception_handlers = {}
|
26
|
+
extend ClassMethods
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module ClassMethods
|
31
|
+
def handle_exception(exception_class, code: 422, message: nil)
|
32
|
+
body = message || Proc.new
|
33
|
+
self.exception_handlers[exception_class] = { code: code, message: body }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'dry/validation'
|
2
|
+
require 'kraftwerk/response'
|
3
|
+
|
4
|
+
module Kraftwerk
|
5
|
+
module Endpoint
|
6
|
+
module Validatable
|
7
|
+
# Unlike in Hanami, where we usually want to validate params manually and, if they are invalid,
|
8
|
+
# take a proper action, in Kraftwerk if validation is not passed, the request should not happen at all.
|
9
|
+
# Instead, error messages are returned.
|
10
|
+
def call(params)
|
11
|
+
validation_class = self.class.validation_class
|
12
|
+
return super if validation_class.nil?
|
13
|
+
|
14
|
+
result = validation_class.new.call(params.to_h)
|
15
|
+
if result.success?
|
16
|
+
# pass only whitelisted parameters down
|
17
|
+
super(result.to_h)
|
18
|
+
else
|
19
|
+
Response.new(code: 400, body: result.errors.to_h)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.prepended(base)
|
24
|
+
base.class_eval do
|
25
|
+
class_attribute :validation_class
|
26
|
+
extend ClassMethods
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module ClassMethods
|
31
|
+
def contract(&block)
|
32
|
+
klass = Class.new(Dry::Validation::Contract) do
|
33
|
+
instance_exec(&Proc.new(&block))
|
34
|
+
end
|
35
|
+
self.validation_class = klass
|
36
|
+
end
|
37
|
+
|
38
|
+
def validate_with(klass)
|
39
|
+
# TODO: add check if klass is proper validation class
|
40
|
+
self.validation_class = klass
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hanami/logger'
|
4
|
+
|
5
|
+
module Kraftwerk
|
6
|
+
module Logger
|
7
|
+
class DevLogger < ::Logger
|
8
|
+
def initialize(*args)
|
9
|
+
super
|
10
|
+
self.formatter = proc do |severity, datetime, progname, message|
|
11
|
+
message + "\n"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def handle_request_start(data, meta)
|
16
|
+
msg = String.new
|
17
|
+
msg << "[#{meta[:time]}]\n"
|
18
|
+
msg << color("[#{data[:method]}] ", :magenta)
|
19
|
+
msg << color(data[:path], :green)
|
20
|
+
msg << " "
|
21
|
+
msg << (data[:params]&.empty? ? '' : JSON.dump(data[:params]))
|
22
|
+
info(msg)
|
23
|
+
end
|
24
|
+
|
25
|
+
def handle_request_finish(data, meta)
|
26
|
+
msg = String.new
|
27
|
+
clr = data[:code] < 400 ? :green : :red
|
28
|
+
|
29
|
+
msg << color("[#{data[:code]}] ", clr)
|
30
|
+
msg << "[#{data[:duration].round(3)} ms] "
|
31
|
+
if !data[:body].nil? && data[:body].length > 0
|
32
|
+
msg << json_response(data[:body])
|
33
|
+
else
|
34
|
+
msg << "[empty response]"
|
35
|
+
end
|
36
|
+
|
37
|
+
info(msg + "\n")
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def color(text, color)
|
43
|
+
Hanami::Utils::ShellColor.call(text, color: color)
|
44
|
+
end
|
45
|
+
|
46
|
+
def json_response(response)
|
47
|
+
JSON.dump(JSON.parse(response))
|
48
|
+
rescue
|
49
|
+
"[non-JSON response: #{response}]"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Kraftwerk
|
2
|
+
module Middleware
|
3
|
+
module Reloader
|
4
|
+
def self.new(reloader)
|
5
|
+
Class.new do
|
6
|
+
define_method :initialize do |app|
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
define_method :call do |env|
|
11
|
+
reloader.reload
|
12
|
+
@app.call(env)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kraftwerk
|
4
|
+
module Middleware
|
5
|
+
class RequestTelemetry
|
6
|
+
ROUTER_PARAMS = 'router.params'
|
7
|
+
PATH = 'PATH_INFO'
|
8
|
+
METHOD = 'REQUEST_METHOD'
|
9
|
+
QUERY_STRING = 'QUERY_STRING'
|
10
|
+
RACK_INPUT = 'rack.input'
|
11
|
+
|
12
|
+
attr_reader :app, :kraftwerk_app, :telemetry
|
13
|
+
|
14
|
+
def initialize(app, kraftwerk_app, telemetry: nil)
|
15
|
+
@app = app
|
16
|
+
@kraftwerk_app = kraftwerk_app
|
17
|
+
@telemetry ||= kraftwerk_app::Components['telemetry']
|
18
|
+
end
|
19
|
+
|
20
|
+
def call(env)
|
21
|
+
metadata = { app: kraftwerk_app, time: Time.now }
|
22
|
+
start_data = request_start_data(env)
|
23
|
+
telemetry.execute([:kraftwerk, :request, :start], start_data, metadata)
|
24
|
+
|
25
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
26
|
+
app.call(env).tap do |response|
|
27
|
+
telemetry.execute(
|
28
|
+
[:kraftwerk, :request, :finish],
|
29
|
+
request_finish_data(start_data, response, start_time),
|
30
|
+
metadata
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def request_start_data(env)
|
38
|
+
{
|
39
|
+
method: env[METHOD],
|
40
|
+
path: env[PATH],
|
41
|
+
params: request_params(env)
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def request_finish_data(start_data, response, start_time)
|
46
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
47
|
+
code, _headers, body = response
|
48
|
+
|
49
|
+
start_data.merge(
|
50
|
+
{
|
51
|
+
duration: duration,
|
52
|
+
code: code,
|
53
|
+
body: body[0]
|
54
|
+
}
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Combines params from query string and body into a single hash
|
59
|
+
def request_params(env)
|
60
|
+
params_from_query_string = env[QUERY_STRING] ? CGI.parse(env[QUERY_STRING]) : {}
|
61
|
+
rack_input = env[RACK_INPUT].dup.read
|
62
|
+
params_from_input = rack_input.length > 0 ? CGI.parse(rack_input) : {}
|
63
|
+
params_from_router = env[ROUTER_PARAMS] || {}
|
64
|
+
params_from_query_string.merge(params_from_input).merge(params_from_router)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Kraftwerk
|
2
|
+
class Response
|
3
|
+
attr_reader :code, :body, :headers, :body_raw
|
4
|
+
|
5
|
+
# TODO: support cookies nicely
|
6
|
+
def initialize(code: nil, body: nil, headers: {}, body_raw: false)
|
7
|
+
@code = code
|
8
|
+
@body = body
|
9
|
+
@headers = headers
|
10
|
+
@body_raw = body_raw
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Kraftwerk
|
4
|
+
class ResponseFormatter
|
5
|
+
def call(response:, params:)
|
6
|
+
case response
|
7
|
+
when Kraftwerk::Response
|
8
|
+
body = response.body_raw ? response.body : to_json(response.body)
|
9
|
+
code = code_or_default(response.code, params)
|
10
|
+
headers = response.headers
|
11
|
+
[code, headers, [body]]
|
12
|
+
else
|
13
|
+
body = JSON.dump(response)
|
14
|
+
[code_or_default(nil, params), {}, [body]]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def to_json(response)
|
21
|
+
JSON.dump(response)
|
22
|
+
end
|
23
|
+
|
24
|
+
def code_or_default(code, params)
|
25
|
+
return code unless code.nil?
|
26
|
+
|
27
|
+
method = params.env['REQUEST_METHOD']
|
28
|
+
case method
|
29
|
+
when 'GET' then 200
|
30
|
+
when 'POST' then 201
|
31
|
+
when 'PUT', 'PATCH' then 204
|
32
|
+
when 'DELETE' then 204
|
33
|
+
else 200
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'hanami/router'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Kraftwerk
|
5
|
+
class Router
|
6
|
+
DEFAULT_RESPONSE = [
|
7
|
+
404,
|
8
|
+
{
|
9
|
+
'X-Cascade' => 'pass',
|
10
|
+
'Content-Type' => 'application/json'
|
11
|
+
},
|
12
|
+
[JSON.dump(error: 'not found')]
|
13
|
+
].freeze
|
14
|
+
|
15
|
+
attr_reader :routes
|
16
|
+
|
17
|
+
def initialize(&block)
|
18
|
+
@routes = create_routing(&Proc.new(&block))
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def create_routing(&block)
|
24
|
+
# default_app is an undocumented feature of Hanami router coming from
|
25
|
+
# HttpRouter library, which it relies upon.
|
26
|
+
# It sets a default handler when no route can be matched.
|
27
|
+
# See: https://github.com/hanami/router/issues/119
|
28
|
+
#
|
29
|
+
# In hanami-router 2.0 it has been renamed to not_found
|
30
|
+
default_app = ->(_env) { DEFAULT_RESPONSE }
|
31
|
+
@routes = Hanami::Router.new(not_found: default_app) do
|
32
|
+
instance_exec(&Proc.new(&block))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kraftwerk
|
4
|
+
# Based on
|
5
|
+
# * BEAM Telemetry module
|
6
|
+
# * Work of Tony Pitale: https://github.com/tpitale/telemetry-ruby
|
7
|
+
class Telemetry
|
8
|
+
HandlerIdAlreadyUsed = Class.new(RuntimeError)
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@handlers = Concurrent::Map.new
|
12
|
+
@used_ids = Concurrent::Map.new
|
13
|
+
@semaphore = Concurrent::Semaphore.new(1)
|
14
|
+
end
|
15
|
+
|
16
|
+
def attach(id, event_key, &handler)
|
17
|
+
raise HandlerIdAlreadyUsed.new(id) if @used_ids.key?(id)
|
18
|
+
|
19
|
+
@handlers[event_key] ||= Concurrent::Array.new
|
20
|
+
@handlers[event_key] << { id: id, handler: handler }
|
21
|
+
sync_used_ids_cache
|
22
|
+
end
|
23
|
+
|
24
|
+
def detach(id)
|
25
|
+
event_key = @used_ids[id]
|
26
|
+
@handlers[event_key].delete_if { |handler| handler[:id] == id }
|
27
|
+
sync_used_ids_cache
|
28
|
+
end
|
29
|
+
|
30
|
+
def execute(event_key, values = {}, meta = {})
|
31
|
+
handlers = @handlers[event_key]
|
32
|
+
return if handlers.nil?
|
33
|
+
|
34
|
+
handlers.each do |handler|
|
35
|
+
handler[:handler].call(values, {_event_key: event_key}.merge(meta))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def sync_used_ids_cache
|
42
|
+
ids = Concurrent::Map.new
|
43
|
+
@semaphore.acquire
|
44
|
+
@handlers.each do |key, handlers|
|
45
|
+
handlers.each do |handler|
|
46
|
+
id = handler[:id]
|
47
|
+
ids[id] = key
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
@used_ids = ids
|
52
|
+
ensure
|
53
|
+
@semaphore.release
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
metadata
ADDED
@@ -0,0 +1,291 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: kraftwerk
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Paweł Świątkowski
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-01-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: hanami-controller
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.0.0.alpha
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.0.0.alpha
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: hanami-router
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.0.0.alpha4
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 2.0.0.alpha4
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: hanami-utils
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 2.0.0.alpha
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.0.0.alpha
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: dry-validation
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.5'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.5'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: dry-auto_inject
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: dry-container
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.7'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.7'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: dry-core
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.5'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.5'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: zeitwerk
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: bundler
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '1.16'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '1.16'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: minitest
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '5.0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '5.0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: rack-test
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '1.1'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '1.1'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: rake
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - "~>"
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '13.0'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - "~>"
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '13.0'
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: rubocop
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - ">="
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '0'
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - ">="
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '0'
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
name: rubycritic
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - ">="
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: '0'
|
202
|
+
type: :development
|
203
|
+
prerelease: false
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - ">="
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: '0'
|
209
|
+
- !ruby/object:Gem::Dependency
|
210
|
+
name: sequel
|
211
|
+
requirement: !ruby/object:Gem::Requirement
|
212
|
+
requirements:
|
213
|
+
- - ">="
|
214
|
+
- !ruby/object:Gem::Version
|
215
|
+
version: '0'
|
216
|
+
type: :development
|
217
|
+
prerelease: false
|
218
|
+
version_requirements: !ruby/object:Gem::Requirement
|
219
|
+
requirements:
|
220
|
+
- - ">="
|
221
|
+
- !ruby/object:Gem::Version
|
222
|
+
version: '0'
|
223
|
+
- !ruby/object:Gem::Dependency
|
224
|
+
name: sqlite3
|
225
|
+
requirement: !ruby/object:Gem::Requirement
|
226
|
+
requirements:
|
227
|
+
- - ">="
|
228
|
+
- !ruby/object:Gem::Version
|
229
|
+
version: '0'
|
230
|
+
type: :development
|
231
|
+
prerelease: false
|
232
|
+
version_requirements: !ruby/object:Gem::Requirement
|
233
|
+
requirements:
|
234
|
+
- - ">="
|
235
|
+
- !ruby/object:Gem::Version
|
236
|
+
version: '0'
|
237
|
+
description:
|
238
|
+
email:
|
239
|
+
- katafrakt@vivaldi.net
|
240
|
+
executables: []
|
241
|
+
extensions: []
|
242
|
+
extra_rdoc_files: []
|
243
|
+
files:
|
244
|
+
- ".gitignore"
|
245
|
+
- ".gitlab-ci.yml"
|
246
|
+
- Gemfile
|
247
|
+
- LICENSE.txt
|
248
|
+
- README.md
|
249
|
+
- Rakefile
|
250
|
+
- kraftwerk.gemspec
|
251
|
+
- lib/kraftwerk.rb
|
252
|
+
- lib/kraftwerk/app.rb
|
253
|
+
- lib/kraftwerk/app/components.rb
|
254
|
+
- lib/kraftwerk/app/configuration.rb
|
255
|
+
- lib/kraftwerk/app/dependencies.rb
|
256
|
+
- lib/kraftwerk/endpoint.rb
|
257
|
+
- lib/kraftwerk/endpoint/callable.rb
|
258
|
+
- lib/kraftwerk/endpoint/error_handling.rb
|
259
|
+
- lib/kraftwerk/endpoint/validatable.rb
|
260
|
+
- lib/kraftwerk/logger/dev_logger.rb
|
261
|
+
- lib/kraftwerk/middleware/reloader.rb
|
262
|
+
- lib/kraftwerk/middleware/request_telemetry.rb
|
263
|
+
- lib/kraftwerk/response.rb
|
264
|
+
- lib/kraftwerk/response_formatter.rb
|
265
|
+
- lib/kraftwerk/router.rb
|
266
|
+
- lib/kraftwerk/telemetry.rb
|
267
|
+
- lib/kraftwerk/version.rb
|
268
|
+
homepage: https://gitlab.com/katafrakt/kraftwerk
|
269
|
+
licenses:
|
270
|
+
- MIT
|
271
|
+
metadata: {}
|
272
|
+
post_install_message:
|
273
|
+
rdoc_options: []
|
274
|
+
require_paths:
|
275
|
+
- lib
|
276
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
277
|
+
requirements:
|
278
|
+
- - ">="
|
279
|
+
- !ruby/object:Gem::Version
|
280
|
+
version: '0'
|
281
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
282
|
+
requirements:
|
283
|
+
- - ">="
|
284
|
+
- !ruby/object:Gem::Version
|
285
|
+
version: '0'
|
286
|
+
requirements: []
|
287
|
+
rubygems_version: 3.2.3
|
288
|
+
signing_key:
|
289
|
+
specification_version: 4
|
290
|
+
summary: Framework for crafting JSON APIs with style
|
291
|
+
test_files: []
|