syntropy 0.20 → 0.21

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: 652c2d240350e3a5d0ce18cdd73a76a2ea3920a58ca777f46c34aa8a6e567ecd
4
- data.tar.gz: cd49374a2469e501ea9adec27db55f9b047a418e7f1291894a9dd68c7d2e367f
3
+ metadata.gz: 8069c4fe6a21289babda24dae14b580c5ea476a5d77bdcb09e31ae72d751459c
4
+ data.tar.gz: de80dbc652706f64819cef25d23ca81f19ed0e628d2860ae01a0e5792ea1bc6d
5
5
  SHA512:
6
- metadata.gz: f1861340956ffbf7db1b0163ec7a1fd84f1b2c422ae9d25ab7cfa531b70459e22e306876bba6e1865e1cc398475d536a5726dbe276744b127fe416851162fe62
7
- data.tar.gz: 8ccf9cc252f7efa6e086ca830fbc9a6f836ecb43f91880d9c399aeb05fe8ca42677de6f1493785ef436c3b5a3f999ed50c7834c17b37ff1b779ed24539016ffd
6
+ metadata.gz: ec0769f1619a71b926a19c56499b28c4e2331d05d417c8d0d6d3b21c1804a2ea2e52c93307a716c289c381b617bb66fcd174bb92b0c9b612f80f7561b32f0bba
7
+ data.tar.gz: b836b5a0bd012ae3a179c515cb177959723da53e99c0bb2212a1b5caab6270b38a9a33282b73279a6915ac768324cd61604ce6db02237f33b55980145c87f5e8
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ # 0.21 2025-09-29
2
+
3
+ - Fix routing with wildcard `index+.rb` modules
4
+
1
5
  # 0.20 2025-09-17
2
6
 
3
7
  - Update Papercraft
data/lib/syntropy/app.rb CHANGED
@@ -110,7 +110,9 @@ module Syntropy
110
110
  @router_proc = @routing_tree.router_proc
111
111
  end
112
112
 
113
-
113
+ # Mounts the builtin applet on the routing tree.
114
+ #
115
+ # @return [void]
114
116
  def mount_builtin_applet
115
117
  path = @env[:builtin_applet_path]
116
118
  @builtin_applet ||= Syntropy.builtin_applet(@env, mount_path: path)
@@ -309,6 +309,10 @@ module Syntropy
309
309
  plus, ext = m[1..2]
310
310
  kind = FILE_TYPE[ext]
311
311
  handle_subtree = (plus == '+') && (kind == :module)
312
+ if handle_subtree
313
+ path = path.gsub(/\/index\+$/, '')
314
+ path = '/' if path.empty?
315
+ end
312
316
  set_index_route_target(parent:, path:, kind:, fn:, handle_subtree:)
313
317
  end
314
318
 
@@ -443,29 +447,79 @@ module Syntropy
443
447
  # @return [String] router proc code to be `eval`ed
444
448
  def generate_routing_tree_code
445
449
  buffer = +''
446
- buffer << "# frozen_string_literal: true\n"
450
+ buffer << "# frozen_string_literal: true\n\n"
447
451
 
448
- emit_code_line(buffer, '->(path, params) {')
449
- emit_code_line(buffer, ' entry = @static_map[path]; return entry if entry')
450
- emit_code_line(buffer, ' parts = path.split("/")')
452
+ wildcard_root = @root[:handle_subtree]
453
+ childless_root = !@root[:children] || @root[:children].empty?
451
454
 
452
- if @root[:path] != '/'
453
- root_parts = @root[:path].split('/')
454
- segment_idx = root_parts.size
455
- validate_parts = []
456
- (1..(segment_idx - 1)).each do |i|
457
- validate_parts << "(parts[#{i}] != #{root_parts[i].inspect})"
458
- end
459
- emit_code_line(buffer, " return nil if #{validate_parts.join(' || ')}")
455
+ if wildcard_root && childless_root
456
+ emit_wildcard_childless_root_code(buffer, @root[:path])
460
457
  else
458
+ emit_router_proc_prelude(buffer)
461
459
  segment_idx = 1
460
+ if @root[:path] != '/'
461
+ root_parts = @root[:path].split('/')
462
+ segment_idx = root_parts.size
463
+ emit_root_validate_guard(buffer:, root_parts:)
464
+ end
465
+
466
+ visit_routing_tree_entry(buffer:, entry: @root, segment_idx:)
467
+ emit_router_proc_postlude(buffer, default_route_path: wildcard_root && @root[:path])
462
468
  end
463
469
 
