edgeway 0.2.0 → 0.3.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/README.md +602 -204
  4. data/js/cloudflare-kv.js +47 -0
  5. data/js/cloudflare-ratelimit.js +31 -0
  6. data/js/cloudflare-test-database.js +112 -0
  7. data/js/cloudflare-test-runner.js +76 -0
  8. data/js/cloudflare.js +25 -4
  9. data/js/fetch.js +113 -0
  10. data/js/jspi.js +32 -0
  11. data/js/jwt.js +5 -39
  12. data/js/runtime.js +42 -5
  13. data/lib/edgeway/application.rb +16 -7
  14. data/lib/edgeway/cache/limits.rb +8 -0
  15. data/lib/edgeway/cache/memory_store.rb +24 -0
  16. data/lib/edgeway/cache/null_store.rb +13 -0
  17. data/lib/edgeway/cache/store.rb +189 -0
  18. data/lib/edgeway/cache.rb +13 -0
  19. data/lib/edgeway/cli/build/cache_config.rb +108 -0
  20. data/lib/edgeway/cli/build/database_config.rb +4 -0
  21. data/lib/edgeway/cli/build/js_files.rb +30 -0
  22. data/lib/edgeway/cli/build.rb +17 -10
  23. data/lib/edgeway/cli/cloudflare_test/wasm_test_cli.rb +35 -0
  24. data/lib/edgeway/cli/cloudflare_test.rb +120 -0
  25. data/lib/edgeway/cli/test.rb +25 -9
  26. data/lib/edgeway/cli/toolchain.rb +5 -0
  27. data/lib/edgeway/cli.rb +2 -0
  28. data/lib/edgeway/controller/base.rb +22 -3
  29. data/lib/edgeway/controller/caching.rb +140 -0
  30. data/lib/edgeway/controller/callbacks.rb +5 -1
  31. data/lib/edgeway/controller/rate_limiting.rb +145 -0
  32. data/lib/edgeway/db/query_stats.rb +2 -6
  33. data/lib/edgeway/errors.rb +21 -0
  34. data/lib/edgeway/generators/app/templates/.github/workflows/ci.yml.tt +3 -3
  35. data/lib/edgeway/generators/app/templates/README.md.tt +3 -1
  36. data/lib/edgeway/generators/app/templates/bin/setup.tt +2 -3
  37. data/lib/edgeway/generators/app/templates/config/cache.yml +22 -0
  38. data/lib/edgeway/generators/app/templates/config/database.yml +8 -2
  39. data/lib/edgeway/generators/app/templates/public/.assetsignore +1 -0
  40. data/lib/edgeway/generators/app/templates/wrangler.toml.tt +34 -2
  41. data/lib/edgeway/http/client.rb +80 -0
  42. data/lib/edgeway/http/request.rb +5 -0
  43. data/lib/edgeway/http/stub_transport.rb +38 -0
  44. data/lib/edgeway/logger.rb +33 -5
  45. data/lib/edgeway/rate_limit/limiter.rb +9 -0
  46. data/lib/edgeway/rate_limit/null_limiter.rb +9 -0
  47. data/lib/edgeway/rate_limit/stub_limiter.rb +24 -0
  48. data/lib/edgeway/rate_limit.rb +2 -0
  49. data/lib/edgeway/server.rb +3 -2
  50. data/lib/edgeway/service/cloudflare/d1.rb +6 -21
  51. data/lib/edgeway/service/cloudflare/jwt.rb +3 -15
  52. data/lib/edgeway/service/cloudflare/kv.rb +45 -0
  53. data/lib/edgeway/service/cloudflare/kv_store.rb +58 -0
  54. data/lib/edgeway/service/cloudflare/rate_limit.rb +27 -0
  55. data/lib/edgeway/service/cloudflare/rate_limiter.rb +33 -0
  56. data/lib/edgeway/service/cloudflare/test_databases.rb +65 -0
  57. data/lib/edgeway/service/fetch.rb +26 -0
  58. data/lib/edgeway/service/import_result.rb +24 -0
  59. data/lib/edgeway/test/sqlite_databases.rb +18 -0
  60. data/lib/edgeway/test.rb +72 -33
  61. data/lib/edgeway/version.rb +1 -1
  62. data/lib/edgeway.rb +6 -0
  63. metadata +34 -1
