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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d30cdd4308ec4495bd965180994b34d26439ba7f0127398562eb381cc8eccc95
4
- data.tar.gz: bec8c448d5e15fb2b25478f1cb31e299514fdd2ef9067c81681244e54a0bdae1
3
+ metadata.gz: cd2d16bc3b97e31abe859281ecc0ba7a322a6bce7dc1a71e854a21d80e470fdb
4
+ data.tar.gz: 4f4139c69c3183ddae2b4c2df734424e0a18be4d31b10f937491be9718753156
5
5
  SHA512:
6
- metadata.gz: e72898aabde80621c44d55d2f12a13dcd9210d65c5690aaa444f0b867b81354300e2c6904fbb60e6ddf9c97a784b918616c8dfd3c45ad55ffd84df5fb2d4e19f
7
- data.tar.gz: 97151f7ad7867f3856c7da8971b2d9b63318716bb10319f9865a7c12b92dc61c1559756d5a50db14c4c846ed9f777307e091629545e7ad8a68d676c860636b6d
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ratalada
4
4
  module Contrib
5
- VERSION = "2.0.1"
5
+ VERSION = "3.0.0"
6
6
  end
7
7
  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: 2.0.1
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: '2.0'
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: '2.0'
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