kraftwerk 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|