@@ -0,0 +1,80 @@
1
+ module Edgeway
2
+ module HTTP
3
+ DEFAULT_TIMEOUT = 10
4
+ MAX_TIMEOUT = 300
5
+
6
+ class ClientResponse
7
+ attr_reader :status, :headers, :body
8
+
9
+ def initialize(status, headers, body)
10
+ @status = status
11
+ @headers = headers
12
+ @body = body
13
+ end
14
+
15
+ def success? = @status >= 200 && @status < 300
16
+
17
+ def json = JSON.parse(@body)
18
+ end
19
+
20
+ class Transport
21
+ def request(payload)
22
+ JSON.generate({ "error" => "no HTTP transport is set" })
23
+ end
24
+ end
25
+
26
+ @client = Transport.new
27
+
28
+ def self.client = @client
29
+
30
+ def self.client=(client)
31
+ @client = client
32
+ end
33
+
34
+ def self.get(url, headers: {}, timeout: DEFAULT_TIMEOUT)
35
+ send_request("GET", url, "", headers, timeout)
36
+ end
37
+
38
+ def self.delete(url, headers: {}, timeout: DEFAULT_TIMEOUT)
39
+ send_request("DELETE", url, "", headers, timeout)
40
+ end
41
+
42
+ def self.post(url, body: "", headers: {}, timeout: DEFAULT_TIMEOUT)
43
+ send_request("POST", url, body, headers, timeout)
44
+ end
45
+
46
+ def self.put(url, body: "", headers: {}, timeout: DEFAULT_TIMEOUT)
47
+ send_request("PUT", url, body, headers, timeout)
48
+ end
49
+
50
+ def self.patch(url, body: "", headers: {}, timeout: DEFAULT_TIMEOUT)
51
+ send_request("PATCH", url, body, headers, timeout)
52
+ end
53
+
54
+ def self.send_request(verb, url, body, headers, timeout)
55
+ timeout_ms = timeout_milliseconds(timeout)
56
+ payload = JSON.generate({
57
+ "method" => verb, "url" => url, "headers" => headers, "body" => body,
58
+ "timeout_ms" => timeout_ms
59
+ })
60
+ parse_answer(client.request(payload))
61
+ end
62
+
63
+ def self.timeout_milliseconds(timeout)
64
+ milliseconds = timeout > MAX_TIMEOUT ? 0 : (timeout * 1000).to_i
65
+ raise ArgumentError, "timeout must be 0.001 to #{MAX_TIMEOUT} seconds" if milliseconds < 1
66
+
67
+ milliseconds
68
+ end
69
+
70
+ def self.parse_answer(text)
71
+ answer = JSON.parse(text)
72
+ raise Timeout, answer["error"] if answer.key?("timeout")
73
+ raise Error, answer["error"] if answer.key?("error")
74
+
75
+ body = answer["body_base64"].unpack1("m0")
76
+ body.force_encoding("UTF-8")
77
+ ClientResponse.new(answer["status"], answer["headers"], body)
78
+ end
79
+ end
80
+ end
@@ -25,6 +25,11 @@ module Edgeway
25
25
  ENV["HTTP_" + name.upcase.gsub("-", "_")] || ""
26
26
  end
27
27
 
28
+ # The client address the platform reports, "" when unknown.
29
+ def remote_addr
30
+ ENV["REMOTE_ADDR"] || ""
31
+ end
32
+
28
33
  def json?
29
34
  @content_type.start_with?("application/json")