464
- visit_routing_tree_entry(buffer:, entry: @root, segment_idx:)
470
+ buffer#.tap { puts '*' * 40; puts it; puts }
471
+ end
472
+
473
+ # Emits optimized code for a childless wildcard router.
474
+ #
475
+ # @param buffer [String] output buffer
476
+ # @param root_path [String] router root path
477
+ # @return [void]
478
+ def emit_wildcard_childless_root_code(buffer, root_path)
479
+ emit_code_line(buffer, '->(path, params) {')
480
+ if root_path != '/'
481
+ re = /^#{Regexp.escape(root_path)}(\/.*)?$/
482
+ emit_code_line(buffer, " return if path !~ #{re.inspect}")
483
+ end
484
+ emit_code_line(buffer, " @dynamic_map[#{root_path.inspect}]")
485
+ emit_code_line(buffer, '}')
486
+ end
487
+
488
+ # Emits router proc prelude code.
489
+ #
490
+ # @param buffer [String] output buffer
491
+ # @return [void]
492
+ def emit_router_proc_prelude(buffer)
493
+ emit_code_line(buffer, '->(path, params) {')
494
+ emit_code_line(buffer, ' entry = @static_map[path]; return entry if entry')
495
+ emit_code_line(buffer, ' parts = path.split("/")')
496
+ end
497
+
498
+ # Emits root path validation guard code.
499
+ #
500
+ # @param buffer [String] output buffer
501
+ # @param root_parts [Array<String>] root path parts
502
+ # @return [void]
503
+ def emit_root_validate_guard(buffer:, root_parts:)
504
+ validate_parts = []
505
+ (1...root_parts.size).each do |i|
506
+ validate_parts << "(parts[#{i}] != #{root_parts[i].inspect})"
507
+ end
508
+ emit_code_line(buffer, " return nil if #{validate_parts.join(' || ')}")
509
+ end
465
510
 
466
- emit_code_line(buffer, " return nil")
511
+ # Emits router proc postlude code.
512
+ #
513
+ # @param buffer [String] output buffer
514
+ # @param default_route_path [String, nil] default route path
515
+ # @return [void]
516
+ def emit_router_proc_postlude(buffer, default_route_path:)
517
+ if default_route_path
518
+ emit_code_line(buffer, " return @dynamic_map[#{default_route_path.inspect}]")
519
+ else
520
+ emit_code_line(buffer, " return nil")
521
+ end
467
522
  emit_code_line(buffer, "}")
468
- buffer#.tap { puts '*' * 40; puts it; puts }
469
523
  end
470
524
 
471
525
  # Generates routing logic code for the given route entry.
@@ -507,40 +561,7 @@ module Syntropy
507
561
  end
508
562
 
509
563
  if entry[:children]
510
- param_entry = entry[:children]['[]']
511
- entry[:children].each do |k, child_entry|
512
- # skip if wildcard entry (treated in else clause below)
513
- next if k == '[]'
514
-
515
- # skip if entry is void (no target, no children)
516
- has_target = child_entry[:target]
517
- has_children = child_entry[:children] && !child_entry[:children].empty?
518
- next if !has_target && !has_children
519
-
520
- if has_target && !has_children
521
- # use the target
522
- next if child_entry[:static]
523
-
524
- emit_code_line(buffer, "#{ws}when #{k.inspect}")
525
- if_clause = child_entry[:handle_subtree] ? '' : " if !parts[#{segment_idx + 1}]"
526
- route_value = "@dynamic_map[#{child_entry[:path].inspect}]"
527
- emit_code_line(buffer, "#{ws} return #{route_value}#{if_clause}")
528
-
529
- elsif has_children
530
- # otherwise look at the next segment
531
- next if is_void_route?(child_entry) && !param_entry
532
-
533
- emit_code_line(buffer, "#{ws}when #{k.inspect}")
534
- visit_routing_tree_entry(buffer:, entry: child_entry, indent: indent + 1, segment_idx: segment_idx + 1)
535
- end
536
- end
537
-
538
- # parametric route
539
- if param_entry
540
- emit_code_line(buffer, "#{ws}else")
541
- emit_code_line(buffer, "#{ws} params[#{param_entry[:param].inspect}] = p")
542
- visit_routing_tree_entry(buffer:, entry: param_entry, indent: indent + 1, segment_idx: segment_idx + 1)
543
- end
564
+ emit_routing_tree_entry_children_clauses(buffer:, entry:, indent:, segment_idx:)
544
565
  end
545
566
  emit_code_line(buffer, "#{ws}end")
546
567
  end
@@ -564,7 +585,7 @@ module Syntropy
564
585
  # @param entry [Hash] route entry
565
586
  # @return [bool]
566
587
  def is_void_route?(entry)
567
- return false if entry[:param]
588
+ return false if entry[:param] || entry[:target]
568
589
 
