feature_pack 0.7.0 → 0.9.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 +4 -4
- data/README.md +265 -59
- data/lib/feature_pack/controller.rb +33 -17
- data/lib/feature_pack/error.rb +3 -2
- data/lib/feature_pack/feature_pack_routes.rb +13 -2
- data/lib/feature_pack/group_controller.rb +23 -10
- data/lib/feature_pack.rb +218 -83
- data/lib/generators/feature_pack/add_feature/add_feature_generator.rb +69 -22
- data/lib/generators/feature_pack/add_feature/templates/controller.rb.tt +29 -2
- data/lib/generators/feature_pack/add_feature/templates/manifest.yaml.tt +17 -16
- data/lib/generators/feature_pack/add_group/add_group_generator.rb +61 -24
- data/lib/generators/feature_pack/add_group/templates/_group_space/controller.rb.tt +12 -2
- data/lib/generators/feature_pack/add_group/templates/_group_space/manifest.yaml.tt +8 -2
- metadata +13 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 102101eb87f4153fd1db6cdd46f15bf1776912773498a1f585db49e7a6be707d
|
4
|
+
data.tar.gz: a3492ed0b1add63f95a171964c2e688f9d94d351d18112c2961aabcb1c80b845
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d959f7066efab1bb4371914ad61689384d4c6d9a25c4bbc57a0982f14c6cb694c7025aa5e81557790e52327e6112c6ef28cac5e36612abcfaf01f432d5fe01f1
|
7
|
+
data.tar.gz: f6e1c3c5bd1518732d146567446b4a36b4f4812605425c7ee476c0951685ab97f1ed4f4a87d91defbfb0b876b4428620a7fe28d3737a09b26142b2ad72671c9a
|
data/README.md
CHANGED
@@ -1,91 +1,297 @@
|
|
1
1
|
# Feature Pack Gem
|
2
|
-
Organizes and sets up the architecture of micro-applications within a Ruby On Rails application, enabling the segregation of code, management, and isolation of functionalities, which can be developed, tested, and maintained independently of each other.
|
3
2
|
|
4
|
-
|
3
|
+
[](https://badge.fury.io/rb/feature_pack)
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
5
5
|
|
6
|
-
|
7
|
-
A group is a collection of related features. Groups are represented as directories in the `app/feature_packs` folder. Each group contains a `_group_space` directory that holds group-level views and JavaScript files. Group directories follow this naming pattern:
|
6
|
+
Feature Pack organizes and sets up the architecture of micro-applications within a Ruby on Rails application, enabling the segregation of code, management, and isolation of functionalities. Features can be developed, tested, and maintained independently of each other.
|
8
7
|
|
9
|
-
|
10
|
-
Sample: `group_departments-100100_human_resources`
|
8
|
+
## Installation
|
11
9
|
|
12
|
-
|
13
|
-
`departments-100100` between `group_` and class name, exists only for organization purposes. The bounds are `group_` and next underscore `_`
|
14
|
-
`human_resources` class name of the group (required)
|
10
|
+
Add this line to your application's Gemfile:
|
15
11
|
|
16
|
-
|
17
|
-
|
18
|
-
|
12
|
+
```ruby
|
13
|
+
gem 'feature_pack'
|
14
|
+
```
|
19
15
|
|
20
|
-
|
16
|
+
And then execute:
|
21
17
|
|
22
|
-
|
23
|
-
|
18
|
+
```bash
|
19
|
+
bundle install
|
20
|
+
```
|
24
21
|
|
25
|
-
|
22
|
+
## Setup
|
26
23
|
|
24
|
+
Add to your `config/application.rb`:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
# After Bundler.require
|
28
|
+
require 'feature_pack'
|
29
|
+
FeaturePack.setup
|
27
30
|
```
|
28
|
-
_group_space/views/
|
29
|
-
├── index.html.slim # Default view for the group
|
30
|
-
└── partials/
|
31
|
-
└── _header.html.slim # Base header template for the group
|
32
|
-
└── _footer.html.slim # Base footer template for the group
|
33
|
-
```
|
34
|
-
Can have more views and partials, depending on the defined on `controller.rb` but these are the most common ones.
|
35
31
|
|
36
|
-
|
37
|
-
|
38
|
-
|
32
|
+
## Concepts
|
33
|
+
|
34
|
+
### Groups
|
35
|
+
Groups are collections of related features. They provide:
|
36
|
+
- Shared layouts and partials
|
37
|
+
- Common base controller functionality
|
38
|
+
- Namespace organization
|
39
|
+
- Shared JavaScript modules
|
40
|
+
|
41
|
+
### Features
|
42
|
+
Features are individual functionalities within a group. They provide:
|
43
|
+
- Independent controllers and views
|
44
|
+
- Isolated routes
|
45
|
+
- Feature-specific JavaScript
|
46
|
+
- Complete MVC structure
|
47
|
+
|
48
|
+
## Usage
|
39
49
|
|
40
|
-
|
50
|
+
### Creating a New Group
|
51
|
+
|
52
|
+
```bash
|
53
|
+
rails generate feature_pack:add_group human_resources
|
54
|
+
```
|
55
|
+
|
56
|
+
This creates the following structure:
|
41
57
|
```
|
42
|
-
|
58
|
+
app/feature_packs/
|
59
|
+
└── group_YYMMDD_human_resources/
|
60
|
+
└── _group_space/
|
61
|
+
├── controller.rb
|
62
|
+
├── manifest.yaml
|
63
|
+
├── routes.rb
|
64
|
+
├── javascript/
|
65
|
+
└── views/
|
66
|
+
├── index.html.slim
|
67
|
+
└── partials/
|
68
|
+
├── _header.html.slim
|
69
|
+
└── _footer.html.slim
|
43
70
|
```
|
44
71
|
|
45
|
-
### Feature
|
46
|
-
A feature is a single feature that can be added to a group. Feature naming patter is the same of group, but without the `group_` prefix.
|
72
|
+
### Creating a New Feature
|
47
73
|
|
48
|
-
|
49
|
-
|
74
|
+
```bash
|
75
|
+
rails generate feature_pack:add_feature human_resources/employees
|
76
|
+
```
|
50
77
|
|
51
|
-
|
78
|
+
This creates:
|
52
79
|
```
|
53
|
-
|
80
|
+
app/feature_packs/
|
81
|
+
└── group_YYMMDD_human_resources/
|
82
|
+
└── feature_YYMMDD_employees/
|
83
|
+
├── controller.rb
|
84
|
+
├── manifest.yaml
|
85
|
+
├── routes.rb
|
86
|
+
├── doc/
|
87
|
+
│ └── readme.md
|
88
|
+
├── javascript/
|
89
|
+
├── queries/
|
90
|
+
└── views/
|
91
|
+
├── index.html.slim
|
92
|
+
└── partials/
|
93
|
+
├── _header.html.slim
|
94
|
+
└── _footer.html.slim
|
54
95
|
```
|
55
96
|
|
56
|
-
|
97
|
+
## Code Structure
|
98
|
+
|
99
|
+
### Naming Convention
|
100
|
+
|
101
|
+
#### Groups
|
102
|
+
Format: `group_<id>_<name>`
|
103
|
+
- `group_` - Required prefix
|
104
|
+
- `<id>` - Unique identifier (typically YYMMDD format)
|
105
|
+
- `<name>` - Group name in snake_case
|
106
|
+
|
107
|
+
Example: `group_241209_human_resources`
|
108
|
+
|
109
|
+
#### Features
|
110
|
+
Format: `feature_<id>_<name>`
|
111
|
+
- `feature_` - Required prefix
|
112
|
+
- `<id>` - Unique identifier (typically YYMMDD format)
|
113
|
+
- `<name>` - Feature name in snake_case
|
57
114
|
|
115
|
+
Example: `feature_241209_employees`
|
116
|
+
|
117
|
+
### Directory Structure
|
118
|
+
|
119
|
+
#### Group Space (`_group_space`)
|
120
|
+
Contains group-level resources:
|
121
|
+
- `controller.rb` - Base controller for all features in the group
|
122
|
+
- `manifest.yaml` - Group configuration
|
123
|
+
- `routes.rb` - Group-level routes
|
124
|
+
- `views/` - Shared views and layouts
|
125
|
+
- `javascript/` - Shared JavaScript modules
|
126
|
+
|
127
|
+
#### Feature Directory
|
128
|
+
Contains feature-specific resources:
|
129
|
+
- `controller.rb` - Feature controller
|
130
|
+
- `manifest.yaml` - Feature configuration
|
131
|
+
- `routes.rb` - Feature routes
|
132
|
+
- `views/` - Feature views
|
133
|
+
- `javascript/` - Feature-specific JavaScript
|
134
|
+
- `queries/` - Database queries
|
135
|
+
- `doc/` - Feature documentation
|
136
|
+
|
137
|
+
## Controllers
|
138
|
+
|
139
|
+
### Group Controller
|
140
|
+
```ruby
|
141
|
+
class FeaturePack::HumanResourcesController < FeaturePack::GroupController
|
142
|
+
# Group-wide functionality
|
143
|
+
end
|
144
|
+
```
|
145
|
+
|
146
|
+
### Feature Controller
|
58
147
|
```ruby
|
59
|
-
|
60
|
-
def
|
61
|
-
|
148
|
+
class FeaturePack::HumanResources::EmployeesController < FeaturePack::HumanResourcesController
|
149
|
+
def index
|
150
|
+
# Feature-specific logic
|
151
|
+
end
|
152
|
+
end
|
62
153
|
```
|
63
154
|
|
155
|
+
## Routes
|
156
|
+
|
157
|
+
Routes are automatically configured based on manifest files:
|
158
|
+
|
159
|
+
```yaml
|
160
|
+
# Group manifest.yaml
|
161
|
+
url: /hr
|
162
|
+
name: Human Resources
|
163
|
+
|
164
|
+
# Feature manifest.yaml
|
165
|
+
url: /employees
|
166
|
+
name: Employees Management
|
167
|
+
```
|
168
|
+
|
169
|
+
This generates routes like:
|
170
|
+
- `/hr` - Group index
|
171
|
+
- `/hr/employees` - Feature index
|
172
|
+
|
64
173
|
## Helpers
|
65
174
|
|
66
|
-
###
|
175
|
+
### Path Helpers
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
# Group path
|
179
|
+
feature_pack_group_path(:human_resources)
|
180
|
+
# => "/hr"
|
181
|
+
|
182
|
+
# Feature path
|
183
|
+
feature_pack_path(:human_resources, :employees)
|
184
|
+
# => "/hr/employees"
|
185
|
+
|
186
|
+
# With parameters
|
187
|
+
feature_pack_path(:human_resources, :employees, id: 1)
|
188
|
+
# => "/hr/employees?id=1"
|
189
|
+
```
|
190
|
+
|
191
|
+
### Controller Variables
|
192
|
+
|
193
|
+
Available in controllers and views:
|
194
|
+
- `@group` - Current group object
|
195
|
+
- `@feature` - Current feature object (not available in group controller)
|
196
|
+
|
197
|
+
## View Hierarchy
|
198
|
+
|
199
|
+
Views are resolved in the following order:
|
200
|
+
1. Feature-specific views
|
201
|
+
2. Group-shared views
|
202
|
+
3. Application default views
|
203
|
+
|
204
|
+
### Partials
|
205
|
+
Header and footer partials follow a fallback pattern:
|
206
|
+
1. Feature's `views/partials/_header.html.slim`
|
207
|
+
2. Group's `views/partials/_header.html.slim`
|
208
|
+
3. Application's default header
|
209
|
+
|
210
|
+
## JavaScript Integration
|
211
|
+
|
212
|
+
JavaScript files are automatically discovered and can be referenced:
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
# In views
|
216
|
+
javascript_include_tag @group.javascript_module('shared')
|
217
|
+
javascript_include_tag @feature.javascript_module('employees')
|
218
|
+
```
|
219
|
+
|
220
|
+
## Advanced Configuration
|
221
|
+
|
222
|
+
### Manifest Files
|
223
|
+
|
224
|
+
Group manifest (`_group_space/manifest.yaml`):
|
225
|
+
```yaml
|
226
|
+
url: /hr
|
227
|
+
name: Human Resources
|
228
|
+
const_aliases:
|
229
|
+
- employee_model: Employee
|
230
|
+
- department_model: Department
|
231
|
+
```
|
232
|
+
|
233
|
+
Feature manifest:
|
234
|
+
```yaml
|
235
|
+
url: /employees
|
236
|
+
name: Employees Management
|
237
|
+
const_aliases:
|
238
|
+
- service: EmployeeService
|
239
|
+
```
|
240
|
+
|
241
|
+
### Const Aliases
|
242
|
+
|
243
|
+
Access aliased constants:
|
244
|
+
```ruby
|
245
|
+
# In controllers
|
246
|
+
@group.employee_model # => Employee
|
247
|
+
@feature.service # => FeaturePack::HumanResources::Employees::EmployeeService
|
248
|
+
```
|
249
|
+
|
250
|
+
## Best Practices
|
251
|
+
|
252
|
+
1. **Group Organization**: Group related features that share common functionality
|
253
|
+
2. **Naming**: Use descriptive snake_case names for groups and features
|
254
|
+
3. **Isolation**: Keep features independent and loosely coupled
|
255
|
+
4. **Shared Resources**: Place common code in group space
|
256
|
+
5. **Documentation**: Document each feature in its `doc/readme.md`
|
257
|
+
|
258
|
+
## Troubleshooting
|
259
|
+
|
260
|
+
### Common Issues
|
261
|
+
|
262
|
+
1. **Group/Feature Not Found**
|
263
|
+
- Ensure proper naming convention
|
264
|
+
- Run `FeaturePack.setup` after adding new groups/features
|
265
|
+
- Check manifest files exist
|
266
|
+
|
267
|
+
2. **Routes Not Working**
|
268
|
+
- Verify manifest.yaml has correct URL configuration
|
269
|
+
- Check routes.rb files exist
|
270
|
+
- Restart Rails server after changes
|
271
|
+
|
272
|
+
3. **Views Not Rendering**
|
273
|
+
- Check view file extensions (.html.slim, .html.erb, etc.)
|
274
|
+
- Verify view paths in controller
|
275
|
+
- Check for typos in view names
|
276
|
+
|
277
|
+
## Contributing
|
278
|
+
|
279
|
+
1. Fork it
|
280
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
281
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
282
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
283
|
+
5. Create new Pull Request
|
284
|
+
|
285
|
+
## License
|
67
286
|
|
68
|
-
The
|
287
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
69
288
|
|
70
|
-
|
71
|
-
|
72
|
-
**Usage example:**
|
73
|
-
```ruby
|
74
|
-
# Assuming `group` is a valid group name in symbol
|
75
|
-
group_url = feature_pack_group_path(:group_name)
|
76
|
-
```
|
289
|
+
## Author
|
77
290
|
|
78
|
-
|
79
|
-
|
80
|
-
**Usage example:**
|
81
|
-
```ruby
|
82
|
-
# Assuming `group` and `feature` are valid objects
|
83
|
-
feature_url = feature_pack_path(:my_group, :my_feature)
|
84
|
-
```
|
291
|
+
Gedean Dias - gedean.dias@gmail.com
|
85
292
|
|
86
|
-
|
293
|
+
## Links
|
87
294
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
@group variable is available in the group controller and in the its features controllers.
|
295
|
+
- [GitHub Repository](https://github.com/gedean/feature_pack)
|
296
|
+
- [RubyGems](https://rubygems.org/gems/feature_pack)
|
297
|
+
- [Bug Reports](https://github.com/gedean/feature_pack/issues)
|
@@ -1,42 +1,58 @@
|
|
1
|
+
# Base controller for all feature controllers
|
2
|
+
# Handles the setup of features, views, and layouts
|
1
3
|
class FeaturePack::Controller < ApplicationController
|
2
|
-
|
3
|
-
before_action :set_view_lookup_context_prefix
|
4
|
-
before_action :set_layout_paths
|
4
|
+
prepend_before_action :setup_feature
|
5
5
|
|
6
|
+
# Default index action
|
6
7
|
def index; end
|
7
8
|
|
8
9
|
private
|
9
10
|
|
11
|
+
# Main setup method that configures the feature environment
|
12
|
+
def setup_feature
|
13
|
+
set_group_and_feature
|
14
|
+
set_view_lookup_context_prefix
|
15
|
+
set_layout_paths
|
16
|
+
end
|
17
|
+
|
18
|
+
# Extracts and sets the group and feature from the controller path
|
10
19
|
def set_group_and_feature
|
11
|
-
group_name, feature_name = params['controller']
|
12
|
-
|
13
|
-
|
20
|
+
group_name, feature_name = params['controller']
|
21
|
+
.delete_prefix('feature_pack/')
|
22
|
+
.split('/')
|
23
|
+
.map(&:to_sym)
|
24
|
+
|
25
|
+
@group = FeaturePack.group(group_name)
|
26
|
+
@feature = FeaturePack.feature(group_name, feature_name)
|
27
|
+
|
28
|
+
raise FeaturePack::Error::NoGroup, "Group '#{group_name}' not found" if @group.nil?
|
29
|
+
raise FeaturePack::Error::NoDataError, "Feature '#{feature_name}' not found in group '#{group_name}'" if @feature.nil?
|
14
30
|
end
|
15
31
|
|
32
|
+
# Configures the view lookup path to include feature-specific views
|
16
33
|
def set_view_lookup_context_prefix
|
17
|
-
|
18
|
-
|
19
|
-
|
34
|
+
return if lookup_context.prefixes.include?(@feature.views_relative_path)
|
35
|
+
|
36
|
+
lookup_context.prefixes.prepend(@feature.views_relative_path)
|
20
37
|
end
|
21
38
|
|
39
|
+
# Sets up header and footer layout paths with fallback logic
|
40
|
+
# Search order:
|
41
|
+
# 1. Feature-specific partials
|
42
|
+
# 2. Group-level partials (fallback)
|
43
|
+
# 3. Application default (if neither exists)
|
22
44
|
def set_layout_paths
|
23
|
-
=begin
|
24
|
-
Header/Footer Lookup order
|
25
|
-
|
26
|
-
- Feature dir/_partials, if not exists
|
27
|
-
- Fallback to Group, if not exists
|
28
|
-
- Fallback to Application's default header/footer
|
29
|
-
=end
|
30
|
-
|
31
45
|
feature_partials_path = @feature.views_relative_path.join('partials')
|
32
46
|
group_partials_path = @feature.group.views_path.concat('/partials')
|
33
47
|
|
48
|
+
# Set header layout
|
34
49
|
if template_exists?('header', feature_partials_path, true)
|
35
50
|
@header_layout_path = @feature.view('partials/header')
|
36
51
|
elsif template_exists?('header', group_partials_path, true)
|
37
52
|
@header_layout_path = @feature.group.view('partials/header')
|
38
53
|
end
|
39
54
|
|
55
|
+
# Set footer layout
|
40
56
|
if template_exists?('footer', feature_partials_path, true)
|
41
57
|
@footer_layout_path = @feature.view('partials/footer')
|
42
58
|
elsif template_exists?('footer', group_partials_path, true)
|
data/lib/feature_pack/error.rb
CHANGED
@@ -1,14 +1,25 @@
|
|
1
|
+
# Routes configuration for FeaturePack
|
2
|
+
# This file is loaded by Rails routes to set up all group and feature routes
|
3
|
+
|
1
4
|
FeaturePack.groups.each do |group|
|
5
|
+
# Configure group-level routes
|
2
6
|
scope group.manifest[:url], as: group.name do
|
3
|
-
|
7
|
+
if group.routes_file.nil?
|
8
|
+
raise FeaturePack::Error::NoDataError,
|
9
|
+
"Group '#{group.name}' routes file not found in #{group.metadata_path}"
|
10
|
+
end
|
4
11
|
|
5
12
|
draw(group.routes_file)
|
6
13
|
end
|
7
14
|
|
15
|
+
# Configure feature-level routes within the group namespace
|
8
16
|
namespace group.name, path: group.manifest[:url] do
|
9
17
|
group.features.each do |feature|
|
10
18
|
scope feature.manifest[:url], as: feature.name do
|
11
|
-
|
19
|
+
if feature.routes_file.nil?
|
20
|
+
raise FeaturePack::Error::NoDataError,
|
21
|
+
"Feature '#{feature.name}' routes file not found in #{feature.routes_file_path}"
|
22
|
+
end
|
12
23
|
|
13
24
|
draw(feature.routes_file)
|
14
25
|
end
|
@@ -1,31 +1,44 @@
|
|
1
|
+
# Base controller for all group controllers
|
2
|
+
# Handles the setup of groups and their views
|
1
3
|
class FeaturePack::GroupController < ApplicationController
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
4
|
+
prepend_before_action :setup_group
|
5
|
+
|
6
|
+
# Default index action
|
6
7
|
def index; end
|
7
8
|
|
8
9
|
private
|
9
10
|
|
11
|
+
# Main setup method that configures the group environment
|
12
|
+
def setup_group
|
13
|
+
set_group
|
14
|
+
set_view_lookup_context_prefix
|
15
|
+
set_layout_paths
|
16
|
+
end
|
17
|
+
|
18
|
+
# Extracts and sets the group from the controller path
|
10
19
|
def set_group
|
11
20
|
group_name = params[:controller].split('/')[1].to_sym
|
12
21
|
@group = FeaturePack.group(group_name)
|
22
|
+
|
23
|
+
raise FeaturePack::Error::NoGroup, "Group '#{group_name}' not found" if @group.nil?
|
13
24
|
end
|
14
25
|
|
26
|
+
# Configures the view lookup path to include group-specific views
|
15
27
|
def set_view_lookup_context_prefix
|
16
|
-
|
17
|
-
|
18
|
-
|
28
|
+
return if lookup_context.prefixes.include?(@group.views_path)
|
29
|
+
|
30
|
+
lookup_context.prefixes.prepend(@group.views_path)
|
19
31
|
end
|
20
32
|
|
33
|
+
# Sets up header and footer layout paths for the group
|
21
34
|
def set_layout_paths
|
22
|
-
|
35
|
+
partials_path = @group.views_path.concat('/partials')
|
23
36
|
|
24
|
-
if template_exists?('header',
|
37
|
+
if template_exists?('header', partials_path, true)
|
25
38
|
@header_layout_path = @group.view('partials/header')
|
26
39
|
end
|
27
40
|
|
28
|
-
if template_exists?('footer',
|
41
|
+
if template_exists?('footer', partials_path, true)
|
29
42
|
@footer_layout_path = @group.view('partials/footer')
|
30
43
|
end
|
31
44
|
end
|
data/lib/feature_pack.rb
CHANGED
@@ -1,12 +1,18 @@
|
|
1
1
|
require 'active_support/all'
|
2
2
|
|
3
|
+
# FeaturePack module provides a way to organize Rails applications into
|
4
|
+
# groups and features, enabling better code organization and isolation
|
3
5
|
module FeaturePack
|
6
|
+
# Pattern constants for identifying groups and features
|
4
7
|
GROUP_ID_PATTERN = /^group_.*?_/.freeze
|
5
8
|
FEATURE_ID_PATTERN = /^feature_.*?_/.freeze
|
9
|
+
|
10
|
+
# Directory and file name constants
|
6
11
|
GROUP_SPACE_DIRECTORY = '_group_space'.freeze
|
7
12
|
MANIFEST_FILE_NAME = 'manifest.yaml'.freeze
|
8
13
|
CONTROLLER_FILE_NAME = 'controller.rb'.freeze
|
9
14
|
|
15
|
+
# Attribute readers that will be dynamically defined
|
10
16
|
ATTR_READERS = %i[
|
11
17
|
path
|
12
18
|
features_path
|
@@ -17,41 +23,113 @@ module FeaturePack
|
|
17
23
|
javascript_files_paths
|
18
24
|
].freeze
|
19
25
|
|
20
|
-
|
21
|
-
|
26
|
+
class << self
|
27
|
+
# Sets up the FeaturePack system
|
28
|
+
# This method should be called once during Rails initialization
|
29
|
+
def setup
|
30
|
+
raise 'FeaturePack already setup!' if defined?(@@setup_executed_flag)
|
22
31
|
|
23
|
-
|
24
|
-
|
32
|
+
initialize_paths
|
33
|
+
load_dependencies
|
34
|
+
discover_groups
|
35
|
+
discover_features
|
36
|
+
finalize_setup
|
37
|
+
end
|
25
38
|
|
26
|
-
|
27
|
-
|
28
|
-
|
39
|
+
# Finds a group by name
|
40
|
+
# @param group_name [Symbol] The name of the group
|
41
|
+
# @return [OpenStruct, nil] The group object or nil if not found
|
42
|
+
def group(group_name)
|
43
|
+
@@groups.find { |g| g.name.eql?(group_name) }
|
44
|
+
end
|
29
45
|
|
30
|
-
|
31
|
-
|
46
|
+
# Finds a feature within a group
|
47
|
+
# @param group_name [Symbol] The name of the group
|
48
|
+
# @param feature_name [Symbol] The name of the feature
|
49
|
+
# @return [OpenStruct, nil] The feature object or nil if not found
|
50
|
+
def feature(group_name, feature_name)
|
51
|
+
requested_group = group(group_name)
|
52
|
+
return nil if requested_group.nil?
|
53
|
+
|
54
|
+
requested_group.feature(feature_name)
|
55
|
+
end
|
32
56
|
|
33
|
-
|
57
|
+
private
|
34
58
|
|
35
|
-
|
36
|
-
|
59
|
+
def initialize_paths
|
60
|
+
@@path = Pathname.new(__dir__)
|
61
|
+
@@features_path = Pathname.new(Rails.root.join('app/feature_packs'))
|
62
|
+
|
63
|
+
validate_features_path!
|
64
|
+
|
65
|
+
@@groups_controllers_paths = []
|
66
|
+
@@features_controllers_paths = []
|
67
|
+
@@ignored_paths = Dir.glob("#{@@features_path}/[!]*/")
|
68
|
+
@@javascript_files_paths = discover_javascript_files
|
69
|
+
end
|
37
70
|
|
38
|
-
|
71
|
+
def load_dependencies
|
72
|
+
load @@path.join('feature_pack/error.rb')
|
73
|
+
end
|
39
74
|
|
40
|
-
|
75
|
+
def validate_features_path!
|
76
|
+
raise "Invalid features_path: '#{@@features_path}'" if @@features_path.nil?
|
77
|
+
raise "Features path does not exist: '#{@@features_path}'" unless Dir.exist?(@@features_path)
|
78
|
+
end
|
41
79
|
|
42
|
-
|
80
|
+
def discover_javascript_files
|
81
|
+
Dir.glob("#{@@features_path}/[!_]*/**/*.js")
|
82
|
+
.map { |js_path| js_path.sub(/^#{Regexp.escape(@@features_path.to_s)}\//, '') }
|
83
|
+
.to_a
|
84
|
+
end
|
43
85
|
|
44
|
-
|
86
|
+
def finalize_setup
|
87
|
+
ATTR_READERS.each { |attr| define_singleton_method(attr) { class_variable_get("@@#{attr}") } }
|
88
|
+
@@ignored_paths << @@path.join('feature_pack/feature_pack_routes.rb')
|
89
|
+
@@setup_executed_flag = true
|
90
|
+
end
|
91
|
+
|
92
|
+
def discover_groups
|
93
|
+
@@groups = Dir.glob("#{@@features_path}/[!_]*/").map do |group_path|
|
94
|
+
build_group(group_path)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def build_group(group_path)
|
45
99
|
relative_path = Pathname.new(group_path)
|
46
100
|
base_path = File.basename(group_path, File::SEPARATOR)
|
101
|
+
|
102
|
+
validate_group_id!(base_path)
|
103
|
+
|
104
|
+
routes_file = find_group_routes_file(group_path, base_path)
|
105
|
+
@@groups_controllers_paths << File.join(group_path, GROUP_SPACE_DIRECTORY, CONTROLLER_FILE_NAME)
|
106
|
+
|
107
|
+
group = create_group_struct(base_path, group_path, relative_path, routes_file)
|
108
|
+
setup_group_aliases(group)
|
109
|
+
define_group_methods(group)
|
110
|
+
|
111
|
+
group
|
112
|
+
end
|
47
113
|
|
48
|
-
|
49
|
-
|
114
|
+
def validate_group_id!(base_path)
|
115
|
+
if base_path.scan(GROUP_ID_PATTERN).empty?
|
116
|
+
raise "Group '#{base_path}' does not have a valid ID. Expected format: group_<id>_<name>"
|
117
|
+
end
|
118
|
+
end
|
50
119
|
|
51
|
-
|
120
|
+
def find_group_routes_file(group_path, base_path)
|
121
|
+
routes_path = File.join(group_path, GROUP_SPACE_DIRECTORY, 'routes.rb')
|
122
|
+
File.exist?(routes_path) ? File.join(base_path, GROUP_SPACE_DIRECTORY, 'routes') : nil
|
123
|
+
end
|
52
124
|
|
53
|
-
|
54
|
-
|
125
|
+
def create_group_struct(base_path, group_path, relative_path, routes_file)
|
126
|
+
manifest_path = File.join(group_path, GROUP_SPACE_DIRECTORY, MANIFEST_FILE_NAME)
|
127
|
+
|
128
|
+
unless File.exist?(manifest_path)
|
129
|
+
raise "Manifest file not found for group '#{base_path}' at #{manifest_path}"
|
130
|
+
end
|
131
|
+
|
132
|
+
OpenStruct.new(
|
55
133
|
id: base_path.scan(GROUP_ID_PATTERN).first.delete_suffix('_'),
|
56
134
|
name: base_path.gsub(GROUP_ID_PATTERN, '').to_sym,
|
57
135
|
metadata_path: @@features_path.join(group_path, GROUP_SPACE_DIRECTORY),
|
@@ -59,82 +137,139 @@ module FeaturePack
|
|
59
137
|
base_dir: File.basename(relative_path, File::SEPARATOR),
|
60
138
|
routes_file: routes_file,
|
61
139
|
features: [],
|
62
|
-
manifest:
|
140
|
+
manifest: load_manifest(manifest_path)
|
63
141
|
)
|
142
|
+
end
|
143
|
+
|
144
|
+
def load_manifest(manifest_path)
|
145
|
+
YAML.load_file(manifest_path).deep_symbolize_keys
|
146
|
+
rescue => e
|
147
|
+
raise "Failed to load manifest at #{manifest_path}: #{e.message}"
|
148
|
+
end
|
64
149
|
|
150
|
+
def setup_group_aliases(group)
|
65
151
|
group.manifest.fetch(:const_aliases, []).each do |alias_data|
|
66
152
|
alias_method_name, alias_const_name = alias_data.first
|
67
|
-
group.define_singleton_method(alias_method_name)
|
153
|
+
group.define_singleton_method(alias_method_name) do
|
154
|
+
"FeaturePack::#{group.name.to_s.camelize}::#{alias_const_name}".constantize
|
155
|
+
end
|
68
156
|
end
|
157
|
+
end
|
69
158
|
|
70
|
-
|
71
|
-
def group.
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
group
|
159
|
+
def define_group_methods(group)
|
160
|
+
def group.feature(feature_name)
|
161
|
+
features.find { |p| p.name.eql?(feature_name) }
|
162
|
+
end
|
163
|
+
|
164
|
+
def group.views_path
|
165
|
+
"#{base_dir}/#{GROUP_SPACE_DIRECTORY}/views"
|
166
|
+
end
|
167
|
+
|
168
|
+
def group.view(view_name)
|
169
|
+
"#{base_dir}/#{GROUP_SPACE_DIRECTORY}/views/#{view_name}"
|
170
|
+
end
|
171
|
+
|
172
|
+
def group.javascript_module(javascript_file_name)
|
173
|
+
"#{base_dir}/#{GROUP_SPACE_DIRECTORY}/javascript/#{javascript_file_name}"
|
174
|
+
end
|
76
175
|
end
|
77
176
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
base_path = File.basename(feature_path, File::SEPARATOR)
|
83
|
-
|
84
|
-
feature_name = base_path.gsub(FEATURE_ID_PATTERN, '').to_sym
|
85
|
-
|
86
|
-
routes_file_path = relative_path.join('routes.rb')
|
87
|
-
|
88
|
-
# The custom routes file loads before the Rails default routes,
|
89
|
-
# leading to errors like NoMethodError for 'scope'.
|
90
|
-
# Ignoring them is required to prevent these issues.
|
91
|
-
@@ignored_paths << routes_file_path
|
92
|
-
|
93
|
-
# Due to Zeiwerk rules, Controllers have special load process
|
94
|
-
@@features_controllers_paths << relative_path.join(CONTROLLER_FILE_NAME)
|
95
|
-
|
96
|
-
@@ignored_paths << relative_path.join(CONTROLLER_FILE_NAME)
|
97
|
-
|
98
|
-
raise "Resource '#{relative_path}' does not have a valid ID" if base_path.scan(FEATURE_ID_PATTERN).empty?
|
99
|
-
feature_sub_path = relative_path.sub(/^#{Regexp.escape(@@features_path.to_s)}\//, '')
|
100
|
-
feature = OpenStruct.new(
|
101
|
-
id: base_path.scan(FEATURE_ID_PATTERN).first.delete_suffix('_'),
|
102
|
-
name: feature_name,
|
103
|
-
group: group,
|
104
|
-
absolute_path: absolute_path,
|
105
|
-
relative_path: relative_path,
|
106
|
-
sub_path: feature_sub_path,
|
107
|
-
routes_file_path: routes_file_path,
|
108
|
-
routes_file: feature_sub_path.join('routes'),
|
109
|
-
# controller_path: relative_path.join('controller'),
|
110
|
-
views_absolute_path: absolute_path.join('views'),
|
111
|
-
views_relative_path: relative_path.sub(/^#{Regexp.escape(@@features_path.to_s)}\//, '').join('views'),
|
112
|
-
javascript_relative_path: relative_path.sub(/^#{Regexp.escape(@@features_path.to_s)}\//, '').join('javascript'),
|
113
|
-
manifest: YAML.load_file(File.join(feature_path, MANIFEST_FILE_NAME)).deep_symbolize_keys
|
114
|
-
)
|
115
|
-
|
116
|
-
def feature.class_name = "FeaturePack::#{group.name.name.camelize}::#{name.name.camelize}"
|
117
|
-
def feature.namespace = class_name.constantize
|
118
|
-
|
119
|
-
feature.manifest.fetch(:const_aliases, []).each do |alias_data|
|
120
|
-
alias_method_name, alias_const_name = alias_data.first
|
121
|
-
feature.define_singleton_method(alias_method_name) { "#{class_name}::#{alias_const_name}".constantize }
|
177
|
+
def discover_features
|
178
|
+
@@groups.each do |group|
|
179
|
+
Dir.glob("#{group.relative_path}[!_]*/").each do |feature_path|
|
180
|
+
build_feature(group, feature_path)
|
122
181
|
end
|
182
|
+
end
|
183
|
+
end
|
123
184
|
|
124
|
-
|
125
|
-
|
185
|
+
def build_feature(group, feature_path)
|
186
|
+
absolute_path = @@features_path.join(feature_path)
|
187
|
+
relative_path = Pathname.new(feature_path)
|
188
|
+
base_path = File.basename(feature_path, File::SEPARATOR)
|
189
|
+
|
190
|
+
validate_feature_id!(base_path, relative_path)
|
191
|
+
|
192
|
+
feature_name = base_path.gsub(FEATURE_ID_PATTERN, '').to_sym
|
193
|
+
routes_file_path = relative_path.join('routes.rb')
|
194
|
+
|
195
|
+
setup_feature_paths(relative_path, routes_file_path)
|
196
|
+
|
197
|
+
feature = create_feature_struct(
|
198
|
+
base_path, feature_name, group, absolute_path,
|
199
|
+
relative_path, routes_file_path, feature_path
|
200
|
+
)
|
201
|
+
|
202
|
+
define_feature_methods(feature)
|
203
|
+
setup_feature_aliases(feature)
|
204
|
+
|
205
|
+
group.features << feature
|
206
|
+
end
|
126
207
|
|
127
|
-
|
208
|
+
def validate_feature_id!(base_path, relative_path)
|
209
|
+
if base_path.scan(FEATURE_ID_PATTERN).empty?
|
210
|
+
raise "Feature '#{relative_path}' does not have a valid ID. Expected format: feature_<id>_<name>"
|
128
211
|
end
|
129
212
|
end
|
130
213
|
|
131
|
-
|
132
|
-
|
214
|
+
def setup_feature_paths(relative_path, routes_file_path)
|
215
|
+
# Custom routes file loads before Rails default routes
|
216
|
+
@@ignored_paths << routes_file_path
|
217
|
+
|
218
|
+
# Controllers have special load process due to Zeitwerk
|
219
|
+
controller_path = relative_path.join(CONTROLLER_FILE_NAME)
|
220
|
+
@@features_controllers_paths << controller_path
|
221
|
+
@@ignored_paths << controller_path
|
222
|
+
end
|
223
|
+
|
224
|
+
def create_feature_struct(base_path, feature_name, group, absolute_path, relative_path, routes_file_path, feature_path)
|
225
|
+
feature_sub_path = relative_path.sub(/^#{Regexp.escape(@@features_path.to_s)}\//, '')
|
226
|
+
manifest_path = File.join(feature_path, MANIFEST_FILE_NAME)
|
227
|
+
|
228
|
+
unless File.exist?(manifest_path)
|
229
|
+
raise "Manifest file not found for feature '#{feature_name}' at #{manifest_path}"
|
230
|
+
end
|
231
|
+
|
232
|
+
OpenStruct.new(
|
233
|
+
id: base_path.scan(FEATURE_ID_PATTERN).first.delete_suffix('_'),
|
234
|
+
name: feature_name,
|
235
|
+
group: group,
|
236
|
+
absolute_path: absolute_path,
|
237
|
+
relative_path: relative_path,
|
238
|
+
sub_path: feature_sub_path,
|
239
|
+
routes_file_path: routes_file_path,
|
240
|
+
routes_file: feature_sub_path.join('routes'),
|
241
|
+
views_absolute_path: absolute_path.join('views'),
|
242
|
+
views_relative_path: relative_path.sub(/^#{Regexp.escape(@@features_path.to_s)}\//, '').join('views'),
|
243
|
+
javascript_relative_path: relative_path.sub(/^#{Regexp.escape(@@features_path.to_s)}\//, '').join('javascript'),
|
244
|
+
manifest: load_manifest(manifest_path)
|
245
|
+
)
|
246
|
+
end
|
133
247
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
248
|
+
def define_feature_methods(feature)
|
249
|
+
def feature.class_name
|
250
|
+
"FeaturePack::#{group.name.to_s.camelize}::#{name.to_s.camelize}"
|
251
|
+
end
|
252
|
+
|
253
|
+
def feature.namespace
|
254
|
+
class_name.constantize
|
255
|
+
end
|
256
|
+
|
257
|
+
def feature.view(view_name)
|
258
|
+
"#{views_relative_path}/#{view_name}"
|
259
|
+
end
|
260
|
+
|
261
|
+
def feature.javascript_module(javascript_file_name)
|
262
|
+
"#{javascript_relative_path}/#{javascript_file_name}"
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def setup_feature_aliases(feature)
|
267
|
+
feature.manifest.fetch(:const_aliases, []).each do |alias_data|
|
268
|
+
alias_method_name, alias_const_name = alias_data.first
|
269
|
+
feature.define_singleton_method(alias_method_name) do
|
270
|
+
"#{class_name}::#{alias_const_name}".constantize
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
139
274
|
end
|
140
275
|
end
|
@@ -1,34 +1,81 @@
|
|
1
1
|
require 'rails/generators/base'
|
2
2
|
require 'rails/generators/named_base'
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
4
|
+
module FeaturePack
|
5
|
+
# Generator for creating new features within a group
|
6
|
+
# Usage: rails generate feature_pack:add_feature GROUP_NAME/FEATURE_NAME
|
7
|
+
class AddFeatureGenerator < Rails::Generators::NamedBase
|
8
|
+
desc 'Creates a new Feature within an existing Group'
|
9
|
+
source_root File.expand_path('templates', __dir__)
|
7
10
|
|
8
|
-
|
11
|
+
argument :name, type: :string, required: true, desc: 'The group/feature name (snake_case) format: group_name/feature_name'
|
9
12
|
|
10
|
-
|
11
|
-
|
13
|
+
def add_feature
|
14
|
+
validate_feature_name!
|
15
|
+
parse_names
|
16
|
+
check_group_existence!
|
17
|
+
check_feature_existence!
|
18
|
+
|
19
|
+
@feature_id = generate_feature_id
|
20
|
+
@feature_dir = @group.relative_path.join("feature_#{@feature_id}_#{@feature_name}")
|
21
|
+
|
22
|
+
create_feature_files
|
23
|
+
|
24
|
+
say "Feature '#{@feature_name}' created successfully in group '#{@group_name}'!", :green
|
25
|
+
say "Location: #{@feature_dir}", :green
|
26
|
+
end
|
12
27
|
|
13
|
-
|
14
|
-
@group = FeaturePack.group(@group_name.to_sym)
|
15
|
-
@group_class_name = @group_name.camelcase
|
16
|
-
@feature_class_name = @feature_name.camelcase
|
28
|
+
private
|
17
29
|
|
18
|
-
|
30
|
+
def validate_feature_name!
|
31
|
+
if name.count('/') != 1
|
32
|
+
raise Thor::Error, "Feature name must be in format: group_name/feature_name"
|
33
|
+
end
|
34
|
+
end
|
19
35
|
|
20
|
-
|
36
|
+
def parse_names
|
37
|
+
@group_name, @feature_name = name.split('/')
|
38
|
+
|
39
|
+
unless @group_name.match?(/^[a-z_]+$/) && @feature_name.match?(/^[a-z_]+$/)
|
40
|
+
raise Thor::Error, "Group and feature names must be in snake_case format"
|
41
|
+
end
|
42
|
+
|
43
|
+
@group_class_name = @group_name.camelcase
|
44
|
+
@feature_class_name = @feature_name.camelcase
|
45
|
+
end
|
21
46
|
|
22
|
-
|
23
|
-
|
47
|
+
def check_group_existence!
|
48
|
+
@group = FeaturePack.group(@group_name.to_sym)
|
49
|
+
|
50
|
+
if @group.nil?
|
51
|
+
raise Thor::Error, "Group '#{@group_name}' doesn't exist. Create it first with: rails generate feature_pack:add_group #{@group_name}"
|
52
|
+
end
|
53
|
+
end
|
24
54
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
55
|
+
def check_feature_existence!
|
56
|
+
if FeaturePack.feature(@group_name.to_sym, @feature_name.to_sym).present?
|
57
|
+
raise Thor::Error, "Feature '#{@feature_name}' already exists in group '#{@group_name}'"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def generate_feature_id
|
62
|
+
# Generate a unique ID based on timestamp
|
63
|
+
# Format: YYMMDD (can't contain underscores)
|
64
|
+
Time.now.strftime('%y%m%d')
|
65
|
+
end
|
66
|
+
|
67
|
+
def create_feature_files
|
68
|
+
template './controller.rb.tt', @feature_dir.join('controller.rb')
|
69
|
+
template './manifest.yaml.tt', @feature_dir.join('manifest.yaml')
|
70
|
+
template './routes.rb.tt', @feature_dir.join('routes.rb')
|
71
|
+
template './views/index.html.slim.tt', @feature_dir.join('views/index.html.slim')
|
72
|
+
template './views/partials/_header.html.slim.tt', @feature_dir.join('views/partials/_header.html.slim')
|
73
|
+
template './views/partials/_footer.html.slim.tt', @feature_dir.join('views/partials/_footer.html.slim')
|
74
|
+
template './doc/readme.md.tt', @feature_dir.join('doc/readme.md')
|
75
|
+
|
76
|
+
# Create directories
|
77
|
+
empty_directory @feature_dir.join('queries')
|
78
|
+
empty_directory @feature_dir.join('javascript')
|
79
|
+
end
|
33
80
|
end
|
34
81
|
end
|
@@ -1,3 +1,30 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# Feature controller for <%= @feature_class_name %>
|
2
|
+
class FeaturePack::<%= @group_class_name %>::<%= @feature_class_name %>Controller < FeaturePack::<%= @group_class_name %>Controller
|
3
|
+
# The 'index' action is already defined in the parent controller
|
4
|
+
# Add your feature-specific actions and logic here
|
5
|
+
|
6
|
+
# Example actions:
|
7
|
+
# def show
|
8
|
+
# @item = find_item(params[:id])
|
9
|
+
# end
|
10
|
+
|
11
|
+
# def new
|
12
|
+
# @item = Item.new
|
13
|
+
# end
|
14
|
+
|
15
|
+
# def create
|
16
|
+
# @item = Item.new(item_params)
|
17
|
+
# if @item.save
|
18
|
+
# redirect_to feature_pack_path(:<%= @group_name %>, :<%= @feature_name %>)
|
19
|
+
# else
|
20
|
+
# render :new
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Add feature-specific private methods here
|
27
|
+
# def item_params
|
28
|
+
# params.require(:item).permit(:name, :description)
|
29
|
+
# end
|
3
30
|
end
|
@@ -1,16 +1,17 @@
|
|
1
|
-
|
2
|
-
:
|
3
|
-
:
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
#
|
8
|
-
:
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
1
|
+
# Feature manifest configuration
|
2
|
+
name: <%= @feature_class_name.humanize %>
|
3
|
+
description: Description of <%= @feature_name.humanize %> feature
|
4
|
+
url: /<%= @feature_name.gsub('_', '-') %>
|
5
|
+
version: 1.0.0
|
6
|
+
|
7
|
+
# Optional: Define constant aliases for this feature
|
8
|
+
# const_aliases:
|
9
|
+
# - service: <%= @feature_class_name %>Service
|
10
|
+
# - repository: <%= @feature_class_name %>Repository
|
11
|
+
|
12
|
+
# Optional: Define access policies
|
13
|
+
# access_policy:
|
14
|
+
# admin: '*'
|
15
|
+
# user:
|
16
|
+
# - index
|
17
|
+
# - show
|
@@ -1,29 +1,66 @@
|
|
1
1
|
require 'rails/generators/base'
|
2
2
|
require 'rails/generators/named_base'
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
4
|
+
module FeaturePack
|
5
|
+
# Generator for creating new feature groups
|
6
|
+
# Usage: rails generate feature_pack:add_group GROUP_NAME
|
7
|
+
class AddGroupGenerator < Rails::Generators::NamedBase
|
8
|
+
desc 'Creates a new Feature Group with the standard directory structure'
|
9
|
+
source_root File.expand_path('templates', __dir__)
|
10
|
+
|
11
|
+
argument :name, type: :string, required: true, desc: 'The name (snake_case) of the group to add'
|
12
|
+
|
13
|
+
def create_feature_group
|
14
|
+
validate_group_name!
|
15
|
+
check_group_existence!
|
16
|
+
|
17
|
+
@class_name = name.camelcase
|
18
|
+
@group_id = generate_group_id
|
19
|
+
|
20
|
+
group_dir = FeaturePack.features_path.join("group_#{@group_id}_#{name}")
|
21
|
+
|
22
|
+
create_group_files(group_dir)
|
23
|
+
|
24
|
+
say "Group '#{name}' created successfully!", :green
|
25
|
+
say "Location: #{group_dir}", :green
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def validate_group_name!
|
31
|
+
if name.include?('/')
|
32
|
+
raise Thor::Error, "Group name cannot contain '/'. Use snake_case format."
|
33
|
+
end
|
34
|
+
|
35
|
+
unless name.match?(/^[a-z_]+$/)
|
36
|
+
raise Thor::Error, "Group name must be in snake_case format (lowercase letters and underscores only)."
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def check_group_existence!
|
41
|
+
if FeaturePack.group(name.to_sym).present?
|
42
|
+
raise Thor::Error, "Group '#{name}' already exists"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def generate_group_id
|
47
|
+
# Generate a unique ID based on timestamp
|
48
|
+
# Format: YYMMDD (can't contain underscores)
|
49
|
+
Time.now.strftime('%y%m%d')
|
50
|
+
end
|
51
|
+
|
52
|
+
def create_group_files(group_dir)
|
53
|
+
template './_group_space/controller.rb.tt', group_dir.join('_group_space', 'controller.rb')
|
54
|
+
template './_group_space/manifest.yaml.tt', group_dir.join('_group_space', 'manifest.yaml')
|
55
|
+
template './_group_space/routes.rb.tt', group_dir.join('_group_space', 'routes.rb')
|
56
|
+
template './_group_space/views/index.html.slim.tt', group_dir.join('_group_space', 'views/index.html.slim')
|
57
|
+
template './_group_space/views/partials/_header.html.slim.tt',
|
58
|
+
group_dir.join('_group_space', 'views/partials/_header.html.slim')
|
59
|
+
template './_group_space/views/partials/_footer.html.slim.tt',
|
60
|
+
group_dir.join('_group_space', 'views/partials/_footer.html.slim')
|
61
|
+
|
62
|
+
# Create javascript directory
|
63
|
+
empty_directory group_dir.join('_group_space', 'javascript')
|
64
|
+
end
|
28
65
|
end
|
29
66
|
end
|
@@ -1,3 +1,13 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# Group controller for <%= @class_name %>
|
2
|
+
class FeaturePack::<%= @class_name %>Controller < FeaturePack::GroupController
|
3
|
+
# The 'index' action is already defined in the parent GroupController
|
4
|
+
# Add any group-wide functionality here that will be inherited by all features
|
5
|
+
|
6
|
+
# Example:
|
7
|
+
# before_action :authenticate_user!
|
8
|
+
# before_action :check_permissions
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
# Add shared private methods for this group here
|
3
13
|
end
|
@@ -1,2 +1,8 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# Group manifest configuration
|
2
|
+
name: <%= @class_name.humanize %>
|
3
|
+
url: /<%= name.gsub('_', '-') %>
|
4
|
+
|
5
|
+
# Optional: Define constant aliases for this group
|
6
|
+
# const_aliases:
|
7
|
+
# - model_name: ModelClass
|
8
|
+
# - service_name: ServiceClass
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: feature_pack
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gedean Dias
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-02-09 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: activesupport
|
@@ -29,10 +29,10 @@ dependencies:
|
|
29
29
|
- - "<"
|
30
30
|
- !ruby/object:Gem::Version
|
31
31
|
version: '9.0'
|
32
|
-
description:
|
33
|
-
|
34
|
-
|
35
|
-
|
32
|
+
description: |
|
33
|
+
Organizes and sets up the architecture of micro-applications within a Rails application,
|
34
|
+
enabling segregation, management, and isolation of functionalities, thereby supporting
|
35
|
+
independent development, testing, and maintenance.
|
36
36
|
email: gedean.dias@gmail.com
|
37
37
|
executables: []
|
38
38
|
extensions: []
|
@@ -66,8 +66,11 @@ files:
|
|
66
66
|
homepage: https://github.com/gedean/feature_pack
|
67
67
|
licenses:
|
68
68
|
- MIT
|
69
|
-
metadata:
|
70
|
-
|
69
|
+
metadata:
|
70
|
+
bug_tracker_uri: https://github.com/gedean/feature_pack/issues
|
71
|
+
source_code_uri: https://github.com/gedean/feature_pack
|
72
|
+
changelog_uri: https://github.com/gedean/feature_pack/blob/main/CHANGELOG.MD
|
73
|
+
post_install_message: Please check the README file for use instructions.
|
71
74
|
rdoc_options: []
|
72
75
|
require_paths:
|
73
76
|
- lib
|
@@ -82,7 +85,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
82
85
|
- !ruby/object:Gem::Version
|
83
86
|
version: '0'
|
84
87
|
requirements: []
|
85
|
-
rubygems_version: 3.6.
|
88
|
+
rubygems_version: 3.6.9
|
86
89
|
specification_version: 4
|
87
|
-
summary: A different
|
90
|
+
summary: A different approach to organizing Rails app features.
|
88
91
|
test_files: []
|