30
35
  end
@@ -0,0 +1,38 @@
1
+ require_relative "client"
2
+
3
+ module Edgeway
4
+ module HTTP
5
+ class UnstubbedRequest < Edgeway::Error; end
6
+
7
+ class StubTransport < Transport
8
+ attr_reader :requests
9
+
10
+ def initialize
11
+ @answers = {}
12
+ @requests = []
13
+ end
14
+
15
+ def stub(verb, url, status, body, headers)
16
+ lowered = {}
17
+ headers.each { |name, value| lowered[name.to_s.downcase] = value.to_s }
18
+ @answers[key(verb, url)] = JSON.generate({ "status" => status, "headers" => lowered, "body_base64" => [body].pack("m0") })
19
+ end
20
+
21
+ def stub_error(verb, url, message, timeout)
22
+ answer = { "error" => message }
23
+ answer["timeout"] = true if timeout
24
+ @answers[key(verb, url)] = JSON.generate(answer)
25
+ end
26
+
27
+ def request(payload)
28
+ sent = JSON.parse(payload)
29
+ @requests << sent
30
+ @answers[key(sent["method"], sent["url"])] || raise(UnstubbedRequest, "no stub for #{sent["method"]} #{sent["url"]} (add stub_http to the test)")
31
+ end
32
+
33
+ private
34
+
35
+ def key(verb, url) = "#{verb.to_s.upcase} #{url}"
36
+ end
37
+ end
38
+ end
@@ -1,12 +1,40 @@
1
1
  module Edgeway
2
- # Writes to stderr; the runtime sends it to console.error.
3
2
  class Logger
4
- def info(message)
5
- STDERR.puts "[edgeway] #{message}"
3
+ LEVELS = ["debug", "info", "warn", "error"]
4
+
5
+ def initialize(output = STDERR)
6
+ @output = output
7
+ end
8
+
9
+ def debug(message, fields = {})
10
+ write("debug", message, fields)
11
+ end
12
+
13
+ def info(message, fields = {})
14
+ write("info", message, fields)
15
+ end
16
+
17
+ def warn(message, fields = {})
18
+ write("warn", message, fields)
19
+ end
20
+
21
+ def error(message, fields = {})
22
+ write("error", message, fields)
23
+ end
24
+
25
+ private
26
+
27
+ def write(level, message, fields)
28
+ return if LEVELS.index(level) < threshold
29
+ entry = {}
30
+ fields.each { |name, value| entry[name.to_s] = value }
31
+ entry["level"] = level
32
+ entry["message"] = message
33
+ @output.puts JSON.generate(entry)
6
34
  end
7
35
 
8
- def error(message)
9
- STDERR.puts "[edgeway] ERROR #{message}"
36
+ def threshold
37
+ LEVELS.index(Edgeway.config("LOG_LEVEL", "info")) || LEVELS.index("info")
10
38
  end
11
39
  end
12
40
 
@@ -0,0 +1,9 @@
1
+ module Edgeway
2
+ module RateLimit
3
+ class Limiter
4
+ def exceeded?(binding_name, key)
5
+ raise Edgeway::Error, "#{self.class.name} does not check limits"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ require_relative "limiter"
2
+
3
+ module Edgeway
4
+ module RateLimit
5
+ class NullLimiter < Limiter
6
+ def exceeded?(binding_name, key) = false
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,24 @@
1
+ require_relative "limiter"
2
+
3
+ module Edgeway
4
+ module RateLimit
5
+ class StubLimiter < Limiter
6
+ attr_reader :checks
7
+
8
+ def initialize
9
+ @exceeded_bindings = []
10
+ @checks = []
11
+ end
12
+
13
+ def stub(binding_name, exceeded)
14
+ @exceeded_bindings.delete(binding_name)
15
+ @exceeded_bindings << binding_name if exceeded
16
+ end
17
+
18
+ def exceeded?(binding_name, key)
19
+ @checks << [binding_name, key]
20
+ @exceeded_bindings.include?(binding_name)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,2 @@
1
+ require_relative "rate_limit/limiter"
2
+ require_relative "rate_limit/null_limiter"
@@ -17,8 +17,9 @@ module Edgeway
17
17
  def self.log_database_stats(name)
