roda-project 0.1.15 → 0.1.17
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/roda/project/bin/generators/routes.rb +60 -25
- data/lib/roda/project/templates/base/app/app.rb.erb +3 -2
- data/lib/roda/project/templates/base/scaffold/AGENTS.md.erb +37 -18
- data/lib/roda/project/templates/base/scaffold/Gemfile.erb +0 -1
- data/lib/roda/project/templates/base/scaffold/boot.rb.erb +16 -4
- data/lib/roda/project/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6fa6f449cc4a6f6e7342135d2de859c481b517dc9666ca9e9d64bbc5141b27b5
|
|
4
|
+
data.tar.gz: f95248f05abe4a1df4b5cd1018823c9243d1fa27fab9904b6f3c9dc966f972e4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9022aca6837ac8803328e3d785514fe3c93d9bdbd2a272c1eee5c1ea231298543db5ef3840fd53f83ba5a6f56d92199a1750d8462635b3146ddb6adea4407067
|
|
7
|
+
data.tar.gz: a589f4edadacd2f4aa54a5ef53526764c6bebefbacc16553768fc0d24a38851c7744577c843279d84a8976a287aeb0208f37e66f15e6f2ec5a8e4c0a4a060645
|
|
@@ -12,9 +12,9 @@ class Roda
|
|
|
12
12
|
|
|
13
13
|
puts "* creating routes"
|
|
14
14
|
generate_routes
|
|
15
|
+
generate_nested_branch_files
|
|
15
16
|
generate_views
|
|
16
17
|
generate_tests
|
|
17
|
-
print_nested_branch_reminder
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
private
|
|
@@ -24,10 +24,10 @@ class Roda
|
|
|
24
24
|
route_definitions = routes_list.map do |route_str|
|
|
25
25
|
method, name = parse_route_string(route_str)
|
|
26
26
|
view_name = name
|
|
27
|
-
view_name = "index" if view_name ==
|
|
27
|
+
view_name = "index" if view_name == nil
|
|
28
28
|
view_line = (method == "get" && must_generate_views?) ? "\n view('#{view_name}')" : ""
|
|
29
29
|
|
|
30
|
-
if name !=
|
|
30
|
+
if name != nil
|
|
31
31
|
" r.#{method} \"#{name}\" do#{view_line}\n end"
|
|
32
32
|
else
|
|
33
33
|
" r.#{method} do#{view_line}\n end"
|
|
@@ -37,9 +37,8 @@ class Roda
|
|
|
37
37
|
hash_branch_header = if branch_name.include?("/")
|
|
38
38
|
parts = branch_name.split("/")
|
|
39
39
|
sub_path = parts[0..-2].join("/")
|
|
40
|
-
namespace = sub_path.include?("/") ? ":\"#{sub_path}\"" : ":#{sub_path}"
|
|
41
40
|
branch_segment = parts.last
|
|
42
|
-
"hash_branch #{
|
|
41
|
+
"hash_branch #{namespace_for(sub_path)}, \"#{branch_segment}\" do |r|"
|
|
43
42
|
else
|
|
44
43
|
"hash_branch \"#{branch_name}\" do |r|"
|
|
45
44
|
end
|
|
@@ -62,7 +61,7 @@ class Roda
|
|
|
62
61
|
routes_list.each do |route_str|
|
|
63
62
|
method, name = parse_route_string(route_str)
|
|
64
63
|
if method == "get"
|
|
65
|
-
name = "index" if name ==
|
|
64
|
+
name = "index" if name == nil
|
|
66
65
|
view_filename = File.join(branch_views_dir, "#{name}.erb")
|
|
67
66
|
File.write(view_filename, "")
|
|
68
67
|
action_success_message(view_filename)
|
|
@@ -79,14 +78,14 @@ class Roda
|
|
|
79
78
|
test_route_definitions = routes_list.map do |route_str|
|
|
80
79
|
method, name = parse_route_string(route_str)
|
|
81
80
|
|
|
82
|
-
if name !=
|
|
81
|
+
if name != nil
|
|
83
82
|
" it \"responds to #{method.upcase} /#{branch_name}/#{name}\" do\n" \
|
|
84
83
|
" #{method} \"/#{branch_name}/#{name}\"\n" \
|
|
85
84
|
" expect(last_response.status).to eq(200)\n" \
|
|
86
85
|
" end"
|
|
87
86
|
else
|
|
88
|
-
" it \"responds to #{method.upcase} /#{branch_name} do\n" \
|
|
89
|
-
" #{method} \"/#{branch_name}\n" \
|
|
87
|
+
" it \"responds to #{method.upcase} /#{branch_name}\" do\n" \
|
|
88
|
+
" #{method} \"/#{branch_name}\"\n" \
|
|
90
89
|
" expect(last_response.status).to eq(200)\n" \
|
|
91
90
|
" end"
|
|
92
91
|
end
|
|
@@ -103,13 +102,63 @@ class Roda
|
|
|
103
102
|
action_success_message(test_filename)
|
|
104
103
|
end
|
|
105
104
|
|
|
105
|
+
def generate_nested_branch_files
|
|
106
|
+
return unless branch_name.include?("/")
|
|
107
|
+
|
|
108
|
+
parts = branch_name.split("/")
|
|
109
|
+
parts[0..-2].each_with_index do |_segment, index|
|
|
110
|
+
prefix = parts[0..index].join("/")
|
|
111
|
+
filename = File.join("app/routes", "#{prefix}.rb")
|
|
112
|
+
|
|
113
|
+
if File.exist?(filename)
|
|
114
|
+
ensure_hash_branches_line(filename, namespace_for(prefix))
|
|
115
|
+
next
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
dir = File.dirname(filename)
|
|
119
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
|
120
|
+
File.write(filename, nested_branch_file_content(prefix, index))
|
|
121
|
+
action_success_message(filename)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def nested_branch_file_content(prefix, index)
|
|
126
|
+
parts = prefix.split("/")
|
|
127
|
+
segment = parts.last
|
|
128
|
+
header = if index.zero?
|
|
129
|
+
"hash_branch \"#{segment}\" do |r|"
|
|
130
|
+
else
|
|
131
|
+
parent_path = parts[0..-2].join("/")
|
|
132
|
+
"hash_branch #{namespace_for(parent_path)}, \"#{segment}\" do |r|"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
<<~RUBY
|
|
136
|
+
class #{@context.const_project_name}
|
|
137
|
+
#{header} # #{prefix} branch
|
|
138
|
+
r.hash_branches(#{namespace_for(prefix)}) # #{prefix}/ +1 routes
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
RUBY
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def ensure_hash_branches_line(filename, namespace)
|
|
145
|
+
expected_line = "r.hash_branches(#{namespace})"
|
|
146
|
+
return if File.read(filename).include?(expected_line)
|
|
147
|
+
|
|
148
|
+
puts " warning: #{filename} already exists — please add:"
|
|
149
|
+
puts " #{expected_line}"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def namespace_for(sub_path)
|
|
153
|
+
sub_path.include?("/") ? ":\"#{sub_path}\"" : ":#{sub_path}"
|
|
154
|
+
end
|
|
155
|
+
|
|
106
156
|
def must_generate_views?
|
|
107
157
|
@options[:views] && Dir.exist?("app/views")
|
|
108
158
|
end
|
|
109
159
|
|
|
110
160
|
def parse_route_string(route_str)
|
|
111
|
-
|
|
112
|
-
method ||= "get"
|
|
161
|
+
method, name = route_str.split(":", 2)
|
|
113
162
|
[method, name]
|
|
114
163
|
end
|
|
115
164
|
|
|
@@ -120,20 +169,6 @@ class Roda
|
|
|
120
169
|
def branch_name
|
|
121
170
|
@branch_name ||= @args[0]
|
|
122
171
|
end
|
|
123
|
-
|
|
124
|
-
def print_nested_branch_reminder
|
|
125
|
-
return unless branch_name.include?("/")
|
|
126
|
-
|
|
127
|
-
parts = branch_name.split("/")
|
|
128
|
-
sub_path = parts[0..-2].join("/")
|
|
129
|
-
namespace = sub_path.include?("/") ? ":\"#{sub_path}\"" : ":#{sub_path}"
|
|
130
|
-
|
|
131
|
-
puts "\ndont forget to add:\n\n" \
|
|
132
|
-
"autoload_hash_branch_dir(#{namespace}, \"./app/routes/#{sub_path}\")\n\n" \
|
|
133
|
-
"route do |r|\n" \
|
|
134
|
-
" r.on(\"#{sub_path}\") { r.hash_branches(#{namespace}) }\n" \
|
|
135
|
-
"end\n"
|
|
136
|
-
end
|
|
137
172
|
end
|
|
138
173
|
end
|
|
139
174
|
end
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
class <%= context.const_project_name %> < Roda
|
|
2
2
|
# Routing<% if context.fullstack? %>
|
|
3
|
-
plugin :
|
|
3
|
+
plugin :hash_branch_view_subdir<% end %>
|
|
4
4
|
plugin :autoload_hash_branches
|
|
5
|
-
autoload_hash_branch_dir(
|
|
5
|
+
HASH_BRANCHES.each { |args| autoload_hash_branch_dir(*args) }
|
|
6
|
+
|
|
6
7
|
plugin :all_verbs
|
|
7
8
|
plugin :not_found
|
|
8
9
|
<% if context.fullstack? %>
|
|
@@ -1,47 +1,66 @@
|
|
|
1
1
|
# Project Code Generation Rules
|
|
2
2
|
|
|
3
3
|
## Core Stack
|
|
4
|
+
|
|
4
5
|
- **Web Framework**: Roda (STRICT: Do NOT use Rails, ActionController, or Sinatra conventions).
|
|
5
6
|
- **No Rails Magic:** Do not use `ActiveSupport` methods (like `.present?` or `.blank?`) unless the gem is explicitly in the Gemfile.<% if context.database %>
|
|
6
7
|
- **ORM**: Sequel (STRICT: Do NOT use ActiveRecord. Use Sequel Models, Datasets, and Migrations).<% end %>
|
|
7
8
|
- **Autoloader**: Zeitwerk (Do NOT manually `require` files located in the `app/` directory. Only require standard libraries or gems).<% if context.fullstack? %>
|
|
8
9
|
- **Frontend**: esbuild + ERB (Fullstack mode).<% end %>
|
|
9
10
|
|
|
10
|
-
## Code Style & Conventions
|
|
11
|
+
## Code Style & Conventions<%= i = 0 %>
|
|
12
|
+
|
|
13
|
+
### <%= i=i+1 %>. Types
|
|
11
14
|
|
|
12
|
-
### 1. Types
|
|
13
15
|
- Rely on standard Ruby 3.x patterns. Use explicit YARD docs for method signatures.
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
<% if context.fullstack? %>### <%= i=i+1 %>. Views & Presentation
|
|
18
|
+
|
|
19
|
+
- ERB templates go in `app/views/`.
|
|
20
|
+
- The `view` method inside a route block will match the hash branch to find the view path location:
|
|
21
|
+
- For `hash_branch "foo/bar", "zas" do |r|`, calling `view('something')` will search an erb file in "views/foo/bar/zas/something.erb".
|
|
22
|
+
- For `hash_branch "zas" do |r|`, calling `view('something')` will search an erb file in "views/zas/something.erb".
|
|
23
|
+
|
|
24
|
+
<% end %>### <%= i=i+1 %>. Routing
|
|
25
|
+
|
|
16
26
|
- The main app resides in `app/[project_name].rb`.
|
|
17
27
|
- Sub-routes MUST be implemented using Roda **Hash Branches** (`hash_branches` plugin) and placed in `app/routes/`.
|
|
18
|
-
-
|
|
28
|
+
- **Strict Segment Limit per Branch:** Each hash branch must contain only **one** additional route segment and only **one** CRUD. Do not create deep routing blocks, examples:
|
|
29
|
+
- **Allowed** inside `hash_branch "foo" do |r|`: Handling `GET "foo/bar"`, `POST "foo/bar"`, `GET "foo/something"`, UPDATE `foo/[id]/bar`, GET `foo/[id]/bar` or `GET "foo"`.
|
|
30
|
+
- **FORBIDDEN** inside `hash_branch "foo" do |r|`: Handling deeper segments like `GET "foo/bar/zas"` or `GET "foo/resource/[id]/edit"`.
|
|
31
|
+
- **Solution for Deep Routes:** For deeper paths, you MUST create a new, nested hash branch (e.g., `hash_branch "foo", "bar" do |r|`), in a new nested directory like "app/routes/foo/bar" with views (if any) inside a new view directory like "views/foo/bar".
|
|
32
|
+
- Use Roda's tree routing efficiently (`r.on`, `r.is`, `r.get`, `r.post`).
|
|
33
|
+
- You **MUST** use the `bin/roda g routes` generator for create new route files, examples:
|
|
34
|
+
- `bin/roda g routes branch/name get:foo post:bar` creates a file containing `GET "branch/name/foo"` and `POST "branch/name/bar"` routes.
|
|
35
|
+
- `bin/roda g routes branch_name get post delete:foo` creates a file containing `GET "branch_name"`, `POST "branch_name"` and `DELETE "branch_name/foo"` routes.<% if context.database %>
|
|
36
|
+
|
|
37
|
+
### <%= i=i+1 %>. Database & Models (Sequel)
|
|
19
38
|
|
|
20
|
-
### 3. Database & Models (Sequel)
|
|
21
39
|
- Models reside in `app/models/`.
|
|
22
40
|
- Migrations reside in `db/migrations/` and MUST use sequential numbering (e.g., `001_...`).
|
|
41
|
+
- Seeds reside in db/migrations/seeds.rb.
|
|
23
42
|
- When writing database queries, use Sequel's dataset methods (e.g., `where`, `select`, `exclude`, `join`). Do not use string interpolation for SQL queries.
|
|
24
43
|
|
|
25
|
-
<% end
|
|
26
|
-
- ERB templates go in `app/views/`.
|
|
44
|
+
<% end %>### <%= i=i+1 %>. Testing
|
|
27
45
|
|
|
28
|
-
<% end %>### 5. Testing
|
|
29
46
|
- Specs reside in `spec/app/`.
|
|
30
47
|
- Use `rack-test` for route testing.
|
|
31
48
|
- Ensure database transactions are rolled back after each test block.
|
|
32
49
|
- Mirror the file structure of `app/` inside `spec/app/` (e.g., `app/models/user.rb` -> `spec/app/models/user_spec.rb`).
|
|
50
|
+
- Each route **MUST** have tests for each behavior.
|
|
51
|
+
- Each model custom public method **MUST** have tests.
|
|
52
|
+
|
|
53
|
+
### <%= i=i+1 %>. Config
|
|
33
54
|
|
|
34
|
-
|
|
35
|
-
-
|
|
55
|
+
- **STRICT:** Config values and ENV **MUST** be centralized in `app/config/config.rb`.
|
|
56
|
+
- External tools, gems or services that need a single initialization or/and specific configuration reside in `app/config/providers/*`.
|
|
57
|
+
|
|
58
|
+
### <%= i=i+1 %>. Boot Sequence & Initialization
|
|
59
|
+
|
|
60
|
+
- **`boot.rb`**: The absolute starting point. It sets up `Bundler`, global constants, boots `Zeitwerk` for autoloading the `app/` directory, and loads tools, gems or service providers from `app/config/providers/`.
|
|
36
61
|
- **`config.ru`**: The Rack entrypoint. It defines middleware (like `Rack::LiveReload` in development) and mounts the primary Roda application class defined in `app/[project_name].rb`.
|
|
37
62
|
|
|
38
|
-
###
|
|
39
|
-
- Do NOT run `bundle exec rake` for scaffolding/servers. The project uses a custom CLI: `bin/roda`. Suggest commands using `bin/roda` (e.g., generators, running the server).
|
|
63
|
+
### <%= i=i+1 %>. Tooling
|
|
40
64
|
|
|
41
|
-
|
|
65
|
+
- Do NOT run `bundle exec rake` for scaffolding/servers. The project uses a custom CLI: `bin/roda`.
|
|
42
66
|
- To maintain consistency and avoid manual boilerplate, the project uses custom generators for scaffolding new features. These automatically hook into Zeitwerk's expectations, Sequel's DSL, and Roda's Hash Branches.
|
|
43
|
-
- **`bin/roda g routes <branch> [options]`**: Scaffolds a Roda hash branch file in `app/routes/`, matching spec files, and ERB views (for GET routes). Supports nested paths (e.g., `api/v1`) and bare verbs (e.g., `books :get :post`).<% if context.database %>
|
|
44
|
-
- **`bin/roda g model <name> [fields]`**: Generates a Sequel ORM model class, a matching model spec, and a timestamped Sequel migration. Supports namespacing (`Admin/User`) and auto-pluralizes table names.
|
|
45
|
-
- **`bin/roda g migration <name> [fields]`**: Creates a timestamped Sequel migration. It automatically detects intent from the name (e.g., `add_status_to_users` generates the `alter_table` block) and maps Rails-like field types (e.g., `user:references`, `body:text`) to Sequel schema methods.
|
|
46
|
-
<% end %>- **`bin/roda g service <type> <name>`**: Scaffolds a class or module service in `app/services/` and a matching spec. Defaults to `class` if type is omitted.
|
|
47
|
-
|
|
@@ -31,11 +31,23 @@ Providers::Mailer.boot
|
|
|
31
31
|
Providers::Logger.boot
|
|
32
32
|
|
|
33
33
|
# Consts<% if context.database %>
|
|
34
|
-
DB = Providers::DB::Conn.get
|
|
34
|
+
DB = Providers::DB::Conn.get<% end %>
|
|
35
|
+
ROUTES_DIR = "./app/routes"
|
|
36
|
+
base_path = Pathname.new(ROUTES_DIR)
|
|
37
|
+
HASH_BRANCHES = []
|
|
38
|
+
HASH_BRANCHES << [base_path]
|
|
39
|
+
Dir.glob(File.join(ROUTES_DIR, "**/*")).each do |dir_path|
|
|
40
|
+
next unless File.directory?(dir_path)
|
|
41
|
+
|
|
42
|
+
path = Pathname.new(dir_path)
|
|
43
|
+
relative_path = path.relative_path_from(base_path).to_s
|
|
44
|
+
|
|
45
|
+
HASH_BRANCHES << [relative_path.to_sym, path.to_s]
|
|
46
|
+
end
|
|
35
47
|
|
|
36
48
|
if Config.not_production?
|
|
37
|
-
require "debug"
|
|
49
|
+
require "debug"<% if context.database %>
|
|
38
50
|
# Automatic models annotations in dev environment
|
|
39
51
|
require 'sequel/annotate'
|
|
40
|
-
Sequel::Annotate.annotate(Dir['app/models/*.rb'], position: :before, border: true) rescue nil
|
|
41
|
-
end
|
|
52
|
+
Sequel::Annotate.annotate(Dir['app/models/*.rb'], position: :before, border: true) rescue nil<% end %>
|
|
53
|
+
end
|
data/lib/roda/project/version.rb
CHANGED