569
590
  if entry[:children]
570
591
  return true if !entry[:children]['[]'] && entry[:children]&.values&.all? { is_void_route?(it) }
@@ -575,6 +596,58 @@ module Syntropy
575
596
  false
576
597
  end
577
598
 
599
+ # Emits case clauses for the given entry's children.
600
+ #
601
+ # @param buffer [String] output buffer
602
+ # @param entry [Hash] route entry
603
+ # @param indent [Integer] indent level
604
+ # @param segment_idx [Integer] path segment index
605
+ # @return [void]
606
+ def emit_routing_tree_entry_children_clauses(buffer:, entry:, indent:, segment_idx:)
607
+ ws = ' ' * (indent * 2)
608
+
609
+ param_entry = entry[:children]['[]']
610
+ entry[:children].each do |k, child_entry|
611
+ # skip if wildcard entry (treated in else clause below)
612
+ next if k == '[]'
613
+
614
+ # skip if entry is void (no target, no children)
615
+ has_target = child_entry[:target]
616
+ has_children = child_entry[:children] && !child_entry[:children].empty?
617
+ next if !has_target && !has_children
618
+
619
+ if has_target && !has_children
620
+ # use the target
621
+ next if child_entry[:static]
622
+
623
+ emit_code_line(buffer, "#{ws}when #{k.inspect}")
624
+ if_clause = child_entry[:handle_subtree] ? '' : " if !parts[#{segment_idx + 1}]"
625
+
626
+ child_path = child_entry[:path]
627
+ route_value = "@dynamic_map[#{child_path.inspect}]"
628
+ emit_code_line(buffer, "#{ws} return #{route_value}#{if_clause}")
629
+
630
+ elsif has_children
631
+ # otherwise look at the next segment
632
+ next if is_void_route?(child_entry) && !param_entry
633
+
634
+ emit_code_line(buffer, "#{ws}when #{k.inspect}")
635
+ visit_routing_tree_entry(buffer:, entry: child_entry, indent: indent + 1, segment_idx: segment_idx + 1)
636
+ end
637
+ end
638
+
639
+ # parametric route
640
+ if param_entry
641
+ emit_code_line(buffer, "#{ws}else")
642
+ emit_code_line(buffer, "#{ws} params[#{param_entry[:param].inspect}] = p")
643
+ visit_routing_tree_entry(buffer:, entry: param_entry, indent: indent + 1, segment_idx: segment_idx + 1)
644
+ # wildcard route
645
+ elsif entry[:handle_subtree]
646
+ emit_code_line(buffer, "#{ws}else")
647
+ emit_code_line(buffer, "#{ws} return @dynamic_map[#{entry[:path].inspect}]")
648
+ end
649
+ end
650
+
578
651
  DEBUG = !!ENV['DEBUG']
579
652
 
580
653
  # Emits the given code into the given buffer, with a line break at the end.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
- VERSION = '0.20'
4
+ VERSION = '0.21'
5
5
  end
@@ -408,3 +408,167 @@ class RoutingTreeTest < Minitest::Test
408
408
  }
409
409
  end
410
410
  end
