Module: Jimmu::Router

Defined in:
lib/jimmu.rb

Overview

Resolves request paths to .erb files under views/, following the file-based routing convention described in the Jimmu spec:

views/index.erb            ->  /
views/about.erb            ->  /about
views/blog/index.erb       ->  /blog
views/blog/post.erb        ->  /blog/post
views/user/[id].erb        ->  /user/:id
views/post/[category]/[id].erb -> /post/:category/:id

A literal path segment always wins over a [dynamic] one at the same directory level (so views/user/admin.erb beats views/user/[id].erb for the request "/user/admin", while any other value still falls through to the dynamic route).

Directory entries are enumerated directly (Dir.children) rather than matched with Dir.glob bracket patterns, since glob's [...] character-class syntax collides with the literal [name] folder naming convention and is easy to get subtly wrong across relative vs. absolute paths.

Class Method Summary collapse

Class Method Details

.match_pattern(pattern, request_path) ⇒ Object

Matches a single registered pattern (e.g. "/room/[name]") against an incoming request path, used for websocket routes. Returns a params Hash on match, or nil.



929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
# File 'lib/jimmu.rb', line 929

def match_pattern(pattern, request_path)
  pattern_segments = split_segments(pattern)
  path_segments = split_segments(request_path)
  return nil unless pattern_segments.length == path_segments.length

  params = {}
  pattern_segments.each_with_index do |seg, i|
    actual = path_segments[i]
    if seg.start_with?('[') && seg.end_with?(']')
      params[seg[1..-2].to_sym] = actual
    elsif seg != actual
      return nil
    end
  end
  params
end

.resolve_dir(base_dir, segments, params) ⇒ Object



950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
# File 'lib/jimmu.rb', line 950

def resolve_dir(base_dir, segments, params)
  return nil unless Dir.exist?(base_dir)

  if segments.empty?
    idx = File.join(base_dir, 'index.erb')
    return File.file?(idx) ? [idx, params] : nil
  end

  seg = segments[0]
  rest = segments[1..-1]

  if rest.empty?
    exact = File.join(base_dir, "#{seg}.erb")
    return [exact, params] if File.file?(exact)

    subdir = File.join(base_dir, seg)
    if Dir.exist?(subdir)
      idx = File.join(subdir, 'index.erb')
      return [idx, params] if File.file?(idx)
    end
  else
    subdir = File.join(base_dir, seg)
    if Dir.exist?(subdir)
      result = resolve_dir(subdir, rest, params)
      return result if result
    end
  end

  resolve_dynamic(base_dir, seg, rest, params)
end

.resolve_static(public_dir, request_path) ⇒ Object

Returns the on-disk path if request_path maps to a real file under public_dir, else nil. Guards against path traversal.



916
917
918
919
920
921
922
923
924
# File 'lib/jimmu.rb', line 916

def resolve_static(public_dir, request_path)
  return nil if request_path.include?('..')
  decoded = Jimmu.percent_decode(request_path)
  return nil if decoded.include?('..')
  path = File.join(public_dir, decoded)
  return nil unless path.start_with?(File.expand_path(public_dir) + File::SEPARATOR) ||
                     File.expand_path(path).start_with?(File.expand_path(public_dir) + File::SEPARATOR)
  File.file?(path) ? path : nil
end

.resolve_view(views_dir, request_path) ⇒ Object

Returns [file_path, params] or nil.



909
910
911
912
# File 'lib/jimmu.rb', line 909

def resolve_view(views_dir, request_path)
  segments = split_segments(request_path)
  resolve_dir(views_dir, segments, {})
end

.split_segments(path) ⇒ Object



946
947
948
# File 'lib/jimmu.rb', line 946

def split_segments(path)
  path.to_s.split('/').reject(&:empty?)
end