18
18
  stats = Model.query_stats(name)
19
19
  return unless stats.queries > 0
20
- prefix = name == Model::PRIMARY ? "" : "database=#{name} "
21
- Edgeway.logger.info(prefix + stats.log_text)
20
+ fields = stats.log_fields
21
+ fields["database"] = name.to_s unless name == Model::PRIMARY
22
+ Edgeway.logger.info("query stats", fields)
22
23
  end
23
24
  end
24
25
  end
@@ -3,6 +3,7 @@
3
3
  # Spinel's, so CRuby can't read it (tests use edgeway/db/memory).
4
4
  require "json"
5
5
  require_relative "../../db/adapter"
6
+ require_relative "../import_result"
6
7
 
7
8
  module Edgeway
8
9
  module Cloudflare
@@ -34,8 +35,6 @@ module Edgeway
34
35
  # suspended (JSPI), so they read like blocking calls here.
35
36
  module D1Imports
36
37
  ffi_source <<~C
37
- #include <stdlib.h>
38
-
39
38
  #define D1_IMPORT(name) __attribute__((import_module("edgeway_d1"), import_name(name)))
40
39
 
41
40
  D1_IMPORT("query") int js_d1_query(const char *binding, const char *sql, const char *binds);
@@ -44,38 +43,24 @@ module Edgeway
44
43
  D1_IMPORT("batch") int js_d1_batch(const char *binding, const char *statements);
45
44
  D1_IMPORT("take") void js_d1_take(char *buf);
46
45
 
47
- /* The last result, NUL-terminated; grown as needed and reused. */
48
- static char *result;
49
- static int result_cap;
50
-
51
- static const char *take_result(int length) {
52
- if (length + 1 > result_cap) {
53
- char *grown = realloc(result, length + 1);
54
- if (!grown) abort();
55
- result = grown;
56
- result_cap = length + 1;
57
- }
58
- js_d1_take(result);
59
- result[length] = 0;
60
- return result;
61
- }
46
+ static ImportResultBuffer d1_result;
62
47
 
63
48
  /* What ffi_func binds. Its own extern declaration would lose the
64
49
  import attributes, so the imports are wrapped. */
65
50
  const char *d1_query(const char *binding, const char *sql, const char *binds) {
66
- return take_result(js_d1_query(binding, sql, binds));
51
+ return take_import_result(&d1_result, js_d1_query(binding, sql, binds), js_d1_take);
67
52
  }
68
53
 
69
54
  const char *d1_execute(const char *binding, const char *sql, const char *binds) {
70
- return take_result(js_d1_execute(binding, sql, binds));
55
+ return take_import_result(&d1_result, js_d1_execute(binding, sql, binds), js_d1_take);
71
56
  }
72
57
 
73
58
  const char *d1_exec(const char *binding, const char *sql) {
74
- return take_result(js_d1_exec(binding, sql));
59
+ return take_import_result(&d1_result, js_d1_exec(binding, sql), js_d1_take);
75
60
  }
76
61
 
77
62
  const char *d1_batch(const char *binding, const char *statements) {
78
- return take_result(js_d1_batch(binding, statements));
63
+ return take_import_result(&d1_result, js_d1_batch(binding, statements), js_d1_take);
79
64
  }
80
65
  C
81
66
  ffi_func :d1_query, [:str, :str, :str], :str
@@ -3,33 +3,21 @@
3
3
  # (ffi_source is Spinel's; tests use edgeway/http/auth/stub).
4
4
  require "json"
5
5
  require_relative "../../http/auth/jwt"