411
+
412
+ class RoutingTreeWildcardIndexTest < Minitest::Test
413
+ def test_wildcard_root_index_routing_on_docs
414
+ file_tree = {
415
+ 'site': {
416
+ 'index+.rb': '',
417
+ }
418
+ }
419
+
420
+ @root_dir = "/tmp/#{__FILE__.gsub('/', '-')}-#{SecureRandom.hex}"
421
+ make_tmp_file_tree(@root_dir, file_tree)
422
+ @rt = Syntropy::RoutingTree.new(root_dir: File.join(@root_dir, 'site'), mount_path: '/docs')
423
+
424
+ router = @rt.router_proc
425
+
426
+ route = router.('/docs', {})
427
+ assert_equal '/docs', route[:path]
428
+
429
+ route = router.('/docs/foo', {})
430
+ assert_equal '/docs', route[:path]
431
+
432
+ route = router.('/docs/foo/bar', {})
433
+ assert_equal '/docs', route[:path]
434
+
435
+ route = router.('/docsa', {})
436
+ assert_nil route
437
+ end
438
+
439
+ def test_wildcard_root_index_routing_on_root
440
+ file_tree = {
441
+ 'site': {
442
+ 'index+.rb': '',
443
+ }
444
+ }
445
+
446
+ @root_dir = "/tmp/#{__FILE__.gsub('/', '-')}-#{SecureRandom.hex}"
447
+ make_tmp_file_tree(@root_dir, file_tree)
448
+ @rt = Syntropy::RoutingTree.new(root_dir: File.join(@root_dir, 'site'), mount_path: '/')
449
+
450
+ router = @rt.router_proc
451
+
452
+ route = router.('/', {})
453
+ assert_equal '/', route[:path]
454
+
455
+ route = router.('/foo', {})
456
+ assert_equal '/', route[:path]
457
+
458
+ route = router.('/foo/bar', {})
459
+ assert_equal '/', route[:path]
460
+ end
461
+
462
+ def test_wildcard_root_index_with_children
463
+ file_tree = {
464
+ 'site': {
465
+ 'about.rb': '',
466
+ 'foo+.rb': '',
467
+ 'index+.rb': '',
468
+ }
469
+ }
470
+
471
+ @root_dir = "/tmp/#{__FILE__.gsub('/', '-')}-#{SecureRandom.hex}"
472
+ make_tmp_file_tree(@root_dir, file_tree)
473
+ @rt = Syntropy::RoutingTree.new(root_dir: File.join(@root_dir, 'site'), mount_path: '/docs')
474
+ router = @rt.router_proc
475
+
476
+ route = router.('/docs', {})
477
+ assert_equal '/docs', route[:path]
478
+
479
+ route = router.('/docs/about', {})
480
+ assert_equal '/docs/about', route[:path]
481
+
482
+ route = router.('/docs/foo', {})
483
+ assert_equal '/docs/foo+', route[:path]
484
+
485
+ route = router.('/docs/foo/bar', {})
486
+ assert_equal '/docs/foo+', route[:path]
487
+
488
+ route = router.('/docs/baz', {})
489
+ assert_equal '/docs', route[:path]
490
+
491
+ route = router.('/docsa', {})
492
+ assert_nil route
493
+ end
494
+
495
+ def test_wildcard_root_index_with_static_children
496
+ file_tree = {
497
+ 'site': {
498
+ 'about.rb': '',
499
+ 'foo.rb': '',
500
+ 'index+.rb': '',
501
+ }
502
+ }
503
+
504
+ @root_dir = "/tmp/#{__FILE__.gsub('/', '-')}-#{SecureRandom.hex}"
505
+ make_tmp_file_tree(@root_dir, file_tree)
506
+ @rt = Syntropy::RoutingTree.new(root_dir: File.join(@root_dir, 'site'), mount_path: '/docs')
507
+ router = @rt.router_proc
508
+
509
+ route = router.('/docs', {})
510
+ assert_equal '/docs', route[:path]
511
+
512
+ route = router.('/docs/about', {})
513
+ assert_equal '/docs/about', route[:path]
514
+
515
+ route = router.('/docs/foo', {})
516
+ assert_equal '/docs/foo', route[:path]
517
+
518
+ route = router.('/docs/foo/bar', {})
519
+ assert_equal '/docs', route[:path]
520
+
521
+ route = router.('/docs/baz', {})
522
+ assert_equal '/docs', route[:path]
523
+
524
+ route = router.('/docsa', {})
525
+ assert_nil route
526
+ end
527
+
528
+ def test_wildcard_nested_index
529
+ file_tree = {
530
+ 'site': {
531
+ 'foo': {
532
+ 'index+.rb': ''
533
+ },
534
+ 'bar': {
535
+ 'about.rb': '',
536
+ 'foo.rb': '',
537
+ 'index+.rb': '',
538
+ },
539
+ # 'baz+.rb': ''
540
+ }
541
+ }
542
+
543
+ @root_dir = "/tmp/#{__FILE__.gsub('/', '-')}-#{SecureRandom.hex}"
544
+ make_tmp_file_tree(@root_dir, file_tree)
545
+ @rt = Syntropy::RoutingTree.new(root_dir: File.join(@root_dir, 'site'), mount_path: '/docs')
546
+ router = @rt.router_proc
547
+
548
+ route = router.('/docs', {})
549
+ assert_nil route
550
+
551
+ route = router.('/docs/foo', {})
552
+ assert_equal '/docs/foo', route[:path]
553
+
554
+ route = router.('/docs/foo/bar', {})
555
+ assert_equal '/docs/foo', route[:path]
556
+
557
+ route = router.('/docs/foo/about', {})
558
+ assert_equal '/docs/foo', route[:path]
559
+
560
+ route = router.('/docs/bar', {})
561
+ assert_equal '/docs/bar', route[:path]
562
+
563
+ route = router.('/docs/bar/baz', {})
564
+ assert_equal '/docs/bar', route[:path]
565
+
566
+ route = router.('/docs/bar/about', {})
567
+ assert_equal '/docs/bar/about', route[:path]
568
+
569
+ route = router.('/docs/bars/about', {})
570
+ assert_nil route
571
+ end
572
+
573
+
574
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: syntropy
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.20'
4
+ version: '0.21'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner