edgeway 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +444 -0
- data/exe/edgeway +6 -0
- data/js/cgi.js +157 -0
- data/js/cloudflare.js +103 -0
- data/js/cors.js +87 -0
- data/js/jwt.js +279 -0
- data/js/runtime.js +99 -0
- data/js/wasi-shim.js +191 -0
- data/lib/edgeway/application.rb +70 -0
- data/lib/edgeway/cli/app_config.rb +35 -0
- data/lib/edgeway/cli/app_files.rb +37 -0
- data/lib/edgeway/cli/build.rb +90 -0
- data/lib/edgeway/cli/generate.rb +160 -0
- data/lib/edgeway/cli/new.rb +43 -0
- data/lib/edgeway/cli/package_manager.rb +21 -0
- data/lib/edgeway/cli/precheck.rb +45 -0
- data/lib/edgeway/cli/routes.rb +22 -0
- data/lib/edgeway/cli/test.rb +33 -0
- data/lib/edgeway/cli/toolchain.rb +35 -0
- data/lib/edgeway/cli.rb +59 -0
- data/lib/edgeway/config.rb +9 -0
- data/lib/edgeway/controller/authentication.rb +42 -0
- data/lib/edgeway/controller/base.rb +95 -0
- data/lib/edgeway/controller/callbacks.rb +29 -0
- data/lib/edgeway/controller/dispatcher.rb +39 -0
- data/lib/edgeway/controller/rendering.rb +65 -0
- data/lib/edgeway/controller/rescuable.rb +26 -0
- data/lib/edgeway/db/adapter.rb +32 -0
- data/lib/edgeway/db/memory.rb +73 -0
- data/lib/edgeway/db/sqlite.rb +190 -0
- data/lib/edgeway/errors.rb +61 -0
- data/lib/edgeway/generators/app/templates/.gitignore +6 -0
- data/lib/edgeway/generators/app/templates/Gemfile.tt +4 -0
- data/lib/edgeway/generators/app/templates/README.md.tt +22 -0
- data/lib/edgeway/generators/app/templates/app/controllers/application_controller.rb +9 -0
- data/lib/edgeway/generators/app/templates/app/controllers/health_controller.rb +3 -0
- data/lib/edgeway/generators/app/templates/app/models/application_model.rb +3 -0
- data/lib/edgeway/generators/app/templates/bin/build.tt +7 -0
- data/lib/edgeway/generators/app/templates/bin/deploy.tt +10 -0
- data/lib/edgeway/generators/app/templates/bin/setup.tt +38 -0
- data/lib/edgeway/generators/app/templates/config/database.yml +14 -0
- data/lib/edgeway/generators/app/templates/config/routes.rb +4 -0
- data/lib/edgeway/generators/app/templates/db/migrations/.keep +0 -0
- data/lib/edgeway/generators/app/templates/mise.toml.tt +24 -0
- data/lib/edgeway/generators/app/templates/package.json.tt +7 -0
- data/lib/edgeway/generators/app/templates/src/worker.js +4 -0
- data/lib/edgeway/generators/app/templates/test/controllers/health_test.rb +16 -0
- data/lib/edgeway/generators/app/templates/test/test_helper.rb +1 -0
- data/lib/edgeway/generators/app/templates/wrangler.toml.tt +23 -0
- data/lib/edgeway/http/auth/jwt.rb +127 -0
- data/lib/edgeway/http/auth/stub.rb +29 -0
- data/lib/edgeway/http/json_body.rb +37 -0
- data/lib/edgeway/http/parameters.rb +42 -0
- data/lib/edgeway/http/request.rb +115 -0
- data/lib/edgeway/http/response.rb +46 -0
- data/lib/edgeway/inflector.rb +16 -0
- data/lib/edgeway/logger.rb +21 -0
- data/lib/edgeway/model/attributes.rb +85 -0
- data/lib/edgeway/model/base.rb +173 -0
- data/lib/edgeway/model/macros.rb +60 -0
- data/lib/edgeway/model/persistence.rb +154 -0
- data/lib/edgeway/model/relation.rb +295 -0
- data/lib/edgeway/model/validations.rb +136 -0
- data/lib/edgeway/routing/mapper.rb +57 -0
- data/lib/edgeway/routing/route.rb +39 -0
- data/lib/edgeway/routing/route_set.rb +27 -0
- data/lib/edgeway/server.rb +12 -0
- data/lib/edgeway/service/cloudflare/d1.rb +101 -0
- data/lib/edgeway/service/cloudflare/jwt.rb +44 -0
- data/lib/edgeway/test.rb +97 -0
- data/lib/edgeway/version.rb +5 -0
- data/lib/edgeway.rb +30 -0
- metadata +115 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
|
|
6
|
+
module Edgeway
|
|
7
|
+
module CLI
|
|
8
|
+
module Precheck
|
|
9
|
+
def self.sqlite!
|
|
10
|
+
return if ENV["EDGEWAY_SKIP_PRECHECK"] == "1"
|
|
11
|
+
|
|
12
|
+
cc = compiler
|
|
13
|
+
return if cc.nil? || sqlite_links?(cc)
|
|
14
|
+
|
|
15
|
+
raise Error, <<~MSG.chomp
|
|
16
|
+
libsqlite3 cannot be linked (`#{cc.join(" ")} -lsqlite3` failed); `edgeway test` needs the SQLite development library.
|
|
17
|
+
Debian/Ubuntu: sudo apt install libsqlite3-dev
|
|
18
|
+
Fedora: sudo dnf install sqlite-devel
|
|
19
|
+
macOS: brew install sqlite
|
|
20
|
+
If it is installed somewhere else, set LIBRARY_PATH to a directory holding libsqlite3.so.
|
|
21
|
+
Set EDGEWAY_SKIP_PRECHECK=1 to skip this check.
|
|
22
|
+
MSG
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The compiler command as an argv array, or nil when there is none.
|
|
26
|
+
def self.compiler
|
|
27
|
+
env = ENV["CC"].to_s.strip
|
|
28
|
+
unless env.empty?
|
|
29
|
+
argv = Shellwords.split(env)
|
|
30
|
+
return argv if !argv.empty? && (argv[0].include?("/") ? File.executable?(argv[0]) : Toolchain.find_on_path(argv[0]))
|
|
31
|
+
end
|
|
32
|
+
%w[cc gcc clang].each { |n| return [n] if Toolchain.find_on_path(n) }
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.sqlite_links?(cc)
|
|
37
|
+
Dir.mktmpdir("edgeway-precheck") do |dir|
|
|
38
|
+
src = File.join(dir, "t.c")
|
|
39
|
+
File.write(src, "int main(){return 0;}\n")
|
|
40
|
+
system(*cc, src, "-o", File.join(dir, "t"), "-lsqlite3", out: File::NULL, err: File::NULL) ? true : false
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edgeway
|
|
4
|
+
module CLI
|
|
5
|
+
class Routes
|
|
6
|
+
def self.run(_args)
|
|
7
|
+
path = File.join(Dir.pwd, "config/routes.rb")
|
|
8
|
+
raise Error, "no config/routes.rb in #{Dir.pwd}" unless File.exist?(path)
|
|
9
|
+
|
|
10
|
+
$LOAD_PATH.unshift(File.join(CLI.gem_dir, "lib"))
|
|
11
|
+
require "edgeway"
|
|
12
|
+
load path
|
|
13
|
+
list = Edgeway.application.routes.list
|
|
14
|
+
verb_w = list.map { |r| r.verb.size }.max.to_i
|
|
15
|
+
path_w = list.map { |r| r.pattern.size }.max.to_i
|
|
16
|
+
list.each do |r|
|
|
17
|
+
puts "#{r.verb.ljust(verb_w)} #{r.pattern.ljust(path_w)} #{r.controller_path}##{r.action}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edgeway
|
|
4
|
+
module CLI
|
|
5
|
+
class Test
|
|
6
|
+
def self.run(args)
|
|
7
|
+
root = Dir.pwd
|
|
8
|
+
Precheck.sqlite!
|
|
9
|
+
new(root).write_test_app
|
|
10
|
+
exec("spinel_test-unit", "-I", root, "-I", CLI.gem_dir + "/lib", *args)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(root)
|
|
14
|
+
@root = root
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def write_test_app
|
|
18
|
+
raise Error, "no config/routes.rb in #{@root}" unless File.exist?(File.join(@root, "config/routes.rb"))
|
|
19
|
+
|
|
20
|
+
FileUtils.mkdir_p(File.join(@root, "tmp"))
|
|
21
|
+
lines = ["# Generated by `edgeway test` from app/ and config/; do not edit.", 'require "edgeway"']
|
|
22
|
+
lines << 'require "edgeway/db/memory"'
|
|
23
|
+
lines << 'require "edgeway/db/sqlite"'
|
|
24
|
+
# Where the migrations are read from at test time (the binary may run
|
|
25
|
+
# from another directory); Edgeway::UnitTest#setup applies them.
|
|
26
|
+
lines << "module Edgeway; TEST_APP_ROOT = #{@root.dump}; end"
|
|
27
|
+
lines << ""
|
|
28
|
+
lines.concat(AppFiles.new(@root).require_lines)
|
|
29
|
+
File.write(File.join(@root, "tmp/test_app.rb"), lines.join("\n") + "\n")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Edgeway
|
|
4
|
+
module CLI
|
|
5
|
+
# Finds the Spinel compiler, for `edgeway build` and the Rakefile.
|
|
6
|
+
#
|
|
7
|
+
# 1. SPINEL (a Spinel checkout, for example)
|
|
8
|
+
# 2. spinel on PATH (mise puts its spinel there when it is activated)
|
|
9
|
+
# 3. `mise which spinel` (mise is installed but not activated)
|
|
10
|
+
#
|
|
11
|
+
# The WASI SDK that --target=wasm32-wasi needs is Spinel's to find: it
|
|
12
|
+
# reads WASI_SDK (mise.toml sets it), then WASI_SDK_PATH, and defaults to
|
|
13
|
+
# /opt/wasi-sdk.
|
|
14
|
+
module Toolchain
|
|
15
|
+
def self.spinel
|
|
16
|
+
env = ENV["SPINEL"]
|
|
17
|
+
return env unless env.nil? || env.empty?
|
|
18
|
+
|
|
19
|
+
find_on_path("spinel") || mise_which("spinel")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.find_on_path(name)
|
|
23
|
+
ENV["PATH"].to_s.split(File::PATH_SEPARATOR).map { |d| File.join(d, name) }
|
|
24
|
+
.find { |f| File.file?(f) && File.executable?(f) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.mise_which(name)
|
|
28
|
+
return nil unless find_on_path("mise")
|
|
29
|
+
|
|
30
|
+
out = IO.popen(["mise", "which", name], err: File::NULL, &:read).strip
|
|
31
|
+
$?.success? && !out.empty? ? out : nil
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/edgeway/cli.rb
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The edgeway command (CRuby). Not part of the wasm: lib/edgeway.rb does not
|
|
4
|
+
# require it.
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "yaml"
|
|
7
|
+
require_relative "version"
|
|
8
|
+
require_relative "cli/app_config"
|
|
9
|
+
require_relative "cli/toolchain"
|
|
10
|
+
require_relative "cli/app_files"
|
|
11
|
+
require_relative "cli/build"
|
|
12
|
+
require_relative "cli/precheck"
|
|
13
|
+
require_relative "cli/test"
|
|
14
|
+
require_relative "cli/routes"
|
|
15
|
+
require_relative "cli/package_manager"
|
|
16
|
+
require_relative "cli/generate"
|
|
17
|
+
require_relative "cli/new"
|
|
18
|
+
|
|
19
|
+
module Edgeway
|
|
20
|
+
module CLI
|
|
21
|
+
USAGE = <<~TEXT
|
|
22
|
+
Usage: edgeway COMMAND [options]
|
|
23
|
+
|
|
24
|
+
new APP create a new app in APP/
|
|
25
|
+
build [--env ENV] compile the app to build/app.wasm (ENV: development)
|
|
26
|
+
test [ARGS] regenerate tmp/test_app.rb, run test/ with spinel_test-unit
|
|
27
|
+
generate, g KIND NAME generate a model (NAME field:type ...) or a controller (NAME action ...)
|
|
28
|
+
routes print the routes
|
|
29
|
+
version print the version
|
|
30
|
+
TEXT
|
|
31
|
+
|
|
32
|
+
class Error < StandardError; end
|
|
33
|
+
|
|
34
|
+
# The directory of the gem: lib/ is what Spinel gets as -I, js/ is
|
|
35
|
+
# copied into the app's build/.
|
|
36
|
+
def self.gem_dir
|
|
37
|
+
File.expand_path("../..", __dir__)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.start(argv)
|
|
41
|
+
command, *args = argv
|
|
42
|
+
case command
|
|
43
|
+
when "new" then New.run(args)
|
|
44
|
+
when "build" then Build.run(args)
|
|
45
|
+
when "test" then Test.run(args)
|
|
46
|
+
when "generate", "g" then Generate.run(args)
|
|
47
|
+
when "routes" then Routes.run(args)
|
|
48
|
+
when "version", "-v", "--version" then puts "edgeway #{Edgeway::VERSION}"
|
|
49
|
+
when nil, "help", "-h", "--help" then puts USAGE
|
|
50
|
+
else
|
|
51
|
+
warn "edgeway: unknown command #{command}\n\n#{USAGE}"
|
|
52
|
+
exit 1
|
|
53
|
+
end
|
|
54
|
+
rescue Error => e
|
|
55
|
+
warn "edgeway: #{e.message}"
|
|
56
|
+
exit 1
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Edgeway
|
|
2
|
+
module Controller
|
|
3
|
+
# Bearer-token authentication:
|
|
4
|
+
#
|
|
5
|
+
# class ApplicationController < Edgeway::Controller::Base
|
|
6
|
+
# before_action :authenticate!
|
|
7
|
+
# end
|
|
8
|
+
# # or before_action :authenticate!, only: [:create]
|
|
9
|
+
#
|
|
10
|
+
# authenticate! answers 401 unless the request has an
|
|
11
|
+
# "Authorization: Bearer <JWT>" whose token passes Edgeway::Auth; the
|
|
12
|
+
# claims are then in current_claims.
|
|
13
|
+
module Authentication
|
|
14
|
+
# Raises Unauthorized (no token) or InvalidToken. Both are 401 with a
|
|
15
|
+
# WWW-Authenticate header, unless the app rescue_froms them.
|
|
16
|
+
def authenticate!
|
|
17
|
+
token = bearer_token
|
|
18
|
+
raise Unauthorized, "authentication required" if token.empty?
|
|
19
|
+
@current_claims = Auth.decode(token)
|
|
20
|
+
@authenticated = true
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The token's claims (a Hash of the JSON payload), {} when the request
|
|
25
|
+
# was not authenticated.
|
|
26
|
+
def current_claims
|
|
27
|
+
@authenticated ? @current_claims : {}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def authenticated? = @authenticated
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def bearer_token
|
|
35
|
+
value = @request.header("Authorization")
|
|
36
|
+
scheme = "Bearer "
|
|
37
|
+
return "" unless value.length > scheme.length && value[0, scheme.length].downcase == "bearer "
|
|
38
|
+
value[scheme.length, value.length - scheme.length].strip
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
module Edgeway
|
|
2
|
+
module Controller
|
|
3
|
+
class Base
|
|
4
|
+
include Callbacks
|
|
5
|
+
include Rescuable
|
|
6
|
+
include Rendering
|
|
7
|
+
include Authentication
|
|
8
|
+
|
|
9
|
+
NON_ACTIONS = ["render", "head", "headers", "params", "request", "performed?", "to_response",
|
|
10
|
+
"action?", "run_before_actions", "rescue_with_handler",
|
|
11
|
+
"authenticate!", "current_claims", "authenticated?"]
|
|
12
|
+
|
|
13
|
+
def self.before_action(name, only: [], except: [])
|
|
14
|
+
@own_before_actions ||= []
|
|
15
|
+
@own_before_actions << Callback.new(name, only.map(&:to_s), except.map(&:to_s))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.own_before_actions = (@own_before_actions || [])
|
|
19
|
+
|
|
20
|
+
def self.rescue_from(error_class, with:)
|
|
21
|
+
@own_handlers ||= []
|
|
22
|
+
@own_handlers << Handler.new(error_class, with)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.own_handlers = (@own_handlers || [])
|
|
26
|
+
|
|
27
|
+
def self.lineage
|
|
28
|
+
chain = []
|
|
29
|
+
k = self
|
|
30
|
+
while k
|
|
31
|
+
chain.unshift(k)
|
|
32
|
+
break if k == Base
|
|
33
|
+
k = k.superclass
|
|
34
|
+
end
|
|
35
|
+
chain
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.all_before_actions
|
|
39
|
+
all = []
|
|
40
|
+
lineage.each { |c| c.own_before_actions.each { |cb| all << cb } }
|
|
41
|
+
all
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.all_handlers
|
|
45
|
+
all = []
|
|
46
|
+
lineage.each { |c| c.own_handlers.each { |h| all << h } }
|
|
47
|
+
all
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
attr_reader :request
|
|
51
|
+
|
|
52
|
+
def initialize(request)
|
|
53
|
+
@request = request
|
|
54
|
+
@status = 200
|
|
55
|
+
@headers = {}
|
|
56
|
+
@response_body = ""
|
|
57
|
+
@performed = false
|
|
58
|
+
@params = nil
|
|
59
|
+
@current_claims = {}
|
|
60
|
+
@authenticated = false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def headers = @headers
|
|
64
|
+
|
|
65
|
+
def action?(name)
|
|
66
|
+
self.respond_to?(name.to_sym) && !NON_ACTIONS.include?(name)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def params
|
|
70
|
+
@params ||= wrapped_params
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def wrapped_params
|
|
76
|
+
all = @request.params
|
|
77
|
+
scope = self.class.param_scope
|
|
78
|
+
prefix = "#{scope}["
|
|
79
|
+
return all unless @request.json?
|
|
80
|
+
return all if @request.body_params.keys.any? { |k| k.start_with?(prefix) }
|
|
81
|
+
wrapped = HTTP::Parameters.new(all.to_h.dup)
|
|
82
|
+
@request.body_params.each do |k, v|
|
|
83
|
+
wrapped["#{scope}[#{k}]"] = v unless k.include?("[")
|
|
84
|
+
end
|
|
85
|
+
wrapped
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def self.param_scope
|
|
89
|
+
base = name.split("::").last.sub(/Controller\z/, "")
|
|
90
|
+
base = base.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
|
|
91
|
+
Inflector.singularize(base)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Edgeway
|
|
2
|
+
module Controller
|
|
3
|
+
class Callback
|
|
4
|
+
attr_reader :name
|
|
5
|
+
|
|
6
|
+
def initialize(name, only, except)
|
|
7
|
+
@name = name
|
|
8
|
+
@only = only
|
|
9
|
+
@except = except
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def applies?(action)
|
|
13
|
+
return false if @except.include?(action)
|
|
14
|
+
@only.empty? || @only.include?(action)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module Callbacks
|
|
19
|
+
def run_before_actions(action)
|
|
20
|
+
self.class.all_before_actions.each do |callback|
|
|
21
|
+
next unless callback.applies?(action)
|
|
22
|
+
send(callback.name)
|
|
23
|
+
return false if performed?
|
|
24
|
+
end
|
|
25
|
+
true
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Edgeway
|
|
2
|
+
module Controller
|
|
3
|
+
module Dispatcher
|
|
4
|
+
def self.controller_class(route)
|
|
5
|
+
klass = nil
|
|
6
|
+
begin
|
|
7
|
+
klass = Object.const_get(route.controller_class_name)
|
|
8
|
+
rescue NameError
|
|
9
|
+
return nil
|
|
10
|
+
end
|
|
11
|
+
klass.is_a?(Class) && klass < Base ? klass : nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.dispatch(route, request)
|
|
15
|
+
klass = controller_class(route)
|
|
16
|
+
raise RoutingError, "no controller for #{route.controller_path}" if klass.nil?
|
|
17
|
+
controller = klass.new(request)
|
|
18
|
+
# A route whose action isn't defined (or is one of Base's own public
|
|
19
|
+
# methods) is a 404, as in Rails.
|
|
20
|
+
raise RoutingError, "no route for #{request.method} #{request.path}" unless controller.action?(route.action)
|
|
21
|
+
begin
|
|
22
|
+
# The action is called here, on an instance of a class found by
|
|
23
|
+
# name, and not from a Base method: with a name taken as a method
|
|
24
|
+
# argument Spinel's send looks among the literals of the program,
|
|
25
|
+
# and misses a subclass's methods (matz/spinel#8592). public_send:
|
|
26
|
+
# a route can't reach a private method.
|
|
27
|
+
action = route.action.to_sym
|
|
28
|
+
controller.public_send(action) if controller.run_before_actions(route.action)
|
|
29
|
+
controller.head(:no_content) unless controller.performed?
|
|
30
|
+
rescue => e
|
|
31
|
+
raise e unless controller.rescue_with_handler(e)
|
|
32
|
+
# A handler that rendered nothing answers 204, as an action does.
|
|
33
|
+
controller.head(:no_content) unless controller.performed?
|
|
34
|
+
end
|
|
35
|
+
controller.to_response
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module Edgeway
|
|
2
|
+
module Controller
|
|
3
|
+
module Rendering
|
|
4
|
+
STATUS_CODES = {
|
|
5
|
+
"ok" => 200, "created" => 201, "accepted" => 202, "no_content" => 204,
|
|
6
|
+
"moved_permanently" => 301, "found" => 302, "not_modified" => 304,
|
|
7
|
+
"bad_request" => 400, "unauthorized" => 401, "forbidden" => 403, "not_found" => 404,
|
|
8
|
+
"method_not_allowed" => 405, "conflict" => 409, "unprocessable_entity" => 422,
|
|
9
|
+
"too_many_requests" => 429, "internal_server_error" => 500, "not_implemented" => 501,
|
|
10
|
+
"service_unavailable" => 503
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
def self.json_response(status, json_text)
|
|
14
|
+
HTTP::Response.json(status, json_text)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.status_code(status)
|
|
18
|
+
return status if status.is_a?(Integer)
|
|
19
|
+
code = STATUS_CODES[status.to_s]
|
|
20
|
+
raise ArgumentError, "unknown status: #{status.inspect}" if code.nil?
|
|
21
|
+
code
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.json_value(obj)
|
|
25
|
+
if obj.is_a?(Model::Relation)
|
|
26
|
+
obj.to_a.map { |m| m.as_json }
|
|
27
|
+
elsif obj.is_a?(Model::Base)
|
|
28
|
+
obj.as_json
|
|
29
|
+
elsif obj.is_a?(HTTP::Parameters)
|
|
30
|
+
obj.to_h
|
|
31
|
+
elsif obj.is_a?(Array)
|
|
32
|
+
obj.map { |x| json_value(x) }
|
|
33
|
+
elsif obj.is_a?(Hash)
|
|
34
|
+
h = {}
|
|
35
|
+
obj.each { |k, v| h[k] = json_value(v) }
|
|
36
|
+
h
|
|
37
|
+
else
|
|
38
|
+
obj
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def render(json:, status: 200)
|
|
43
|
+
@status = Rendering.status_code(status)
|
|
44
|
+
@headers["Content-Type"] = "application/json"
|
|
45
|
+
@response_body = JSON.generate(Rendering.json_value(json)) + "\n"
|
|
46
|
+
@performed = true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def head(status)
|
|
50
|
+
@status = Rendering.status_code(status)
|
|
51
|
+
@response_body = ""
|
|
52
|
+
@performed = true
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def performed? = @performed
|
|
56
|
+
|
|
57
|
+
def to_response
|
|
58
|
+
response = HTTP::Response.new(@status, @response_body)
|
|
59
|
+
@headers.each { |k, v| response.headers[k] = v }
|
|
60
|
+
response.check_headers
|
|
61
|
+
response
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Edgeway
|
|
2
|
+
module Controller
|
|
3
|
+
class Handler
|
|
4
|
+
attr_reader :error_class, :method_name
|
|
5
|
+
|
|
6
|
+
def initialize(error_class, method_name)
|
|
7
|
+
@error_class = error_class
|
|
8
|
+
@method_name = method_name
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module Rescuable
|
|
13
|
+
# true when a handler took the error. The handler is called with the
|
|
14
|
+
# error, as in Rails (def not_found(e)).
|
|
15
|
+
def rescue_with_handler(error)
|
|
16
|
+
self.class.all_handlers.reverse.each do |handler|
|
|
17
|
+
if error.is_a?(handler.error_class)
|
|
18
|
+
send(handler.method_name, error)
|
|
19
|
+
return true
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Edgeway
|
|
2
|
+
module DB
|
|
3
|
+
class Adapter
|
|
4
|
+
def select_all(sql, binds)
|
|
5
|
+
raise Error, "#{self.class} does not define select_all"
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def execute(sql, binds)
|
|
9
|
+
raise Error, "#{self.class} does not define execute"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def batch(statements)
|
|
13
|
+
raise Error, "#{self.class} does not define batch"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def insert_returning(sql, binds)
|
|
17
|
+
rows = select_all(sql, binds)
|
|
18
|
+
raise DatabaseError, "INSERT ... RETURNING answered no row" if rows.empty?
|
|
19
|
+
rows[0]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def fail_with(message)
|
|
23
|
+
raise RecordNotUnique, message if message.include?("UNIQUE constraint failed")
|
|
24
|
+
raise DatabaseError, message
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def supports_returning? = true
|
|
28
|
+
def placeholder(n) = "?"
|
|
29
|
+
def quote_boolean(value) = value ? 1 : 0
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
require_relative "adapter"
|
|
2
|
+
|
|
3
|
+
module Edgeway
|
|
4
|
+
module DB
|
|
5
|
+
# An adapter for tests: it records the SQL it is given and answers the
|
|
6
|
+
# rows the test queued (none: an empty answer). Nothing is stored.
|
|
7
|
+
#
|
|
8
|
+
# db = Edgeway::DB::MemoryAdapter.new
|
|
9
|
+
# Edgeway::Model.connection = db
|
|
10
|
+
# db.queue([{ "id" => 1, "title" => "a" }])
|
|
11
|
+
# Todo.find(1)
|
|
12
|
+
# puts db.log # SELECT ... | [1]
|
|
13
|
+
class MemoryAdapter < Adapter
|
|
14
|
+
attr_reader :log
|
|
15
|
+
attr_writer :returning
|
|
16
|
+
# The "changes" count execute answers (1 by default), for delete_all.
|
|
17
|
+
attr_writer :changes
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
@log = []
|
|
21
|
+
@queue = []
|
|
22
|
+
@returning = true
|
|
23
|
+
@changes = 1
|
|
24
|
+
@failure = nil
|
|
25
|
+
@failure_on = ""
|
|
26
|
+
@next_id = 1
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The rows the next query answers (one queue entry per query).
|
|
30
|
+
def queue(rows)
|
|
31
|
+
@queue << rows
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Makes the next query or execute whose SQL contains `containing` (any,
|
|
35
|
+
# by default) fail with this database message, after logging it, as D1
|
|
36
|
+
# would: "UNIQUE constraint failed: t.c" is a RecordNotUnique.
|
|
37
|
+
def fail_next(message, containing = "")
|
|
38
|
+
@failure = message
|
|
39
|
+
@failure_on = containing
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def supports_returning? = @returning
|
|
43
|
+
|
|
44
|
+
def select_all(sql, binds)
|
|
45
|
+
record(sql, binds)
|
|
46
|
+
@queue.empty? ? [] : @queue.shift
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def execute(sql, binds)
|
|
50
|
+
record(sql, binds)
|
|
51
|
+
id = @next_id
|
|
52
|
+
@next_id += 1
|
|
53
|
+
{ "changes" => @changes, "last_insert_id" => id }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def batch(statements)
|
|
57
|
+
@log << "BATCH #{statements.size}"
|
|
58
|
+
statements.map { |s| execute(s[0], s[1]) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def record(sql, binds)
|
|
64
|
+
@log << "#{sql} | #{JSON.generate(binds)}"
|
|
65
|
+
if !@failure.nil? && sql.include?(@failure_on)
|
|
66
|
+
message = @failure
|
|
67
|
+
@failure = nil
|
|
68
|
+
fail_with(message)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|