6
+ require_relative "../import_result"
6
7
 
7
8
  module Edgeway
8
9
  module Cloudflare
9
10
  module JWTImports
10
11
  ffi_source <<~C
11
- #include <stdlib.h>
12
-
13
12
  #define JWT_IMPORT(name) __attribute__((import_module("edgeway_jwt"), import_name(name)))
14
13
 
15
14
  JWT_IMPORT("verify") int js_jwt_verify(const char *token);
16
15
  JWT_IMPORT("take") void js_jwt_take(char *buf);
17
16
 
18
- /* The last result, NUL-terminated; grown as needed and reused. */
19
- static char *jwt_result;
20
- static int jwt_result_cap;
17
+ static ImportResultBuffer jwt_result;
21
18
 
22
19
  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;
20
+ return take_import_result(&jwt_result, js_jwt_verify(token), js_jwt_take);
33
21
  }
34
22
  C
35
23
  ffi_func :jwt_verify, [:str], :str
@@ -0,0 +1,45 @@
1
+ require "json"
2
+ require_relative "../import_result"
3
+ require_relative "kv_store"
4
+
5
+ module Edgeway
6
+ module Cloudflare
7
+ module KVImports
8
+ ffi_source <<~C
9
+ #define KV_IMPORT(name) __attribute__((import_module("edgeway_kv"), import_name(name)))
10
+
11
+ KV_IMPORT("get") int js_kv_get(const char *binding, const char *key);
12
+ KV_IMPORT("put") int js_kv_put(const char *binding, const char *key, const char *text, int expires_in);
13
+ KV_IMPORT("delete") int js_kv_delete(const char *binding, const char *key);
14
+ KV_IMPORT("take") void js_kv_take(char *buffer);
15
+
16
+ static ImportResultBuffer kv_result;
17
+
18
+ const char *kv_get(const char *binding, const char *key) {
19
+ return take_import_result(&kv_result, js_kv_get(binding, key), js_kv_take);
20
+ }
21
+
22
+ const char *kv_put(const char *binding, const char *key, const char *text, int expires_in) {
23
+ return take_import_result(&kv_result, js_kv_put(binding, key, text, expires_in), js_kv_take);
24
+ }
25
+
26
+ const char *kv_delete(const char *binding, const char *key) {
27
+ return take_import_result(&kv_result, js_kv_delete(binding, key), js_kv_take);
28
+ }
29
+ C
30
+ ffi_func :kv_get, [:str, :str], :str
31
+ ffi_func :kv_put, [:str, :str, :str, :int], :str
32
+ ffi_func :kv_delete, [:str, :str], :str
33
+ end
34
+
35
+ class KVImportTransport < KVTransport
36
+ def get(binding_name, key) = KVImports.kv_get(binding_name, key)
37
+
38
+ def put(binding_name, key, text, expires_in_seconds)
39
+ KVImports.kv_put(binding_name, key, text, expires_in_seconds)
40
+ end
41
+
42
+ def delete(binding_name, key) = KVImports.kv_delete(binding_name, key)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,58 @@
1
+ require "json"
2
+ require_relative "../../cache/store"
3
+
4
+ module Edgeway
5
+ module Cloudflare
6
+ class KVTransport
7
+ def get(binding_name, key)
8
+ raise Edgeway::Error, "#{self.class.name} does not get"
9
+ end
10
+
11
+ def put(binding_name, key, text, expires_in_seconds)
12
+ raise Edgeway::Error, "#{self.class.name} does not put"
13
+ end
14
+
15
+ def delete(binding_name, key)
16
+ raise Edgeway::Error, "#{self.class.name} does not delete"
17
+ end
18
+ end
19
+
20
+ class KVStore < Cache::Store
21
+ def initialize(binding:, namespace: nil, expires_in: nil, transport:)
22
+ super(namespace: namespace, expires_in: expires_in)
23
+ @binding_name = binding
24
+ @transport = transport
25
+ end
26
+
27
+ def read_text(key)
28
+ check_key(key)
29
+ answer = parse_answer("get", @transport.get(@binding_name, key))
30
+ answer.nil? ? nil : answer["value"]
31
+ end
32
+
33
+ def write_text(key, text, expires_in_seconds)
34
+ check_key(key)
35
+ !parse_answer("put", @transport.put(@binding_name, key, text, expires_in_seconds)).nil?
36
+ end
37
+
38
+ def delete_text(key)
39
+ check_key(key)
40
+ !parse_answer("delete", @transport.delete(@binding_name, key)).nil?
41
+ end
42
+
43
+ private
44
+
45
+ def check_key(key)
46
+ raise ArgumentError, "cache key can't be . or .." if key == "." || key == ".."
47
+ end
48
+
49
+ def parse_answer(operation, json)
50
+ answer = JSON.parse(json)
51
+ return answer unless answer.key?("error")
52
+
53
+ Edgeway.logger.warn("cache operation failed", { "operation" => operation, "binding" => @binding_name, "error" => answer["error"] })
54
+ nil
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,27 @@
1
+ require "json"
2
+ require_relative "../import_result"
3
+ require_relative "rate_limiter"
4
+
5
+ module Edgeway
6
+ module Cloudflare
7
+ module RateLimitImports
8
+ ffi_source <<~C
9
+ #define RATE_LIMIT_IMPORT(name) __attribute__((import_module("edgeway_ratelimit"), import_name(name)))
10
+
11
+ RATE_LIMIT_IMPORT("check") int js_rate_limit_check(const char *binding, const char *key);
12
+ RATE_LIMIT_IMPORT("take") void js_rate_limit_take(char *buffer);
13
+
14
+ static ImportResultBuffer rate_limit_result;
15
+
16
+ const char *rate_limit_check(const char *binding, const char *key) {
17
+ return take_import_result(&rate_limit_result, js_rate_limit_check(binding, key), js_rate_limit_take);
18
+ }
19
+ C
20
+ ffi_func :rate_limit_check, [:str, :str], :str
21
+ end
22
+
23
+ class RateLimitImportTransport < RateLimitTransport
24
+ def check(binding_name, key) = RateLimitImports.rate_limit_check(binding_name, key)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,33 @@
1
+ require "json"
2
+ require_relative "../../rate_limit"
3
+
4
+ module Edgeway
5
+ module Cloudflare
6
+ class RateLimitTransport
7
+ def check(binding_name, key)
8
+ raise Edgeway::Error, "#{self.class.name} does not check"
9
+ end
10
+ end
11
+
12
+ class RateLimiter < RateLimit::Limiter
13
+ def initialize(transport:)
14
+ @transport = transport
15
+ end
16
+
17
+ def exceeded?(binding_name, key)
18
+ answer = JSON.parse(@transport.check(binding_name, Digest::SHA256.hexdigest(key)))
19
+ raise ConfigurationError, "rate limit: #{answer["misconfigured"]}" if answer.key?("misconfigured")
20
+ return failed_open(binding_name, answer["error"]) if answer.key?("error")
21
+
22
+ answer["success"] == false
23
+ end
24
+
25
+ private
26
+
27
+ def failed_open(binding_name, message)
28
+ Edgeway.logger.warn("rate limit check failed", { "binding" => binding_name, "error" => message })
29
+ false
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,65 @@
1
+ require_relative "d1"
2
+
3
+ module Edgeway
4
+ module Cloudflare
5
+ module TestImports
6
+ ffi_source <<~C
7
+ __attribute__((import_module("edgeway_test"), import_name("reset"))) void js_test_reset(void);
8
+
9
+ int test_reset(void) {
10
+ js_test_reset();
11
+ return 0;
12
+ }
13
+ C
14
+ ffi_func :test_reset, [], :int
15
+ end
16
+
17
+ class TestD1Adapter < D1Adapter
18
+ attr_reader :log
19
+
20
+ def initialize(binding: "DB")
21
+ super(binding: binding)
22
+ @log = []
23
+ end
24
+
25
+ def select_all(sql, binds)
26
+ log_statement(sql, binds)
27
+ super(sql, binds)
28
+ end
29
+
30
+ def execute(sql, binds)
31
+ log_statement(sql, binds)
32
+ super(sql, binds)
33
+ end
34
+
35
+ def exec(sql)
36
+ log_statement(sql, [])
37
+ super(sql)
38
+ end
39
+
40
+ def batch(statements)
41
+ @log << "BATCH #{statements.size}"
42
+ super(statements)
43
+ end
44
+
45
+ def close
46
+ end
47
+
48
+ private
49
+
50
+ def log_statement(sql, binds)
51
+ @log << "#{sql} | #{JSON.generate(binds)}"
52
+ end
53
+ end
54
+ end
55
+
56
+ module TestDatabases
57
+ def self.reset
58
+ Cloudflare::TestImports.test_reset
59
+ end
60
+
61
+ def self.open(name)
62
+ Cloudflare::TestD1Adapter.new(binding: Edgeway::TEST_BINDINGS.fetch(name))
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,26 @@
1
+ require_relative "../http/client"
2
+ require_relative "import_result"
3
+
4
+ module Edgeway
5
+ module FetchImports
6
+ ffi_source <<~C
7
+ #define FETCH_IMPORT(name) __attribute__((import_module("edgeway_fetch"), import_name(name)))
8
+
9
+ FETCH_IMPORT("request") int js_fetch_request(const char *payload);
10
+ FETCH_IMPORT("take") void js_fetch_take(char *buffer);
11
+
12
+ static ImportResultBuffer fetch_result;
13
+
14
+ const char *fetch_request(const char *payload) {
15
+ return take_import_result(&fetch_result, js_fetch_request(payload), js_fetch_take);
16
+ }
17
+ C
18
+ ffi_func :fetch_request, [:str], :str
19
+ end
20
+
21
+ class FetchTransport < HTTP::Transport
22
+ def request(payload)
23
+ FetchImports.fetch_request(payload)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,24 @@
1
+ module Edgeway
2
+ module ImportResult
3
+ ffi_source <<~C
4
+ #include <stdlib.h>
5
+
6
+ typedef struct {
7
+ char *text;
8
+ int capacity;
9
+ } ImportResultBuffer;
10
+
11
+ static const char *take_import_result(ImportResultBuffer *buffer, int length, void (*take)(char *)) {
12
+ if (length + 1 > buffer->capacity) {
13
+ char *grown = realloc(buffer->text, length + 1);
14
+ if (!grown) abort();
15
+ buffer->text = grown;
16
+ buffer->capacity = length + 1;
17
+ }
18
+ take(buffer->text);
19
+ buffer->text[length] = 0;
20
+ return buffer->text;
21
+ }
22
+ C
23
+ end
24
+ end
@@ -0,0 +1,18 @@
1
+ module Edgeway
2
+ module TestDatabases
3
+ # Called before each test.
4
+ def self.reset
5
+ end
6
+
7
+ def self.open(name)
8
+ migrations = name == Edgeway::Model::PRIMARY ? "db/migrations" : "db/#{name}/migrations"
9
+ directory = File.join(Edgeway::TEST_APP_ROOT, migrations)
10
+ if name != Edgeway::Model::PRIMARY && !File.directory?(directory)
11
+ raise Edgeway::ConfigurationError, "database #{name} (config/database.yml) has no #{migrations} directory"
12
+ end
13
+ database = Edgeway::DB::SQLiteAdapter.new(":memory:")
14
+ database.apply_migrations(directory)
15
+ database
16
+ end
17
+ end
18
+ end