ratalada-contrib 3.1.3 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/ratalada/contrib/inertia/core_ext.rb +1 -3
- data/lib/ratalada/contrib/inertia/helpers.rb +5 -2
- data/lib/ratalada/contrib/inertia/json_params_middleware.rb +1 -2
- data/lib/ratalada/contrib/inertia/middleware.rb +14 -14
- data/lib/ratalada/contrib/inertia/response.rb +1 -5
- data/lib/ratalada/contrib/inertia.rb +9 -6
- data/lib/ratalada/contrib/router/file_based/expo.rb +4 -5
- data/lib/ratalada/contrib/router/file_based/grape_adapter.rb +1 -2
- data/lib/ratalada/contrib/router/file_based/hanami_adapter.rb +1 -3
- data/lib/ratalada/contrib/router/file_based/sinatra_adapter.rb +7 -25
- data/lib/ratalada/contrib/router/file_based.rb +33 -28
- data/lib/ratalada/contrib/version.rb +1 -1
- data/lib/ratalada/contrib/vite.rb +2 -4
- metadata +17 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 573d68187596b59f3cffc50dea8b7388b476d51642dbd5590c13ab9ae9c032a1
|
|
4
|
+
data.tar.gz: 777d49ac4d40319da47e24b2f92e3627b6ff69396aba7051fcbeb579d4023107
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 20f45f1748cbaff125996e822255ac91d467cbb79c62d016866e0d24788f2b61cd7f36b2f4b6afa7046346f1b4848eaa178a312ef786b07f3bce46fb67a2e629
|
|
7
|
+
data.tar.gz: 01f40c238089497d6d2bae340c6ceb1cd886ab305864bb1d40c0b7f74086e9010b9cc6cbd35fc1403fe53c400ee8ffaae45a1e999b074074a5787c0e72b53079
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
# extensions it calls on every resolve. We don't load ActiveSupport, so here
|
|
5
|
-
# they are, AS-compatible.
|
|
3
|
+
# Three ActiveSupport extensions PropsResolver needs, since we don't load it.
|
|
6
4
|
class Object
|
|
7
5
|
def try(method_name, ...) = respond_to?(method_name) ? public_send(method_name, ...) : nil
|
|
8
6
|
|
|
@@ -114,8 +114,11 @@ module Ratalada
|
|
|
114
114
|
request.env["ratalada.inertia.csrf_token"]
|
|
115
115
|
end
|
|
116
116
|
|
|
117
|
-
# The prop factories are the gem's
|
|
118
|
-
#
|
|
117
|
+
# The prop factories are the gem's.
|
|
118
|
+
#
|
|
119
|
+
# @parameter value [Object] a literal value, if any.
|
|
120
|
+
# @parameter block [Proc] evaluated lazily, if given.
|
|
121
|
+
# @returns [Object] the InertiaRails prop.
|
|
119
122
|
def always(value = nil, &block) = Ratalada::Contrib::Inertia.always(value, &block)
|
|
120
123
|
def defer(group: "default", &block) = Ratalada::Contrib::Inertia.defer(group: group, &block)
|
|
121
124
|
def optional(&block) = Ratalada::Contrib::Inertia.optional(&block)
|
|
@@ -5,8 +5,7 @@ require "json"
|
|
|
5
5
|
module Ratalada
|
|
6
6
|
module Contrib
|
|
7
7
|
module Inertia
|
|
8
|
-
#
|
|
9
|
-
# This parses the body into the hash Rack::Request builds `params` from.
|
|
8
|
+
# Parses the JSON Inertia posts, which Sinatra ignores, into `params`.
|
|
10
9
|
class JsonParamsMiddleware
|
|
11
10
|
def initialize(app) = @app = app
|
|
12
11
|
|
|
@@ -3,11 +3,7 @@
|
|
|
3
3
|
module Ratalada
|
|
4
4
|
module Contrib
|
|
5
5
|
module Inertia
|
|
6
|
-
# Rack middleware for the Inertia protocol.
|
|
7
|
-
# InertiaRails::Middleware but built on plain Rack rather than
|
|
8
|
-
# ActionDispatch — the gem's version also manages its Rails flash and
|
|
9
|
-
# session keys, which this app does not use (errors live in
|
|
10
|
-
# session[:_inertia_errors] and are swept by the render helpers).
|
|
6
|
+
# Rack middleware for the Inertia protocol, on plain Rack.
|
|
11
7
|
class Middleware
|
|
12
8
|
INERTIA_HEADER = "HTTP_X_INERTIA"
|
|
13
9
|
INERTIA_VERSION_HEADER = "HTTP_X_INERTIA_VERSION"
|
|
@@ -27,14 +23,12 @@ module Ratalada
|
|
|
27
23
|
status, headers, body = convert_to_location_response!(headers, body)
|
|
28
24
|
end
|
|
29
25
|
|
|
30
|
-
# Only 301/302
|
|
31
|
-
# follow, and 307/308 preserve the request method by design.
|
|
26
|
+
# Only 301/302 become 303; 307/308 preserve the method by design.
|
|
32
27
|
if inertia_non_get_redirect?(env, status)
|
|
33
28
|
status = 303
|
|
34
29
|
end
|
|
35
30
|
|
|
36
|
-
#
|
|
37
|
-
# nothing to refresh.
|
|
31
|
+
# X-Inertia-Location is already a full page visit; nothing to refresh.
|
|
38
32
|
if stale_inertia_get?(env) && !headers["X-Inertia-Location"]
|
|
39
33
|
force_refresh(env)
|
|
40
34
|
else
|
|
@@ -44,9 +38,12 @@ module Ratalada
|
|
|
44
38
|
|
|
45
39
|
private
|
|
46
40
|
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
41
|
+
# Whether a redirect crosses origins, which needs a full visit.
|
|
42
|
+
#
|
|
43
|
+
# @parameter env [Hash] the rack env.
|
|
44
|
+
# @parameter status [Integer] the response status.
|
|
45
|
+
# @parameter headers [Hash] the response headers.
|
|
46
|
+
# @returns [Boolean] true for a cross-origin redirect.
|
|
50
47
|
def external_redirect?(env, status, headers)
|
|
51
48
|
inertia_request?(env) &&
|
|
52
49
|
REDIRECT_STATUSES.include?(status) &&
|
|
@@ -70,8 +67,11 @@ module Ratalada
|
|
|
70
67
|
false
|
|
71
68
|
end
|
|
72
69
|
|
|
73
|
-
# Mutates the headers in place
|
|
74
|
-
#
|
|
70
|
+
# Mutates the headers in place, keeping Set-Cookie.
|
|
71
|
+
#
|
|
72
|
+
# @parameter headers [Hash] the response headers.
|
|
73
|
+
# @parameter body [Object] the response body, closed here.
|
|
74
|
+
# @returns [Array] a 409 with the Inertia-Location header.
|
|
75
75
|
def convert_to_location_response!(headers, body)
|
|
76
76
|
headers["X-Inertia-Location"] = headers.delete("Location")
|
|
77
77
|
headers.delete("Content-Type")
|
|
@@ -5,11 +5,7 @@ require "json"
|
|
|
5
5
|
module Ratalada
|
|
6
6
|
module Contrib
|
|
7
7
|
module Inertia
|
|
8
|
-
# Builds the Inertia page object for a render.
|
|
9
|
-
# under the page props and errors are merged on top; prop resolution —
|
|
10
|
-
# partial reloads, deferred/optional/merge/once/scroll metadata — is the
|
|
11
|
-
# gem's PropsResolver, evaluating blocks in the request context via the
|
|
12
|
-
# gem's PropEvaluator.
|
|
8
|
+
# Builds the Inertia page object for a render.
|
|
13
9
|
class Response
|
|
14
10
|
attr_reader(
|
|
15
11
|
:component,
|
|
@@ -49,8 +49,10 @@ module Ratalada
|
|
|
49
49
|
def cache(...) = ::InertiaRails::CachedProp.new(...)
|
|
50
50
|
def scroll(metadata = nil, **options, &block) = ::InertiaRails::ScrollProp.new(metadata: metadata, **options, &block)
|
|
51
51
|
|
|
52
|
-
# Registers a shared-prop block, run
|
|
53
|
-
#
|
|
52
|
+
# Registers a shared-prop block, run on every inertia render.
|
|
53
|
+
#
|
|
54
|
+
# @parameter block [Proc] produces the shared props, called in the request context and deep-merged under the page props.
|
|
55
|
+
# @returns [Array] the registered share blocks.
|
|
54
56
|
def share(&block)
|
|
55
57
|
unless block
|
|
56
58
|
raise ArgumentError, "share requires a block"
|
|
@@ -61,10 +63,11 @@ module Ratalada
|
|
|
61
63
|
|
|
62
64
|
def share_props(&block) = share(&block)
|
|
63
65
|
|
|
64
|
-
# The asset version sent with every page object.
|
|
65
|
-
#
|
|
66
|
-
#
|
|
66
|
+
# The asset version sent with every page object.
|
|
67
|
+
#
|
|
68
|
+
# @returns [String] the configured version, stringified.
|
|
67
69
|
def current_version
|
|
70
|
+
# page_version wins over inertia_version; either may be callable.
|
|
68
71
|
config = Ratalada.config
|
|
69
72
|
if config.respond_to?(:page_version)
|
|
70
73
|
version = config.page_version
|
|
@@ -82,7 +85,7 @@ unless defined?(::Inertia)
|
|
|
82
85
|
::Inertia = Ratalada::Contrib::Inertia
|
|
83
86
|
end
|
|
84
87
|
|
|
85
|
-
# Contrib settings, registered on the core config
|
|
88
|
+
# Contrib settings, registered on the core config.
|
|
86
89
|
Ratalada.setting :inertia_version, default: "1"
|
|
87
90
|
Ratalada.setting :inertia_layout, default: :layout
|
|
88
91
|
Ratalada.setting :inertia_encrypt_history, default: false
|
|
@@ -16,11 +16,10 @@ module Mustermann
|
|
|
16
16
|
NOT_FOUND = "+not-found"
|
|
17
17
|
NOT_FOUND_CAPTURE = "unmatched"
|
|
18
18
|
|
|
19
|
-
# Turns an Expo Router file path into
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
# @!visibility private
|
|
19
|
+
# Turns an Expo Router file path into its route.
|
|
20
|
+
#
|
|
21
|
+
# @parameter string [String] the file path, relative to the route root.
|
|
22
|
+
# @returns [String] the route it serves.
|
|
24
23
|
def self.route(string)
|
|
25
24
|
string = string.sub(%r{\A(?:\.\.?/|/)+}, "")
|
|
26
25
|
string = string.sub(/(\+api)?\.rb$/, "").sub(%r{/?_layout$}, "")
|
|
@@ -8,8 +8,7 @@ module Ratalada
|
|
|
8
8
|
module Contrib
|
|
9
9
|
module Router
|
|
10
10
|
module FileBased
|
|
11
|
-
# Teaches Grape `route_prefix`,
|
|
12
|
-
# every route defined while it is set hang off it.
|
|
11
|
+
# Teaches Grape `route_prefix`, as Sinatra does.
|
|
13
12
|
module GrapeAdapter
|
|
14
13
|
def route(methods, paths = ["/"], route_options = {}, &block)
|
|
15
14
|
unless route_prefix.to_s.empty?
|
|
@@ -8,9 +8,7 @@ module Ratalada
|
|
|
8
8
|
module Contrib
|
|
9
9
|
module Router
|
|
10
10
|
module FileBased
|
|
11
|
-
# Teaches Hanami::API `route_prefix
|
|
12
|
-
# path prefix — it is what `scope` sets for the length of its block —
|
|
13
|
-
# so this is that, set until the next file.
|
|
11
|
+
# Teaches Hanami::API `route_prefix`, via its router's path prefix.
|
|
14
12
|
module HanamiAdapter
|
|
15
13
|
def route_prefix=(prefix)
|
|
16
14
|
@router.instance_variable_set(:@path_prefix, ::Hanami::Router::Prefix.new("/").join(prefix.to_s))
|
|
@@ -8,37 +8,19 @@ module Ratalada
|
|
|
8
8
|
module Contrib
|
|
9
9
|
module Router
|
|
10
10
|
module FileBased
|
|
11
|
-
# Teaches Sinatra `route_prefix
|
|
12
|
-
# hangs off it, which is how a route file's own `get "/members"` lands
|
|
13
|
-
# under the prefix its path spelled.
|
|
11
|
+
# Teaches Sinatra `route_prefix`.
|
|
14
12
|
module SinatraAdapter
|
|
15
|
-
# Expo spelling, kept
|
|
16
|
-
#
|
|
17
|
-
# capture `user` followed by the literal `-id`, so a hyphenated
|
|
18
|
-
# parameter silently routes nowhere. Mustermann::Expo is the pattern
|
|
19
|
-
# the file path already is.
|
|
13
|
+
# Expo spelling, kept to the pattern: Sinatra cannot spell `[user-id]`.
|
|
14
|
+
# `:user-id` reads as capture `user` then literal `-id`, routing nowhere.
|
|
20
15
|
SPELLING = {placeholder: "[%s]", catch_all: "[...%s]"}.freeze
|
|
21
16
|
|
|
22
17
|
def route_spelling = SPELLING
|
|
23
18
|
|
|
24
|
-
#
|
|
25
|
-
# `error` and `helpers` are all class-level Sinatra DSL, so evaluating
|
|
26
|
-
# every file into a single Sinatra::Base makes a _layout.rb reach files
|
|
27
|
-
# it does not sit above: an app/(api)/_layout.rb with
|
|
28
|
-
# `before { content_type(:json) }` relabels every HTML page in the tree.
|
|
29
|
-
# A layout is meant to scope those to its own subtree, which it can only
|
|
30
|
-
# do if the files under it have a class of their own.
|
|
19
|
+
# Gives each route file a Sinatra subclass, chained as middleware.
|
|
31
20
|
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
# (Base#route_missing), so the first file that spells the path answers
|
|
36
|
-
# and the rest fall through. build_map already orders them
|
|
37
|
-
# most-specific-first.
|
|
38
|
-
#
|
|
39
|
-
# ponytail: one middleware hop per route file, walked per request.
|
|
40
|
-
# Collapse the leaves into one class if a fat tree ever shows up in a
|
|
41
|
-
# profile -- the isolation is the point, not the chain.
|
|
21
|
+
# @parameter parent [Class] the app the subclasses mount on.
|
|
22
|
+
# @parameter map [Hash] route prefix => the files under it.
|
|
23
|
+
# @returns [void]
|
|
42
24
|
def mount_files(parent, map)
|
|
43
25
|
map.each do |prefix, paths|
|
|
44
26
|
child = Class.new(::Sinatra::Base)
|
|
@@ -12,16 +12,16 @@ module Ratalada
|
|
|
12
12
|
|
|
13
13
|
module_function
|
|
14
14
|
|
|
15
|
-
# Evaluates every route file
|
|
16
|
-
# the prefix its path spells. That class is the frontend's own, so this
|
|
17
|
-
# is what a Server.run block calls: the block is already being evaluated
|
|
18
|
-
# into it, and `self` is it. Requiring the frontend's adapter is what
|
|
19
|
-
# teaches the class `route_prefix`.
|
|
15
|
+
# Evaluates every route file under the directory into the app.
|
|
20
16
|
#
|
|
17
|
+
# @example
|
|
21
18
|
# Server.run { Ratalada::Contrib::Router::FileBased.mount(self, "app") }
|
|
19
|
+
#
|
|
20
|
+
# @parameter app [Object] the frontend app class.
|
|
21
|
+
# @parameter directory [String] the route file root.
|
|
22
|
+
# @returns [void]
|
|
22
23
|
def mount(app, directory)
|
|
23
|
-
# The adapter
|
|
24
|
-
# is `:name`, which is what hanami and grape read.
|
|
24
|
+
# The adapter spells placeholders; `:name` suits hanami and grape.
|
|
25
25
|
if app.respond_to?(:route_spelling)
|
|
26
26
|
spelling = app.route_spelling
|
|
27
27
|
else
|
|
@@ -30,10 +30,8 @@ module Ratalada
|
|
|
30
30
|
|
|
31
31
|
map = build_map(directory, **spelling)
|
|
32
32
|
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
# below it. Without that the whole tree shares one class and a nested
|
|
36
|
-
# layout's filter fires for every route in the app.
|
|
33
|
+
# Per-file classes where the adapter can, so `_layout.rb` scopes downward.
|
|
34
|
+
# One shared class makes a nested layout's filter fire everywhere.
|
|
37
35
|
if app.respond_to?(:mount_files)
|
|
38
36
|
app.mount_files(app, map)
|
|
39
37
|
else
|
|
@@ -44,21 +42,25 @@ module Ratalada
|
|
|
44
42
|
end
|
|
45
43
|
end
|
|
46
44
|
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
45
|
+
# Builds a whole app from the directory, for mounting elsewhere.
|
|
46
|
+
#
|
|
47
|
+
# @parameter directory [String] the route file root.
|
|
48
|
+
# @returns [Object] the built app.
|
|
50
49
|
def build(directory)
|
|
51
50
|
Ratalada.frontend.build(proc { FileBased.mount(self, directory) })
|
|
52
51
|
end
|
|
53
52
|
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
53
|
+
# Route => the files that serve it, most specific first.
|
|
54
|
+
#
|
|
55
|
+
# @example
|
|
56
|
+
# {
|
|
57
|
+
# "/organisations/:id/edit" => ["./app/organisations/[id]/edit.rb"]
|
|
58
|
+
# }
|
|
58
59
|
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
60
|
+
# @parameter directory [String] the route file root.
|
|
61
|
+
# @parameter placeholder [String] the adapter's placeholder spelling.
|
|
62
|
+
# @parameter catch_all [String] the adapter's catch-all spelling.
|
|
63
|
+
# @returns [Hash] route => files.
|
|
62
64
|
def build_map(directory, placeholder: ":%s", catch_all: "*%s")
|
|
63
65
|
root = File.expand_path(directory)
|
|
64
66
|
|
|
@@ -81,8 +83,12 @@ module Ratalada
|
|
|
81
83
|
.push(File.join(root, relative))
|
|
82
84
|
end
|
|
83
85
|
|
|
84
|
-
# An expo route in the adapter's own spelling
|
|
85
|
-
#
|
|
86
|
+
# An expo route in the adapter's own spelling.
|
|
87
|
+
#
|
|
88
|
+
# @parameter route [String] the expo route.
|
|
89
|
+
# @parameter placeholder [String] the adapter's placeholder spelling.
|
|
90
|
+
# @parameter catch_all [String] the adapter's catch-all spelling.
|
|
91
|
+
# @returns [String] the adapter's route.
|
|
86
92
|
def spell(route, placeholder, catch_all)
|
|
87
93
|
route.delete_suffix("/").gsub(%r{\[(\.\.\.)?([^\[\]/]+)\]}) do
|
|
88
94
|
format(Regexp.last_match(1) ? catch_all : placeholder, Regexp.last_match(2))
|
|
@@ -93,11 +99,10 @@ module Ratalada
|
|
|
93
99
|
"#{prefix}#{route.delete_suffix("/")}".sub(/\A\z/, "/")
|
|
94
100
|
end
|
|
95
101
|
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
# segment, which `[index].[diffType]` would also satisfy.
|
|
102
|
+
# How specific a route is, as per-segment weights.
|
|
103
|
+
#
|
|
104
|
+
# @parameter route [String] the route path.
|
|
105
|
+
# @returns [Array(Integer)] the segment weights, least specific last.
|
|
101
106
|
def specificity(route)
|
|
102
107
|
route.split("/").drop(1).map do |segment|
|
|
103
108
|
case segment
|
|
@@ -3,11 +3,9 @@ require "vite_ruby"
|
|
|
3
3
|
module Ratalada
|
|
4
4
|
module Contrib
|
|
5
5
|
module Vite
|
|
6
|
-
# Rack middleware that fronts the app with vite's dev-server proxy
|
|
7
|
-
# `bin/vite dev` is running, and is a pass-through otherwise (production,
|
|
8
|
-
# or a build served from the manifest). The decision is ViteRuby's
|
|
9
|
-
# `run_proxy?`, read once at boot like any other `Server.use` argument.
|
|
6
|
+
# Rack middleware that fronts the app with vite's dev-server proxy.
|
|
10
7
|
#
|
|
8
|
+
# @example
|
|
11
9
|
# Server.use(Ratalada::Contrib::Vite::DevServerProxy).run { ... }
|
|
12
10
|
class DevServerProxy
|
|
13
11
|
def initialize(app, ssl_verify_none: true)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ratalada-contrib
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 4.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nathan K
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
18
|
+
version: '4.0'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '
|
|
25
|
+
version: '4.0'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: mustermann
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -37,6 +37,20 @@ dependencies:
|
|
|
37
37
|
- - ">"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '2'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: inertia_rails
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 3.22.0
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: 3.22.0
|
|
40
54
|
description: |
|
|
41
55
|
Optional add-on modules for ratalada that do not belong in the core gem.
|
|
42
56
|
Require only the ones you use; each pulls its own framework (the
|