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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +444 -0
  4. data/exe/edgeway +6 -0
  5. data/js/cgi.js +157 -0
  6. data/js/cloudflare.js +103 -0
  7. data/js/cors.js +87 -0
  8. data/js/jwt.js +279 -0
  9. data/js/runtime.js +99 -0
  10. data/js/wasi-shim.js +191 -0
  11. data/lib/edgeway/application.rb +70 -0
  12. data/lib/edgeway/cli/app_config.rb +35 -0
  13. data/lib/edgeway/cli/app_files.rb +37 -0
  14. data/lib/edgeway/cli/build.rb +90 -0
  15. data/lib/edgeway/cli/generate.rb +160 -0
  16. data/lib/edgeway/cli/new.rb +43 -0
  17. data/lib/edgeway/cli/package_manager.rb +21 -0
  18. data/lib/edgeway/cli/precheck.rb +45 -0
  19. data/lib/edgeway/cli/routes.rb +22 -0
  20. data/lib/edgeway/cli/test.rb +33 -0
  21. data/lib/edgeway/cli/toolchain.rb +35 -0
  22. data/lib/edgeway/cli.rb +59 -0
  23. data/lib/edgeway/config.rb +9 -0
  24. data/lib/edgeway/controller/authentication.rb +42 -0
  25. data/lib/edgeway/controller/base.rb +95 -0
  26. data/lib/edgeway/controller/callbacks.rb +29 -0
  27. data/lib/edgeway/controller/dispatcher.rb +39 -0
  28. data/lib/edgeway/controller/rendering.rb +65 -0
  29. data/lib/edgeway/controller/rescuable.rb +26 -0
  30. data/lib/edgeway/db/adapter.rb +32 -0
  31. data/lib/edgeway/db/memory.rb +73 -0
  32. data/lib/edgeway/db/sqlite.rb +190 -0
  33. data/lib/edgeway/errors.rb +61 -0
  34. data/lib/edgeway/generators/app/templates/.gitignore +6 -0
  35. data/lib/edgeway/generators/app/templates/Gemfile.tt +4 -0
  36. data/lib/edgeway/generators/app/templates/README.md.tt +22 -0
  37. data/lib/edgeway/generators/app/templates/app/controllers/application_controller.rb +9 -0
  38. data/lib/edgeway/generators/app/templates/app/controllers/health_controller.rb +3 -0
  39. data/lib/edgeway/generators/app/templates/app/models/application_model.rb +3 -0
  40. data/lib/edgeway/generators/app/templates/bin/build.tt +7 -0
  41. data/lib/edgeway/generators/app/templates/bin/deploy.tt +10 -0
  42. data/lib/edgeway/generators/app/templates/bin/setup.tt +38 -0
  43. data/lib/edgeway/generators/app/templates/config/database.yml +14 -0
  44. data/lib/edgeway/generators/app/templates/config/routes.rb +4 -0
  45. data/lib/edgeway/generators/app/templates/db/migrations/.keep +0 -0
  46. data/lib/edgeway/generators/app/templates/mise.toml.tt +24 -0
  47. data/lib/edgeway/generators/app/templates/package.json.tt +7 -0
  48. data/lib/edgeway/generators/app/templates/src/worker.js +4 -0
  49. data/lib/edgeway/generators/app/templates/test/controllers/health_test.rb +16 -0
  50. data/lib/edgeway/generators/app/templates/test/test_helper.rb +1 -0
  51. data/lib/edgeway/generators/app/templates/wrangler.toml.tt +23 -0
  52. data/lib/edgeway/http/auth/jwt.rb +127 -0
  53. data/lib/edgeway/http/auth/stub.rb +29 -0
  54. data/lib/edgeway/http/json_body.rb +37 -0
  55. data/lib/edgeway/http/parameters.rb +42 -0
  56. data/lib/edgeway/http/request.rb +115 -0
  57. data/lib/edgeway/http/response.rb +46 -0
  58. data/lib/edgeway/inflector.rb +16 -0
  59. data/lib/edgeway/logger.rb +21 -0
  60. data/lib/edgeway/model/attributes.rb +85 -0
  61. data/lib/edgeway/model/base.rb +173 -0
  62. data/lib/edgeway/model/macros.rb +60 -0
  63. data/lib/edgeway/model/persistence.rb +154 -0
  64. data/lib/edgeway/model/relation.rb +295 -0
  65. data/lib/edgeway/model/validations.rb +136 -0
  66. data/lib/edgeway/routing/mapper.rb +57 -0
  67. data/lib/edgeway/routing/route.rb +39 -0
  68. data/lib/edgeway/routing/route_set.rb +27 -0
  69. data/lib/edgeway/server.rb +12 -0
  70. data/lib/edgeway/service/cloudflare/d1.rb +101 -0
  71. data/lib/edgeway/service/cloudflare/jwt.rb +44 -0
  72. data/lib/edgeway/test.rb +97 -0
  73. data/lib/edgeway/version.rb +5 -0
  74. data/lib/edgeway.rb +30 -0
  75. metadata +115 -0
