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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bd4a7fbadabdbb7170fe43f3a2c5b95c5c48f3ff3922f85c54fc00e0a4c9adc6
4
- data.tar.gz: '0100897c369bb0855515dad4ec9b9c169dcf7a35430283cb51fd81ea3941fbf0'
3
+ metadata.gz: 573d68187596b59f3cffc50dea8b7388b476d51642dbd5590c13ab9ae9c032a1
4
+ data.tar.gz: 777d49ac4d40319da47e24b2f92e3627b6ff69396aba7051fcbeb579d4023107
5
5
  SHA512:
6
- metadata.gz: 0eab446bf859b69de75f4c1ee0963eaff5751c1ab50adefbb0a2df43c959155222f635ffe99e98fac3c686c9008d41a613659212d77245b6922eb58989fff17e
7
- data.tar.gz: a470f4628f5e8de7ff27fde72d1b03e0a83161ab213df976f2f60d6ab0cf82c364b8e2e2e1796ef054e21b1c3336ea34a9d9a3b3602283d8a4d8d078791ad094
6
+ metadata.gz: 20f45f1748cbaff125996e822255ac91d467cbb79c62d016866e0d24788f2b61cd7f36b2f4b6afa7046346f1b4848eaa178a312ef786b07f3bce46fb67a2e629
7
+ data.tar.gz: 01f40c238089497d6d2bae340c6ceb1cd886ab305864bb1d40c0b7f74086e9010b9cc6cbd35fc1403fe53c400ee8ffaae45a1e999b074074a5787c0e72b53079
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # The gem's PropsResolver is plain Ruby except for three ActiveSupport core
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 (Ratalada::Contrib::Inertia delegates to
118
- # InertiaRails).
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
- # The Inertia client posts JSON, which Sinatra does not read into `params`.
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. Mirrors
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 are rewritten to 303: a 303 already forces a GET on
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
- # A response with X-Inertia-Location is already a full page visit —
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
- # XHR follows redirects transparently, so a cross-origin target is only
48
- # reachable through a window.location visit (409 + X-Inertia-Location).
49
- # 307/308 are excluded: a full page visit is always a GET.
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 to keep the rest of the response,
74
- # notably Set-Cookie (which matters mid-OAuth).
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. Shared data is deep-merged
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 in the request context on every
53
- # inertia render and deep-merged under the page props.
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. An app-defined
65
- # page_version setting wins over inertia_version; either may be
66
- # callable, and whatever comes out is stringified.
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 exactly like the core's own.
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 the route it serves: file extensions
20
- # and the +api suffix are dropped, a _layout file stands for its directory,
21
- # group segments are invisible and a trailing index is its directory.
22
- # Mirrors getContextKey/stripInvisibleSegmentsFromPath in expo-router.
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`, the same way as Sinatra: the paths of
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`. Hanami's router already carries a
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`: every route defined while it is set
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 all the way to the pattern. Sinatra's own
16
- # syntax has no way to say `[user-id]`: `:user-id` reads as the
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
- # ONE APP PER ROUTE FILE, NOT ONE FOR THE TREE. `use`, `set`, `before`,
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
- # So each route file (with the layouts above it) becomes its own
33
- # subclass, and they are chained: a Sinatra app used as rack middleware
34
- # forwards the request on when none of its own routes matched
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 in the directory into an app class, under
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 says how its frontend spells a placeholder; the default
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
- # An adapter that can give each route file its own class does, so a
34
- # _layout.rb's `use`, `set`, `before` and `error` reach only the files
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
- # The same, as a whole app built by whichever Ratalada frontend is in
48
- # use — for mounting a directory somewhere other than the server's own
49
- # run block.
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
- # A hash of route => the files that serve it, ordered most specific
55
- # first. Mustermann::Expo does the file path => route translation; all
56
- # this adds is which files to evaluate and how the adapter spells a
57
- # placeholder.
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
- # "/organisations/:id/edit" => ["./app/organisations/[id]/edit.rb"]
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: "/teams/[team]" => "/teams/:team".
85
- # The root route is "" — a prefix nothing hangs off, not a route of "/".
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
- # Least specific last. A segment that is PARTLY literal beats a bare
97
- # capture: `[index].[diffType]` and `[index]` both match "7.diff", and
98
- # the one that spells the dot is the one that means it -- so the
99
- # patterns are anchored to a single capture rather than to the whole
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ratalada
4
4
  module Contrib
5
- VERSION = "3.1.3"
5
+ VERSION = "4.0.1"
6
6
  end
7
7
  end
@@ -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 while
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: 3.1.3
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: '3.0'
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: '3.0'
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