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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5c22151c8712cbc374521e061d5c91e5b4dae3b55ab5d55fc3227fbf57ef49c6
4
- data.tar.gz: 31f190125960494423689e00f70fe16fb32c05da6ea5a5c356fa33b99792d8f3
3
+ metadata.gz: 6fa6f449cc4a6f6e7342135d2de859c481b517dc9666ca9e9d64bbc5141b27b5
4
+ data.tar.gz: f95248f05abe4a1df4b5cd1018823c9243d1fa27fab9904b6f3c9dc966f972e4
5
5
  SHA512:
6
- metadata.gz: 5533eef1235cbbf17bdee8305595a2eee7b32f298960a2b70b080f3ef633f329115d96ae8dcad13490cdfcbc03e092b1ed4559df1fb696604b482e3283ac0ef1
7
- data.tar.gz: 74c4da7361e380d5eb8af1c16f61f06c7540e5fd5464b9df12c184aa4bea4b171f9903e8800bba8c99fcca5f792b67b1b914c1a9a65a86a360fb49c7ec6e586f
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 #{namespace}, \"#{branch_segment}\" do |r|"
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
- name, method = route_str.split(":", 2)
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 :hash_branch_view_namespace<% end %>
3
+ plugin :hash_branch_view_subdir<% end %>
4
4
  plugin :autoload_hash_branches
5
- autoload_hash_branch_dir("./app/routes")
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
- ### 2. Routing (Roda)
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
- - Do not create deep routing blocks. Use Roda's tree routing efficiently (`r.on`, `r.is`, `r.get`, `r.post`).<% if context.database %>
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 %><% if context.fullstack? %>### 4. Views & Presentation
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
- ### 6. Boot Sequence & Initialization
35
- - **`boot.rb`**: The absolute starting point. It sets up `Bundler`, boots `Zeitwerk` for autoloading the `app/` directory, and loads service providers from `app/config/providers/`.
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
- ### 7. Tooling
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
- #### Generators
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
-
@@ -23,7 +23,6 @@ gem "puma"
23
23
 
24
24
  # Views
25
25
  gem 'roda-assets_manifest'
26
- gem 'roda-hash_branch_view_namespace'
27
26
  <% end %>
28
27
  # Performance
29
28
  gem "oj"
@@ -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<% end %>
52
+ Sequel::Annotate.annotate(Dir['app/models/*.rb'], position: :before, border: true) rescue nil<% end %>
53
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  class Roda
4
4
  module Project
5
- VERSION = "0.1.15"
5
+ VERSION = "0.1.17"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roda-project
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.15
4
+ version: 0.1.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Henrique F. Teixeira