@@ -0,0 +1,27 @@
1
+ module Edgeway
2
+ module Routing
3
+ class RouteSet
4
+ attr_reader :list
5
+
6
+ def initialize
7
+ @list = []
8
+ end
9
+
10
+ def draw
11
+ yield Mapper.new(self, "", "")
12
+ end
13
+
14
+ def add(route)
15
+ @list << route
16
+ end
17
+
18
+ def recognize(verb, path)
19
+ @list.each do |route|
20
+ found = route.match(verb, path)
21
+ return [route, found] if found
22
+ end
23
+ raise RoutingError, "no route for #{verb} #{path}"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,12 @@
1
+ module Edgeway
2
+ # Runs the app for one request, CGI style: the request comes in as CGI
3
+ # environment variables and STDIN (HTTP::Request.from_env), and the response
4
+ # goes out on stdout as CGI output (Status and the headers, a blank line,
5
+ # the body), which js/runtime.js turns into the Response.
6
+ module Server
7
+ def self.run
8
+ response = Edgeway.application.call(HTTP::Request.from_env)
9
+ print response.to_cgi
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,101 @@
1
+ # Cloudflare D1, through the wasm import module "edgeway_d1" that
2
+ # js/cloudflare.js implements. Only wasm builds load this file: ffi_source is
3
+ # Spinel's, so CRuby can't read it (tests use edgeway/db/memory).
4
+ require "json"
5
+ require_relative "../../db/adapter"
6
+
7
+ module Edgeway
8
+ module Cloudflare
9
+ # The imports as Ruby methods: d1_query and d1_execute take the binding
10
+ # name, the SQL and the binds (JSON text); d1_batch takes the binding name
11
+ # and the statements ([[sql, binds], ...], JSON text), and answer the result as JSON
12
+ # text:
13
+ #
14
+ # d1_query {"rows": [...]}
15
+ # d1_execute {"changes": n, "last_insert_id": id}
16
+ # d1_batch {"results": [{"changes": n, "last_insert_id": id}, ...]}
17
+ # any {"error": "message"} when D1 failed
18
+ #
19
+ # An import can't hand a string to wasm, so a call is two: query/execute
20
+ # runs the statement and answers the byte length of its result, and
21
+ # take(buf) copies the result into buf. JS wraps query and execute in
22
+ # WebAssembly.Suspending: the D1 Promise is awaited with the wasm stack
23
+ # suspended (JSPI), so they read like blocking calls here.
24
+ module D1Imports
25
+ ffi_source <<~C
26
+ #include <stdlib.h>
27
+
28
+ #define D1_IMPORT(name) __attribute__((import_module("edgeway_d1"), import_name(name)))
29
+
30
+ D1_IMPORT("query") int js_d1_query(const char *binding, const char *sql, const char *binds);
31
+ D1_IMPORT("execute") int js_d1_execute(const char *binding, const char *sql, const char *binds);
32
+ D1_IMPORT("batch") int js_d1_batch(const char *binding, const char *statements);
33
+ D1_IMPORT("take") void js_d1_take(char *buf);
34
+
35
+ /* The last result, NUL-terminated; grown as needed and reused. */
36
+ static char *result;
37
+ static int result_cap;
38
+
39
+ static const char *take_result(int length) {
40
+ if (length + 1 > result_cap) {
41
+ char *grown = realloc(result, length + 1);
42
+ if (!grown) abort();
43
+ result = grown;
44
+ result_cap = length + 1;
45
+ }
46
+ js_d1_take(result);
47
+ result[length] = 0;
48
+ return result;
49
+ }
50
+
51
+ /* What ffi_func binds. Its own extern declaration would lose the
52
+ import attributes, so the imports are wrapped. */
53
+ const char *d1_query(const char *binding, const char *sql, const char *binds) {
54
+ return take_result(js_d1_query(binding, sql, binds));
55
+ }
56
+
57
+ const char *d1_execute(const char *binding, const char *sql, const char *binds) {
58
+ return take_result(js_d1_execute(binding, sql, binds));
59
+ }
60
+
61
+ const char *d1_batch(const char *binding, const char *statements) {
62
+ return take_result(js_d1_batch(binding, statements));
63
+ }
64
+ C
65
+ ffi_func :d1_query, [:str, :str, :str], :str
66
+ ffi_func :d1_execute, [:str, :str, :str], :str
67
+ ffi_func :d1_batch, [:str, :str], :str
68
+ end
69
+
70
+ class D1Adapter < DB::Adapter
71
+ # binding: the name of the D1 binding in wrangler.toml.
72
+ def initialize(binding: "DB")
73
+ @binding_name = binding
74
+ end
75
+
76
+ def select_all(sql, binds)
77
+ result(D1Imports.d1_query(@binding_name, sql, JSON.generate(binds)))["rows"]
78
+ end
79
+
80
+ def execute(sql, binds)
81
+ res = result(D1Imports.d1_execute(@binding_name, sql, JSON.generate(binds)))
82
+ { "changes" => res["changes"].to_i, "last_insert_id" => res["last_insert_id"].to_i }
83
+ end
84
+
85
+ # One D1 batch(): the statements run as a single transaction, so any
86
+ # failure rolls all of them back.
87
+ def batch(statements)
88
+ res = result(D1Imports.d1_batch(@binding_name, JSON.generate(statements)))
89
+ res["results"].map { |r| { "changes" => r["changes"].to_i, "last_insert_id" => r["last_insert_id"].to_i } }
90
+ end
91
+
92
+ private
93
+
94
+ def result(json)
95
+ res = JSON.parse(json)
96
+ fail_with("d1: #{res["error"]}") if res.key?("error")
97
+ res
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,44 @@
1
+ # JWT signature checks on Cloudflare Workers, through the wasm import module
2
+ # "edgeway_jwt" that js/jwt.js implements. Only wasm builds load this file
3
+ # (ffi_source is Spinel's; tests use edgeway/http/auth/stub).
4
+ require "json"
5
+ require_relative "../../http/auth/jwt"
6
+
7
+ module Edgeway
8
+ module Cloudflare
9
+ module JWTImports
10
+ ffi_source <<~C
11
+ #include <stdlib.h>
12
+
13
+ #define JWT_IMPORT(name) __attribute__((import_module("edgeway_jwt"), import_name(name)))
14
+
15
+ JWT_IMPORT("verify") int js_jwt_verify(const char *token);
16
+ JWT_IMPORT("take") void js_jwt_take(char *buf);
17
+
18
+ /* The last result, NUL-terminated; grown as needed and reused. */
19
+ static char *jwt_result;
20
+ static int jwt_result_cap;
21
+
22
+ const char *jwt_verify(const char *token) {
23
+ int length = js_jwt_verify(token);
24
+ if (length + 1 > jwt_result_cap) {
25
+ char *grown = realloc(jwt_result, length + 1);
26
+ if (!grown) abort();
27
+ jwt_result = grown;
28
+ jwt_result_cap = length + 1;
29
+ }
30
+ js_jwt_take(jwt_result);
31
+ jwt_result[length] = 0;
32
+ return jwt_result;
33
+ }
34
+ C
35
+ ffi_func :jwt_verify, [:str], :str
36
+ end
37
+
38
+ class JWTVerifier < Auth::Verifier
39
+ def verify(token)
40
+ JWTImports.jwt_verify(token)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,97 @@
1
+ # Test helper (Edgeway::UnitTest for models, Edgeway::IntegrationTest for requests): `require "edgeway/test"` in test/test_helper.rb.
2
+ # Needs spinel_test-unit (a development dependency of the app, not of edgeway)
3
+ # and tmp/test_app.rb, which `edgeway test` rewrites on every run (and which
4
+ # is git-ignored): Spinel resolves require
5
+ # at compile time, so the app's files are listed there (in Build's order)
6
+ # rather than found at run time. Unlike build/main.rb it never calls
7
+ # Edgeway::Server.run. Run the tests with `edgeway test`, not spinel_test-unit
8
+ # directly: a missing or stale tmp/test_app.rb only shows up as Spinel's own
9
+ # "cannot find tmp/test_app" (the message cannot be customised at compile time).
10
+ require "spinel_test/unit"
11
+ require "edgeway"
12
+ require "edgeway/db/memory"
13
+ require "edgeway/db/sqlite"
14
+ require "tmp/test_app"
15
+
16
+ module Edgeway
17
+ # What a test sees of the answer.
18
+ class TestResponse
19
+ attr_reader :status, :body, :headers
20
+
21
+ def initialize(status, body, headers)
22
+ @status = status
23
+ @body = body
24
+ @headers = headers
25
+ end
26
+ end
27
+
28
+ # What a model test needs: a fresh in-memory SQLite database per test
29
+ # (SQLiteAdapter), with the app's db/migrations applied in file name order,
30
+ # so a test can check what was stored (Todo.find, db.select_all) and read
31
+ # db.log for the SQL. Edgeway's own tests use MemoryAdapter instead, which
32
+ # stores nothing.
33
+ class UnitTest < SpinelTest::Unit::TestCase
34
+ attr_reader :db
35
+
36
+ def setup
37
+ @db = Edgeway::DB::SQLiteAdapter.new(":memory:")
38
+ @db.apply_migrations(File.join(Edgeway::TEST_APP_ROOT, "db/migrations"))
39
+ Edgeway::Model.connection = @db
40
+ end
41
+
42
+ def teardown
43
+ @db.close
44
+ end
45
+ end
46
+
47
+ # A UnitTest that also sends requests to the app (get, post, ...) and reads
48
+ # the answer (response, json, assert_response).
49
+ class IntegrationTest < UnitTest
50
+ attr_reader :response
51
+
52
+ def setup
53
+ super
54
+ @response = nil
55
+ @json = nil
56
+ end
57
+
58
+ def get(path, json: nil, headers: {}) = request("GET", path, json, headers)
59
+ def delete(path, json: nil, headers: {}) = request("DELETE", path, json, headers)
60
+ def post(path, json: nil, headers: {}) = request("POST", path, json, headers)
61
+ def put(path, json: nil, headers: {}) = request("PUT", path, json, headers)
62
+ def patch(path, json: nil, headers: {}) = request("PATCH", path, json, headers)
63
+
64
+ # The body parsed as JSON.
65
+ def json
66
+ @json = JSON.parse(@response.body) if @json.nil?
67
+ @json
68
+ end
69
+
70
+ def assert_response(status)
71
+ assert_equal status, @response.status
72
+ end
73
+
74
+ private
75
+
76
+ def request(verb, target, json, headers)
77
+ path = target
78
+ query = ""
79
+ q = target.index("?")
80
+ unless q.nil?
81
+ path = target[0, q]
82
+ query = target[q + 1, target.length]
83
+ end
84
+ body = json.nil? ? "" : JSON.generate(json)
85
+ content_type = json.nil? ? "" : "application/json"
86
+ headers.each { |k, v| ENV["HTTP_" + k.upcase.gsub("-", "_")] = v }
87
+ begin
88
+ res = Edgeway.application.call(Edgeway::HTTP::Request.new(verb, path, query, content_type, body))
89
+ ensure
90
+ headers.each { |k, _v| ENV.delete("HTTP_" + k.upcase.gsub("-", "_")) }
91
+ end
92
+ @json = nil
93
+ @response = Edgeway::TestResponse.new(res.status, res.body, res.headers)
94
+ @response
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Edgeway
4
+ VERSION = "0.1.0"
5
+ end
data/lib/edgeway.rb ADDED
@@ -0,0 +1,30 @@
1
+ require "json"
2
+ require "time"
3
+ require_relative "edgeway/version"
4
+ require_relative "edgeway/errors"
5
+ require_relative "edgeway/inflector"
6
+ require_relative "edgeway/config"
7
+ require_relative "edgeway/logger"
8
+ require_relative "edgeway/http/response"
9
+ require_relative "edgeway/http/parameters"
10
+ require_relative "edgeway/http/json_body"
11
+ require_relative "edgeway/http/request"
12
+ require_relative "edgeway/routing/route"
13
+ require_relative "edgeway/routing/route_set"
14
+ require_relative "edgeway/routing/mapper"
15
+ require_relative "edgeway/db/adapter"
16
+ require_relative "edgeway/model/attributes"
17
+ require_relative "edgeway/model/validations"
18
+ require_relative "edgeway/model/macros"
19
+ require_relative "edgeway/model/relation"
20
+ require_relative "edgeway/model/persistence"
21
+ require_relative "edgeway/model/base"
22
+ require_relative "edgeway/http/auth/jwt"
23
+ require_relative "edgeway/controller/callbacks"
24
+ require_relative "edgeway/controller/rescuable"
25
+ require_relative "edgeway/controller/rendering"
26
+ require_relative "edgeway/controller/authentication"
27
+ require_relative "edgeway/controller/base"
28
+ require_relative "edgeway/controller/dispatcher"
29
+ require_relative "edgeway/application"
30
+ require_relative "edgeway/server"
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: edgeway
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yuji Yaginuma
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ email:
13
+ - yuuji.yaginuma@gmail.com
14
+ executables:
15
+ - edgeway
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - LICENSE.txt
20
+ - README.md
21
+ - exe/edgeway
22
+ - js/cgi.js
23
+ - js/cloudflare.js
24
+ - js/cors.js
25
+ - js/jwt.js
26
+ - js/runtime.js
27
+ - js/wasi-shim.js
28
+ - lib/edgeway.rb
29
+ - lib/edgeway/application.rb
30
+ - lib/edgeway/cli.rb
31
+ - lib/edgeway/cli/app_config.rb
32
+ - lib/edgeway/cli/app_files.rb
33
+ - lib/edgeway/cli/build.rb
34
+ - lib/edgeway/cli/generate.rb
35
+ - lib/edgeway/cli/new.rb
36
+ - lib/edgeway/cli/package_manager.rb
37
+ - lib/edgeway/cli/precheck.rb
38
+ - lib/edgeway/cli/routes.rb
39
+ - lib/edgeway/cli/test.rb
40
+ - lib/edgeway/cli/toolchain.rb
41
+ - lib/edgeway/config.rb
42
+ - lib/edgeway/controller/authentication.rb
43
+ - lib/edgeway/controller/base.rb
44
+ - lib/edgeway/controller/callbacks.rb
45
+ - lib/edgeway/controller/dispatcher.rb
46
+ - lib/edgeway/controller/rendering.rb
47
+ - lib/edgeway/controller/rescuable.rb
48
+ - lib/edgeway/db/adapter.rb
49
+ - lib/edgeway/db/memory.rb
50
+ - lib/edgeway/db/sqlite.rb
51
+ - lib/edgeway/errors.rb
52
+ - lib/edgeway/generators/app/templates/.gitignore
53
+ - lib/edgeway/generators/app/templates/Gemfile.tt
54
+ - lib/edgeway/generators/app/templates/README.md.tt
55
+ - lib/edgeway/generators/app/templates/app/controllers/application_controller.rb
56
+ - lib/edgeway/generators/app/templates/app/controllers/health_controller.rb
57
+ - lib/edgeway/generators/app/templates/app/models/application_model.rb
58
+ - lib/edgeway/generators/app/templates/bin/build.tt
59
+ - lib/edgeway/generators/app/templates/bin/deploy.tt
60
+ - lib/edgeway/generators/app/templates/bin/setup.tt
61
+ - lib/edgeway/generators/app/templates/config/database.yml
62
+ - lib/edgeway/generators/app/templates/config/routes.rb
63
+ - lib/edgeway/generators/app/templates/db/migrations/.keep
64
+ - lib/edgeway/generators/app/templates/mise.toml.tt
65
+ - lib/edgeway/generators/app/templates/package.json.tt
66
+ - lib/edgeway/generators/app/templates/src/worker.js
67
+ - lib/edgeway/generators/app/templates/test/controllers/health_test.rb
68
+ - lib/edgeway/generators/app/templates/test/test_helper.rb
69
+ - lib/edgeway/generators/app/templates/wrangler.toml.tt
70
+ - lib/edgeway/http/auth/jwt.rb
71
+ - lib/edgeway/http/auth/stub.rb
72
+ - lib/edgeway/http/json_body.rb
73
+ - lib/edgeway/http/parameters.rb
74
+ - lib/edgeway/http/request.rb
75
+ - lib/edgeway/http/response.rb
76
+ - lib/edgeway/inflector.rb
77
+ - lib/edgeway/logger.rb
78
+ - lib/edgeway/model/attributes.rb
79
+ - lib/edgeway/model/base.rb
80
+ - lib/edgeway/model/macros.rb
81
+ - lib/edgeway/model/persistence.rb
82
+ - lib/edgeway/model/relation.rb
83
+ - lib/edgeway/model/validations.rb
84
+ - lib/edgeway/routing/mapper.rb
85
+ - lib/edgeway/routing/route.rb
86
+ - lib/edgeway/routing/route_set.rb
87
+ - lib/edgeway/server.rb
88
+ - lib/edgeway/service/cloudflare/d1.rb
89
+ - lib/edgeway/service/cloudflare/jwt.rb
90
+ - lib/edgeway/test.rb
91
+ - lib/edgeway/version.rb
92
+ homepage: https://github.com/y-yagi/edgeway
93
+ licenses:
94
+ - MIT
95
+ metadata:
96
+ source_code_uri: https://github.com/y-yagi/edgeway
97
+ rubygems_mfa_required: 'true'
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: 3.2.0
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubygems_version: 4.0.17
113
+ specification_version: 4
114
+ summary: A library for Ruby compiled by Spinel to wasm on edge workers.
115
+ test_files: []