ratalada-contrib 2.0.1 → 3.0.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 +4 -4
- data/lib/ratalada/contrib/router/file_based/expo.rb +55 -0
- data/lib/ratalada/contrib/router/file_based/grape_adapter.rb +33 -0
- data/lib/ratalada/contrib/router/file_based/hanami_adapter.rb +24 -0
- data/lib/ratalada/contrib/router/file_based/sinatra_adapter.rb +34 -0
- data/lib/ratalada/contrib/router/file_based.rb +87 -0
- data/lib/ratalada/contrib/version.rb +1 -1
- metadata +22 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cd2d16bc3b97e31abe859281ecc0ba7a322a6bce7dc1a71e854a21d80e470fdb
|
|
4
|
+
data.tar.gz: 4f4139c69c3183ddae2b4c2df734424e0a18be4d31b10f937491be9718753156
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 601080c5ec4fff081b714aaca9d6499bea7eda3d97ef1cfd7f5ea355e00178094c81d52921024d414bf07663fd4090c37d3e943b40bf53c52977629cd85b92f4
|
|
7
|
+
data.tar.gz: 66c6a9b432555f7d0a542f1eed3d364d108cdb2fbb9b2f4e6cc2f414020997369e2a4ac3518f05402400e09a9cfd3c39c8f7a9bcad4825214966c5635065d661
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mustermann"
|
|
4
|
+
require "mustermann/ast/pattern"
|
|
5
|
+
|
|
6
|
+
module Mustermann
|
|
7
|
+
# Expo Router style pattern implementation.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# Mustermann.new('/[foo]', type: :expo) === '/bar' # => true
|
|
11
|
+
#
|
|
12
|
+
# @see Mustermann::Pattern
|
|
13
|
+
class Expo < AST::Pattern
|
|
14
|
+
register :expo
|
|
15
|
+
|
|
16
|
+
NOT_FOUND = "+not-found"
|
|
17
|
+
NOT_FOUND_CAPTURE = "unmatched"
|
|
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
|
|
24
|
+
def self.route(string)
|
|
25
|
+
string = string.sub(%r{\A(?:\.\.?/|/)+}, "")
|
|
26
|
+
string = string.sub(/(\+api)?\.rb$/, "").sub(%r{/?_layout$}, "")
|
|
27
|
+
segments = (?/ + string.delete_prefix(?/)).split(?/, -1)
|
|
28
|
+
if segments.last == "index"
|
|
29
|
+
segments.pop
|
|
30
|
+
end
|
|
31
|
+
segments.reject! { |s| s =~ /\A\(.+\)\z/ }
|
|
32
|
+
if segments.last == NOT_FOUND
|
|
33
|
+
segments[-1] = "[...#{NOT_FOUND_CAPTURE}]"
|
|
34
|
+
end
|
|
35
|
+
segments.join(?/).then { |route| route.empty? ? ?/ : route }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def initialize(string, **options)
|
|
39
|
+
super(Expo.route(string), **options)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
on(nil, ?]) { |c| unexpected(c) }
|
|
43
|
+
|
|
44
|
+
on(?[) do |char|
|
|
45
|
+
name = expect(%r{[^\[\]/]+}, char: char)
|
|
46
|
+
expect(?])
|
|
47
|
+
|
|
48
|
+
if name.start_with?("...")
|
|
49
|
+
node(:named_splat, name[3..], constraint: ".+", convert: ->(e) { e.split(?/) })
|
|
50
|
+
else
|
|
51
|
+
node(:capture, name)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ratalada/grape"
|
|
4
|
+
|
|
5
|
+
require_relative "../file_based"
|
|
6
|
+
|
|
7
|
+
module Ratalada
|
|
8
|
+
module Contrib
|
|
9
|
+
module Router
|
|
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.
|
|
13
|
+
module GrapeAdapter
|
|
14
|
+
def route(methods, paths = ["/"], route_options = {}, &block)
|
|
15
|
+
unless route_prefix.to_s.empty?
|
|
16
|
+
paths = Array(paths).map { |path| FileBased.join(route_prefix, path.to_s) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class Grape::API::Instance # rubocop:disable Style/ClassAndModuleChildren
|
|
28
|
+
class << self
|
|
29
|
+
attr_accessor :route_prefix
|
|
30
|
+
|
|
31
|
+
prepend Ratalada::Contrib::Router::FileBased::GrapeAdapter
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ratalada/hanami"
|
|
4
|
+
|
|
5
|
+
require_relative "../file_based"
|
|
6
|
+
|
|
7
|
+
module Ratalada
|
|
8
|
+
module Contrib
|
|
9
|
+
module Router
|
|
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.
|
|
14
|
+
module HanamiAdapter
|
|
15
|
+
def route_prefix=(prefix)
|
|
16
|
+
@router.instance_variable_set(:@path_prefix, ::Hanami::Router::Prefix.new("/").join(prefix.to_s))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Hanami::API.extend(Ratalada::Contrib::Router::FileBased::HanamiAdapter)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ratalada/sinatra"
|
|
4
|
+
|
|
5
|
+
require_relative "../file_based"
|
|
6
|
+
|
|
7
|
+
module Ratalada
|
|
8
|
+
module Contrib
|
|
9
|
+
module Router
|
|
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.
|
|
14
|
+
module SinatraAdapter
|
|
15
|
+
def route(verb, path, options = {}, &block)
|
|
16
|
+
unless route_prefix.to_s.empty?
|
|
17
|
+
path = FileBased.join(route_prefix, path)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class Sinatra::Base # rubocop:disable Style/ClassAndModuleChildren
|
|
29
|
+
set :route_prefix, ""
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
prepend Ratalada::Contrib::Router::FileBased::SinatraAdapter
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ratalada"
|
|
4
|
+
|
|
5
|
+
require_relative "file_based/expo"
|
|
6
|
+
|
|
7
|
+
module Ratalada
|
|
8
|
+
module Contrib
|
|
9
|
+
module Router
|
|
10
|
+
module FileBased
|
|
11
|
+
LAYOUT = "_layout.rb"
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# The app, built by whichever Ratalada frontend is in use: every route
|
|
16
|
+
# file is evaluated into the frontend's own class, under the prefix its
|
|
17
|
+
# path spells. Requiring the frontend's adapter is what teaches that
|
|
18
|
+
# class `route_prefix`.
|
|
19
|
+
def build(directory)
|
|
20
|
+
map = build_map(directory)
|
|
21
|
+
|
|
22
|
+
routes = proc do
|
|
23
|
+
map.each do |prefix, paths|
|
|
24
|
+
self.route_prefix = prefix
|
|
25
|
+
paths.each { |path| class_eval(File.read(path), path, 1) }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
Ratalada.frontend.build(routes)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# A hash of route => the files that serve it, ordered most specific
|
|
33
|
+
# first. Mustermann::Expo does the file path => route translation; all
|
|
34
|
+
# this adds is which files to evaluate and how the adapter spells a
|
|
35
|
+
# placeholder.
|
|
36
|
+
#
|
|
37
|
+
# {
|
|
38
|
+
# "/organisations/:id/edit" => ["./app/organisations/[id]/edit.rb"]
|
|
39
|
+
# }
|
|
40
|
+
def build_map(directory, placeholder: ":%s", catch_all: "*%s")
|
|
41
|
+
root = File.expand_path(directory)
|
|
42
|
+
|
|
43
|
+
Dir.glob("**/*.rb", base: root)
|
|
44
|
+
.reject { |relative| relative.split("/").any? { |segment| segment.start_with?("_") } }
|
|
45
|
+
.map { |relative| [Mustermann::Expo.route(relative), files(root, relative)] }
|
|
46
|
+
.sort_by { |(route, _paths)| [specificity(route), route] }
|
|
47
|
+
.to_h { |(route, paths)| [spell(route, placeholder, catch_all), paths] }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def files(root, relative)
|
|
51
|
+
directories = relative.split("/")[0..-2]
|
|
52
|
+
|
|
53
|
+
layouts = directories.each_index.map do |index|
|
|
54
|
+
File.join(root, *directories[0..index], LAYOUT)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
[File.join(root, LAYOUT), *layouts]
|
|
58
|
+
.select { |path| File.file?(path) }
|
|
59
|
+
.push(File.join(root, relative))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# An expo route in the adapter's own spelling: "/teams/[team]" => "/teams/:team".
|
|
63
|
+
# The root route is "" — a prefix nothing hangs off, not a route of "/".
|
|
64
|
+
def spell(route, placeholder, catch_all)
|
|
65
|
+
route.delete_suffix("/").gsub(%r{\[(\.\.\.)?([^\[\]/]+)\]}) do
|
|
66
|
+
format(Regexp.last_match(1) ? catch_all : placeholder, Regexp.last_match(2))
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def join(prefix, route)
|
|
71
|
+
"#{prefix}#{route.delete_suffix("/")}".sub(/\A\z/, "/")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def specificity(route)
|
|
75
|
+
route.split("/").drop(1).map do |segment|
|
|
76
|
+
case segment
|
|
77
|
+
when "[...#{Mustermann::Expo::NOT_FOUND_CAPTURE}]" then 3
|
|
78
|
+
when /\A\[\.\.\..+\]\z/ then 2
|
|
79
|
+
when /\A\[.+\]\z/ then 1
|
|
80
|
+
else 0
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
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: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nathan K
|
|
@@ -15,14 +15,28 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
18
|
+
version: '3.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: '3.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: mustermann
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2'
|
|
26
40
|
description: |
|
|
27
41
|
Optional add-on modules for ratalada that do not belong in the core gem.
|
|
28
42
|
Require only the ones you use; each pulls its own framework (the
|
|
@@ -41,6 +55,11 @@ files:
|
|
|
41
55
|
- lib/ratalada/contrib/inertia/helpers.rb
|
|
42
56
|
- lib/ratalada/contrib/inertia/middleware.rb
|
|
43
57
|
- lib/ratalada/contrib/inertia/response.rb
|
|
58
|
+
- lib/ratalada/contrib/router/file_based.rb
|
|
59
|
+
- lib/ratalada/contrib/router/file_based/expo.rb
|
|
60
|
+
- lib/ratalada/contrib/router/file_based/grape_adapter.rb
|
|
61
|
+
- lib/ratalada/contrib/router/file_based/hanami_adapter.rb
|
|
62
|
+
- lib/ratalada/contrib/router/file_based/sinatra_adapter.rb
|
|
44
63
|
- lib/ratalada/contrib/version.rb
|
|
45
64
|
- lib/ratalada/contrib/vite.rb
|
|
46
65
|
homepage: https://github.com/n-at-han-k/ratalada
|