rubelith 0.1.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 +7 -0
- data/README.md +103 -0
- data/bin/rubelith +122 -0
- data/core/crystallis/base.rb +111 -0
- data/core/crystallis/migration.rb +33 -0
- data/core/crystallis/relation.rb +25 -0
- data/core/crystallis/schema.rb +23 -0
- data/core/crystallis/validators.rb +18 -0
- data/core/router.rb +148 -0
- data/lib/lithblade/engine.rb +56 -0
- data/lib/lithblade.rb +8 -0
- data/lib/rubelith/application.rb +591 -0
- data/lib/rubelith/crystallis.rb +12 -0
- data/lib/rubelith/version.rb +52 -0
- data/lib/rubelith.rb +173 -0
- metadata +174 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2b352a7629434b39c54e66e114f859f1321972184de1ac06fa093c65236408bb
|
4
|
+
data.tar.gz: 41a8f332f5c92b9c1825f9487a40e3cb26191fddc0fae0352f093a7e5085d01a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8c31d88c4118a7db055d337046fd0fa4e0df5133ee3c187fc6cb548fdddbc969978ad2a3f9f921521b6e9c901ce4270219cabc3a2637454d1eaf196beb5d4f31
|
7
|
+
data.tar.gz: aa12d326bc2cb0bc26631d43413c279d5143299d4529adf952e3bf23cfcde890c32c19679637b24357ecfdce31c8cbcaf8c35897f1e4381d957531c72b10c539
|
data/README.md
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
|
2
|
+
# Rubelith Framework
|
3
|
+
|
4
|
+
## Overview
|
5
|
+
Rubelith is an enterprise-grade, modular Ruby web framework designed for productivity, security, and scalability. It features a powerful CLI, middleware, authentication, error handling, asset pipeline, and the LithBlade templating engine.
|
6
|
+
|
7
|
+
## Features
|
8
|
+
- Modular MVC architecture (`app/controllers`, `app/models`, `app/views`, etc.)
|
9
|
+
- Powerful CLI (`bin/rubelith`) for server, migrations, jobs, assets, docs, and more
|
10
|
+
- Middleware stack for request/response hooks
|
11
|
+
- JWT & Bcrypt authentication, session and role-based access
|
12
|
+
- Robust error handling and logging
|
13
|
+
- Sass engine integration for asset pipeline
|
14
|
+
- LithBlade templating engine (secure, Blade-inspired syntax)
|
15
|
+
- Policy engine, encryption, custom validators
|
16
|
+
- Multi-database support, job prioritization, background jobs
|
17
|
+
- API docs, request replay, headless mode, REPL, third-party integrations
|
18
|
+
- Security: CSRF/XSS protection, password hashing, safe config loading
|
19
|
+
|
20
|
+
## Getting Started
|
21
|
+
1. Install dependencies:
|
22
|
+
```sh
|
23
|
+
bundle install
|
24
|
+
```
|
25
|
+
2. Run the server:
|
26
|
+
```sh
|
27
|
+
bin/rubelith server
|
28
|
+
```
|
29
|
+
3. Use the CLI for migrations, jobs, assets, docs, and more:
|
30
|
+
```sh
|
31
|
+
bin/rubelith migrate
|
32
|
+
bin/rubelith seed
|
33
|
+
bin/rubelith test
|
34
|
+
bin/rubelith optimize
|
35
|
+
bin/rubelith generate
|
36
|
+
bin/rubelith docs
|
37
|
+
bin/rubelith assets
|
38
|
+
bin/rubelith websockets
|
39
|
+
bin/rubelith api
|
40
|
+
bin/rubelith i18n
|
41
|
+
bin/rubelith monitor
|
42
|
+
bin/rubelith analytics
|
43
|
+
bin/rubelith session
|
44
|
+
bin/rubelith policy
|
45
|
+
bin/rubelith encrypt
|
46
|
+
bin/rubelith decrypt
|
47
|
+
bin/rubelith validate_custom
|
48
|
+
bin/rubelith add_database
|
49
|
+
bin/rubelith get_database
|
50
|
+
bin/rubelith enqueue_priority
|
51
|
+
bin/rubelith run_priority_jobs
|
52
|
+
bin/rubelith generate_api_docs
|
53
|
+
bin/rubelith replay_request
|
54
|
+
bin/rubelith headless_mode
|
55
|
+
bin/rubelith repl
|
56
|
+
bin/rubelith integrate_with
|
57
|
+
# See CLI help for all commands
|
58
|
+
```
|
59
|
+
|
60
|
+
## Project Structure
|
61
|
+
- `app/` - Controllers, models, jobs, mailers, views, support
|
62
|
+
- `bin/rubelith` - CLI entry point
|
63
|
+
- `config/` - Application and route configs
|
64
|
+
- `core/` - Framework internals
|
65
|
+
- `db/` - Migrations and schema
|
66
|
+
- `lib/` - Framework code
|
67
|
+
- `public/` - Static assets
|
68
|
+
- `test/` - Test suite
|
69
|
+
|
70
|
+
## LithBlade Templating Engine
|
71
|
+
- Secure, Blade-inspired syntax: `{{ var }}` (escaped), `{!! var !!}` (raw)
|
72
|
+
- Directives: `@if`, `@elseif`, `@else`, `@endif`, `@foreach`, `@endforeach`, `@include`, `@extends`, `@section`, `@yield`
|
73
|
+
- Path validation and output escaping by default
|
74
|
+
- Use `.lithblade.php` for LithBlade templates/partials
|
75
|
+
|
76
|
+
## Security Best Practices
|
77
|
+
- All output escaped by default
|
78
|
+
- CSRF and XSS protection built-in
|
79
|
+
- Passwords hashed with Bcrypt
|
80
|
+
- JWT secret must be set in production
|
81
|
+
- Safe YAML config loading
|
82
|
+
|
83
|
+
## Testing & Coverage
|
84
|
+
- Run tests: `bin/rubelith test` or `bundle exec rake test`
|
85
|
+
- Coverage: SimpleCov
|
86
|
+
|
87
|
+
## Advanced Features
|
88
|
+
- Background jobs, job prioritization
|
89
|
+
- Multi-database support
|
90
|
+
- API docs generation
|
91
|
+
- Request replay
|
92
|
+
- Headless mode
|
93
|
+
- REPL
|
94
|
+
- Third-party integrations
|
95
|
+
|
96
|
+
## Contributing
|
97
|
+
1. Fork the repo and clone locally
|
98
|
+
2. Create a feature branch
|
99
|
+
3. Add tests for new features
|
100
|
+
4. Submit a pull request
|
101
|
+
|
102
|
+
## License
|
103
|
+
MIT. All rights reserved.
|
data/bin/rubelith
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Rubelith CLI Entry Point
|
3
|
+
|
4
|
+
require_relative '../lib/rubelith'
|
5
|
+
|
6
|
+
module Rubelith
|
7
|
+
class CLI
|
8
|
+
def self.run
|
9
|
+
command = ARGV.shift
|
10
|
+
case command
|
11
|
+
when 'server'
|
12
|
+
require_relative '../config/routes'
|
13
|
+
require 'rack'
|
14
|
+
puts "Starting Rubelith server on http://localhost:9292 ..."
|
15
|
+
Rack::Handler::WEBrick.run Rubelith.application, Port: 9292
|
16
|
+
when 'optimize'
|
17
|
+
puts "Optimizing Rubelith application..."
|
18
|
+
app = Rubelith.application
|
19
|
+
app.load_models
|
20
|
+
File.write('tmp/config_cache.yml', app.config.to_yaml)
|
21
|
+
if app.instance_variable_defined?(:@plugins)
|
22
|
+
app.instance_variable_get(:@plugins).each do |plugin|
|
23
|
+
plugin.load(app) if plugin.respond_to?(:load)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
puts "Optimization complete. Config cached to tmp/config_cache.yml."
|
27
|
+
when 'migrate'
|
28
|
+
puts "Running migrations..."
|
29
|
+
Rubelith.application.migrate!
|
30
|
+
puts "Migrations complete."
|
31
|
+
when 'seed'
|
32
|
+
Rubelith.application.seed!
|
33
|
+
when 'cache'
|
34
|
+
Rubelith.application.clear_cache!
|
35
|
+
when 'test'
|
36
|
+
puts "Running tests..."
|
37
|
+
Rubelith.application.run_tests
|
38
|
+
puts "Tests complete."
|
39
|
+
when 'generate'
|
40
|
+
puts "Scaffolding new resource..."
|
41
|
+
# Stub: Implement generator logic
|
42
|
+
puts "Resource generated."
|
43
|
+
when 'docs'
|
44
|
+
Rubelith.application.generate_docs!
|
45
|
+
when 'event'
|
46
|
+
event_name = ARGV.shift || 'custom_event'
|
47
|
+
Rubelith.application.trigger_event!(event_name)
|
48
|
+
when 'schedule'
|
49
|
+
Rubelith.application.schedule_tasks!
|
50
|
+
when 'assets'
|
51
|
+
Rubelith.application.process_assets!
|
52
|
+
when 'websockets'
|
53
|
+
Rubelith.application.start_websockets!
|
54
|
+
when 'api'
|
55
|
+
Rubelith.application.enable_api_mode!
|
56
|
+
when 'i18n'
|
57
|
+
Rubelith.application.manage_translations!
|
58
|
+
when 'monitor'
|
59
|
+
Rubelith.application.monitor_app!
|
60
|
+
when 'analytics'
|
61
|
+
Rubelith.application.track_analytics!
|
62
|
+
when 'session'
|
63
|
+
Rubelith.application.manage_sessions!
|
64
|
+
when 'policy'
|
65
|
+
user = ARGV.shift
|
66
|
+
record = ARGV.shift
|
67
|
+
action = ARGV.shift
|
68
|
+
result = Rubelith.application.policy_for(user, record, action)
|
69
|
+
puts "Policy result: #{result}"
|
70
|
+
when 'encrypt'
|
71
|
+
value = ARGV.shift
|
72
|
+
key = ARGV.shift || 'rubelith_default_key'
|
73
|
+
puts Rubelith.application.encrypt_field(value, key: key)
|
74
|
+
when 'decrypt'
|
75
|
+
value = ARGV.shift
|
76
|
+
key = ARGV.shift || 'rubelith_default_key'
|
77
|
+
puts Rubelith.application.decrypt_field(value, key: key)
|
78
|
+
when 'validate_custom'
|
79
|
+
name = ARGV.shift
|
80
|
+
value = ARGV.shift
|
81
|
+
result = Rubelith.application.validate_custom(name, value)
|
82
|
+
puts "Validation result: #{result}"
|
83
|
+
when 'add_database'
|
84
|
+
name = ARGV.shift
|
85
|
+
config = ARGV.shift
|
86
|
+
Rubelith.application.add_database(name, config)
|
87
|
+
when 'get_database'
|
88
|
+
name = ARGV.shift
|
89
|
+
puts Rubelith.application.get_database(name).inspect
|
90
|
+
when 'enqueue_priority'
|
91
|
+
job_class = ARGV.shift
|
92
|
+
priority = (ARGV.shift || :normal).to_sym
|
93
|
+
args = ARGV
|
94
|
+
Rubelith.application.enqueue_priority(Object.const_get(job_class), *args, priority: priority)
|
95
|
+
when 'run_priority_jobs'
|
96
|
+
Rubelith.application.run_priority_jobs!
|
97
|
+
when 'generate_api_docs'
|
98
|
+
Rubelith.application.generate_api_docs!
|
99
|
+
when 'replay_request'
|
100
|
+
env = eval(ARGV.shift)
|
101
|
+
Rubelith.application.replay_request(env)
|
102
|
+
when 'headless_mode'
|
103
|
+
Rubelith.application.headless_mode!
|
104
|
+
when 'repl'
|
105
|
+
Rubelith.application.repl!
|
106
|
+
when 'integrate_with'
|
107
|
+
service = ARGV.shift
|
108
|
+
config = ARGV.shift || {}
|
109
|
+
Rubelith.application.integrate_with(service, config)
|
110
|
+
else
|
111
|
+
puts "Unknown command: #{command}"
|
112
|
+
puts "Available commands: server, optimize, migrate, seed, cache, test, generate, docs, event, schedule, assets, websockets, api, i18n, monitor, analytics, session, policy, encrypt, decrypt, validate_custom, add_database, get_database, enqueue_priority, run_priority_jobs, generate_api_docs, replay_request, headless_mode, repl, integrate_with"
|
113
|
+
puts "To start the server: bin/rubelith server"
|
114
|
+
end
|
115
|
+
rescue => e
|
116
|
+
Rubelith.log("CLI error: #{e.message}", level: :error)
|
117
|
+
puts "Fatal error: #{e.message}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
Rubelith::CLI.run
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# Crystallis ORM Base
|
2
|
+
|
3
|
+
require_relative '../../db/schema'
|
4
|
+
|
5
|
+
module Crystallis
|
6
|
+
class Base
|
7
|
+
# Relations
|
8
|
+
def has_many(association)
|
9
|
+
define_singleton_method(association) do |id|
|
10
|
+
assoc_table = association.to_s
|
11
|
+
foreign_key = "#{@table.singularize}_id"
|
12
|
+
@schema.secure_query("SELECT * FROM #{assoc_table} WHERE #{foreign_key} = ?", [id])
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def belongs_to(association)
|
17
|
+
define_singleton_method(association) do |id|
|
18
|
+
assoc_table = association.to_s
|
19
|
+
@schema.secure_query("SELECT * FROM #{assoc_table} WHERE id = ? LIMIT 1", [id])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Validations
|
24
|
+
def validates(field, type, options = {})
|
25
|
+
@validations ||= []
|
26
|
+
@validations << { field: field, type: type, options: options }
|
27
|
+
end
|
28
|
+
|
29
|
+
def valid?(attrs)
|
30
|
+
return true unless @validations
|
31
|
+
@validations.all? do |v|
|
32
|
+
value = attrs[v[:field]]
|
33
|
+
case v[:type]
|
34
|
+
when :presence
|
35
|
+
!value.nil? && !(value.respond_to?(:empty?) && value.empty?)
|
36
|
+
when :uniqueness
|
37
|
+
res = @schema.secure_query("SELECT COUNT(*) FROM #{@table} WHERE #{v[:field]} = ?", [value])
|
38
|
+
res.first[0] == 0
|
39
|
+
when :format
|
40
|
+
value =~ v[:options][:with]
|
41
|
+
else
|
42
|
+
true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
attr_reader :schema, :table
|
47
|
+
|
48
|
+
def initialize(table, adapter: :sqlite, config: {})
|
49
|
+
@schema = Rubelith::DB::Schema.new(adapter: adapter, config: config)
|
50
|
+
@table = table
|
51
|
+
end
|
52
|
+
|
53
|
+
def all
|
54
|
+
@schema.execute("SELECT * FROM #{@table}")
|
55
|
+
end
|
56
|
+
|
57
|
+
def find(id)
|
58
|
+
@schema.secure_query("SELECT * FROM #{@table} WHERE id = ? LIMIT 1", [id])
|
59
|
+
end
|
60
|
+
|
61
|
+
def where(conditions)
|
62
|
+
keys = conditions.keys
|
63
|
+
values = conditions.values
|
64
|
+
clause = keys.map { |k| "#{k} = ?" }.join(" AND ")
|
65
|
+
@schema.secure_query("SELECT * FROM #{@table} WHERE #{clause}", values)
|
66
|
+
end
|
67
|
+
|
68
|
+
def create(attrs)
|
69
|
+
keys = attrs.keys
|
70
|
+
values = attrs.values
|
71
|
+
cols = keys.join(", ")
|
72
|
+
placeholders = (['?'] * keys.size).join(", ")
|
73
|
+
sql = "INSERT INTO #{@table} (#{cols}) VALUES (#{placeholders})"
|
74
|
+
@schema.secure_query(sql, values)
|
75
|
+
end
|
76
|
+
|
77
|
+
def update(id, attrs)
|
78
|
+
keys = attrs.keys
|
79
|
+
values = attrs.values
|
80
|
+
set_clause = keys.map { |k| "#{k} = ?" }.join(", ")
|
81
|
+
sql = "UPDATE #{@table} SET #{set_clause} WHERE id = ?"
|
82
|
+
@schema.secure_query(sql, values + [id])
|
83
|
+
end
|
84
|
+
|
85
|
+
def delete(id)
|
86
|
+
@schema.secure_query("DELETE FROM #{@table} WHERE id = ?", [id])
|
87
|
+
end
|
88
|
+
|
89
|
+
def transaction(&block)
|
90
|
+
@schema.transaction(&block)
|
91
|
+
end
|
92
|
+
|
93
|
+
def migrate(&block)
|
94
|
+
Rubelith::DB::Migration.new(@schema).instance_eval(&block)
|
95
|
+
end
|
96
|
+
|
97
|
+
def switch_adapter(new_adapter, new_config = {})
|
98
|
+
@schema.switch_adapter(new_adapter, new_config)
|
99
|
+
end
|
100
|
+
|
101
|
+
def close
|
102
|
+
@schema.close
|
103
|
+
end
|
104
|
+
|
105
|
+
# Security: SQL injection protection is handled by secure_query
|
106
|
+
# Performance: Uses prepared statements and transactions
|
107
|
+
# Scalability: Adapter switching and connection pooling (future)
|
108
|
+
# Robustness: Exception handling and transaction rollback
|
109
|
+
# Feature-rich: CRUD, migrations, transactions, adapter switching
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# Crystallis ORM Migration
|
2
|
+
|
3
|
+
module Crystallis
|
4
|
+
class Migration
|
5
|
+
attr_reader :schema
|
6
|
+
|
7
|
+
def initialize(schema)
|
8
|
+
@schema = schema
|
9
|
+
end
|
10
|
+
|
11
|
+
def create_table(table, columns)
|
12
|
+
cols = columns.map { |name, type| "#{name} #{type}" }.join(", ")
|
13
|
+
sql = "CREATE TABLE IF NOT EXISTS #{table} (id INTEGER PRIMARY KEY AUTOINCREMENT, #{cols})"
|
14
|
+
@schema.execute(sql)
|
15
|
+
end
|
16
|
+
|
17
|
+
def drop_table(table)
|
18
|
+
sql = "DROP TABLE IF EXISTS #{table}"
|
19
|
+
@schema.execute(sql)
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_column(table, name, type)
|
23
|
+
sql = "ALTER TABLE #{table} ADD COLUMN #{name} #{type}"
|
24
|
+
@schema.execute(sql)
|
25
|
+
end
|
26
|
+
|
27
|
+
def remove_column(table, name)
|
28
|
+
# SQLite does not support removing columns directly
|
29
|
+
# For other DBs, implement as needed
|
30
|
+
# Placeholder
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# Crystallis ORM Relation
|
2
|
+
|
3
|
+
module Crystallis
|
4
|
+
class Relation
|
5
|
+
attr_reader :model, :association, :type
|
6
|
+
|
7
|
+
def initialize(model, association, type)
|
8
|
+
@model = model
|
9
|
+
@association = association
|
10
|
+
@type = type
|
11
|
+
end
|
12
|
+
|
13
|
+
def build_query(id)
|
14
|
+
case type
|
15
|
+
when :has_many
|
16
|
+
foreign_key = "#{model.table.singularize}_id"
|
17
|
+
"SELECT * FROM #{association} WHERE #{foreign_key} = #{id}"
|
18
|
+
when :belongs_to
|
19
|
+
"SELECT * FROM #{association} WHERE id = #{id} LIMIT 1"
|
20
|
+
else
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Crystallis ORM Schema
|
2
|
+
|
3
|
+
module Crystallis
|
4
|
+
class Schema
|
5
|
+
attr_reader :db
|
6
|
+
|
7
|
+
def initialize(db)
|
8
|
+
@db = db
|
9
|
+
end
|
10
|
+
|
11
|
+
def tables
|
12
|
+
db.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
13
|
+
end
|
14
|
+
|
15
|
+
def columns(table)
|
16
|
+
db.execute("PRAGMA table_info(#{table})")
|
17
|
+
end
|
18
|
+
|
19
|
+
def drop_table(table)
|
20
|
+
db.execute("DROP TABLE IF EXISTS #{table}")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Crystallis ORM Validators
|
2
|
+
|
3
|
+
module Crystallis
|
4
|
+
module Validators
|
5
|
+
def self.presence(value)
|
6
|
+
!value.nil? && !(value.respond_to?(:empty?) && value.empty?)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.uniqueness(model, field, value)
|
10
|
+
res = model.where(field => value)
|
11
|
+
res.empty?
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.format(value, regex)
|
15
|
+
!!(value =~ regex)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/core/router.rb
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
# Rubelith Fast, Secure, Scalable Router
|
2
|
+
|
3
|
+
module Rubelith
|
4
|
+
class Router
|
5
|
+
# RESTful helpers
|
6
|
+
def resources(resource, &block)
|
7
|
+
add_route(:get, "/#{resource}", block)
|
8
|
+
add_route(:get, "/#{resource}/:id", block)
|
9
|
+
add_route(:post, "/#{resource}", block)
|
10
|
+
add_route(:put, "/#{resource}/:id", block)
|
11
|
+
add_route(:patch, "/#{resource}/:id", block)
|
12
|
+
add_route(:delete, "/#{resource}/:id", block)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Route parameter parsing and wildcards
|
16
|
+
def parse_params(path, req_path)
|
17
|
+
path_parts = path.split('/').reject(&:empty?)
|
18
|
+
req_parts = req_path.split('/').reject(&:empty?)
|
19
|
+
params = {}
|
20
|
+
path_parts.each_with_index do |part, i|
|
21
|
+
if part.start_with?(':')
|
22
|
+
params[part[1..-1].to_sym] = req_parts[i]
|
23
|
+
elsif part == '*'
|
24
|
+
params[:wildcard] = req_parts[i..-1].join('/')
|
25
|
+
break
|
26
|
+
end
|
27
|
+
end
|
28
|
+
params
|
29
|
+
end
|
30
|
+
|
31
|
+
def match(method, path)
|
32
|
+
method = method.to_s.upcase
|
33
|
+
node = @trie.search(path)
|
34
|
+
return nil unless node
|
35
|
+
route = node[:method] == method ? node : nil
|
36
|
+
if route && route[:action].respond_to?(:arity) && route[:action].arity > 2
|
37
|
+
params = parse_params(route[:path], path)
|
38
|
+
route[:params] = params
|
39
|
+
end
|
40
|
+
route
|
41
|
+
end
|
42
|
+
Route = Struct.new(:method, :path, :action, :constraints)
|
43
|
+
|
44
|
+
def initialize
|
45
|
+
@routes = {}
|
46
|
+
@trie = TrieNode.new
|
47
|
+
@mutex = Mutex.new
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_route(method, path, action, constraints = {})
|
51
|
+
@mutex.synchronize do
|
52
|
+
method = method.to_s.upcase
|
53
|
+
@routes[method] ||= []
|
54
|
+
@routes[method] << Route.new(method, path, action, constraints)
|
55
|
+
@trie.insert(path, { method: method, action: action, constraints: constraints })
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def match(method, path)
|
60
|
+
method = method.to_s.upcase
|
61
|
+
node = @trie.search(path)
|
62
|
+
return nil unless node
|
63
|
+
route = node[:method] == method ? node : nil
|
64
|
+
route
|
65
|
+
end
|
66
|
+
|
67
|
+
def dispatch(env)
|
68
|
+
req = Rack::Request.new(env)
|
69
|
+
route = match(req.request_method, req.path_info)
|
70
|
+
res = Rack::Response.new
|
71
|
+
begin
|
72
|
+
if route
|
73
|
+
# Security: method whitelisting, path sanitization
|
74
|
+
return forbidden(res) unless %w[GET POST PUT PATCH DELETE].include?(route[:method])
|
75
|
+
return forbidden(res) unless safe_path?(req.path_info)
|
76
|
+
# CSRF protection for state-changing methods
|
77
|
+
if %w[POST PUT PATCH DELETE].include?(route[:method])
|
78
|
+
return forbidden(res) unless Rubelith.application.protect_from_csrf(req, res)
|
79
|
+
end
|
80
|
+
if route[:params]
|
81
|
+
result = route[:action].call(req, res, route[:params])
|
82
|
+
else
|
83
|
+
result = route[:action].call(req, res)
|
84
|
+
end
|
85
|
+
res.write(result) if result.is_a?(String)
|
86
|
+
else
|
87
|
+
res.status = 404
|
88
|
+
res.write("404 Not Found")
|
89
|
+
end
|
90
|
+
rescue => e
|
91
|
+
res.status = 500
|
92
|
+
res.write("500 Internal Server Error: #{e.message}")
|
93
|
+
end
|
94
|
+
res.finish
|
95
|
+
end
|
96
|
+
|
97
|
+
def safe_path?(path)
|
98
|
+
!path.match(/[^\w\/-]/)
|
99
|
+
end
|
100
|
+
|
101
|
+
def forbidden(res)
|
102
|
+
res.status = 403
|
103
|
+
res.write("403 Forbidden")
|
104
|
+
res.finish
|
105
|
+
end
|
106
|
+
|
107
|
+
def group(prefix, &block)
|
108
|
+
@group_prefix = prefix
|
109
|
+
instance_eval(&block)
|
110
|
+
@group_prefix = nil
|
111
|
+
end
|
112
|
+
|
113
|
+
def namespace(name, &block)
|
114
|
+
group("/#{name}", &block)
|
115
|
+
end
|
116
|
+
|
117
|
+
def version(ver, &block)
|
118
|
+
group("/v#{ver}", &block)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Trie for fast path matching
|
122
|
+
class TrieNode
|
123
|
+
attr_accessor :children, :route
|
124
|
+
def initialize
|
125
|
+
@children = {}
|
126
|
+
@route = nil
|
127
|
+
end
|
128
|
+
def insert(path, route)
|
129
|
+
parts = path.split('/').reject(&:empty?)
|
130
|
+
node = self
|
131
|
+
parts.each do |part|
|
132
|
+
node.children[part] ||= TrieNode.new
|
133
|
+
node = node.children[part]
|
134
|
+
end
|
135
|
+
node.route = route
|
136
|
+
end
|
137
|
+
def search(path)
|
138
|
+
parts = path.split('/').reject(&:empty?)
|
139
|
+
node = self
|
140
|
+
parts.each do |part|
|
141
|
+
return nil unless node.children[part]
|
142
|
+
node = node.children[part]
|
143
|
+
end
|
144
|
+
node.route
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
|
2
|
+
# LithBlade Templating Engine
|
3
|
+
# Initial scaffold for LithBlade syntax in Rubelith
|
4
|
+
|
5
|
+
module LithBlade
|
6
|
+
class Engine
|
7
|
+
# Securely render a template file with context
|
8
|
+
def render(template_path, context = {})
|
9
|
+
raise "Invalid template path" unless valid_template_path?(template_path)
|
10
|
+
template = File.read(template_path)
|
11
|
+
compiled = compile(template)
|
12
|
+
erb = ERB.new(compiled)
|
13
|
+
erb.result(binding_for(context))
|
14
|
+
rescue => e
|
15
|
+
"LithBlade Error: #{e.message}"
|
16
|
+
end
|
17
|
+
|
18
|
+
# Compile LithBlade syntax to ERB
|
19
|
+
def compile(template)
|
20
|
+
# Escaped output: {{ var }}
|
21
|
+
template = template.gsub(/\{\{\s*(\w+)\s*\}\}/, '<%= h(context["\\1"]) %>')
|
22
|
+
# Raw output: {!! var !!}
|
23
|
+
template = template.gsub(/\{!!\s*(\w+)\s*!!\}/, '<%= context["\\1"] %>')
|
24
|
+
# @if, @elseif, @else, @endif
|
25
|
+
template = template.gsub(/@if\s*\((.*?)\)/, '<% if \1 %>')
|
26
|
+
template = template.gsub(/@elseif\s*\((.*?)\)/, '<% elsif \1 %>')
|
27
|
+
template = template.gsub(/@else/, '<% else %>')
|
28
|
+
template = template.gsub(/@endif/, '<% end %>')
|
29
|
+
# @foreach, @endforeach
|
30
|
+
template = template.gsub(/@foreach\s*\((\w+)\s+in\s+(\w+)\)/, '<% \2.each do |\1| %>')
|
31
|
+
template = template.gsub(/@endforeach/, '<% end %>')
|
32
|
+
# @include('partial')
|
33
|
+
template = template.gsub(/@include\(['"](.*?)['"]\)/, '<%= LithBlade::Engine.new.render("views/\\1.lithblade.php", context) %>')
|
34
|
+
# @extends, @section, @yield (basic stub)
|
35
|
+
# TODO: Implement layout inheritance and sections
|
36
|
+
template
|
37
|
+
end
|
38
|
+
|
39
|
+
# Escape HTML output
|
40
|
+
def h(text)
|
41
|
+
CGI.escapeHTML(text.to_s)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Create a binding with context
|
45
|
+
def binding_for(context)
|
46
|
+
context_binding = binding
|
47
|
+
context.each { |k, v| context_binding.local_variable_set(k, v) }
|
48
|
+
context_binding
|
49
|
+
end
|
50
|
+
|
51
|
+
# Validate template path (prevent path traversal)
|
52
|
+
def valid_template_path?(path)
|
53
|
+
File.expand_path(path).start_with?(File.expand_path("views/")) && File.exist?(path)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|