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
@@ -0,0 +1,591 @@
|
|
1
|
+
# --- Enterprise/Niche Features ---
|
2
|
+
def policy_for(user, record, action)
|
3
|
+
# Stub: Pundit-style policy check
|
4
|
+
policy_class = Object.const_get("#{record.class}Policy") rescue nil
|
5
|
+
if policy_class && policy_class.respond_to?(:new)
|
6
|
+
policy = policy_class.new(user, record)
|
7
|
+
policy.respond_to?(action) ? policy.send(action) : false
|
8
|
+
else
|
9
|
+
false
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def encrypt_field(value, key: 'rubelith_default_key')
|
14
|
+
require 'openssl'
|
15
|
+
cipher = OpenSSL::Cipher.new('AES-256-CBC')
|
16
|
+
cipher.encrypt
|
17
|
+
cipher.key = Digest::SHA256.digest(key)
|
18
|
+
iv = cipher.random_iv
|
19
|
+
encrypted = cipher.update(value.to_s) + cipher.final
|
20
|
+
[encrypted, iv].map { |v| [v].pack('m') }.join('--')
|
21
|
+
end
|
22
|
+
|
23
|
+
def decrypt_field(encrypted, key: 'rubelith_default_key')
|
24
|
+
require 'openssl'
|
25
|
+
encrypted, iv = encrypted.split('--').map { |v| v.unpack1('m') }
|
26
|
+
cipher = OpenSSL::Cipher.new('AES-256-CBC')
|
27
|
+
cipher.decrypt
|
28
|
+
cipher.key = Digest::SHA256.digest(key)
|
29
|
+
cipher.iv = iv
|
30
|
+
cipher.update(encrypted) + cipher.final
|
31
|
+
end
|
32
|
+
|
33
|
+
def register_validator(name, &block)
|
34
|
+
@custom_validators ||= {}
|
35
|
+
@custom_validators[name] = block
|
36
|
+
puts "Validator '#{name}' registered."
|
37
|
+
end
|
38
|
+
|
39
|
+
def validate_custom(name, value)
|
40
|
+
if @custom_validators && @custom_validators[name]
|
41
|
+
@custom_validators[name].call(value)
|
42
|
+
else
|
43
|
+
false
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# LithBlade rendering
|
48
|
+
def render_lithblade(template_path, context = {})
|
49
|
+
LithBlade.render(template_path, context)
|
50
|
+
end
|
51
|
+
|
52
|
+
def add_database(name, config)
|
53
|
+
@databases ||= {}
|
54
|
+
@databases[name] = config
|
55
|
+
puts "Database '#{name}' added."
|
56
|
+
end
|
57
|
+
|
58
|
+
def get_database(name)
|
59
|
+
@databases ? @databases[name] : nil
|
60
|
+
end
|
61
|
+
|
62
|
+
def enqueue_priority(job_class, *args, priority: :normal)
|
63
|
+
@priority_queue ||= Hash.new { |h, k| h[k] = [] }
|
64
|
+
@priority_queue[priority] << { job: job_class, args: args }
|
65
|
+
puts "Job enqueued with priority #{priority}: #{job_class}"
|
66
|
+
end
|
67
|
+
|
68
|
+
def run_priority_jobs!
|
69
|
+
(@priority_queue || {}).each do |_priority, jobs|
|
70
|
+
jobs.each do |job|
|
71
|
+
Thread.new { job[:job].new.perform(*job[:args]) }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
puts 'Priority jobs executed.'
|
75
|
+
end
|
76
|
+
|
77
|
+
def generate_api_docs!
|
78
|
+
docs_path = File.join(Dir.pwd, 'docs')
|
79
|
+
Dir.mkdir(docs_path) unless Dir.exist?(docs_path)
|
80
|
+
File.write(File.join(docs_path, 'openapi.yml'), "# OpenAPI/Swagger docs (stub)")
|
81
|
+
puts 'API documentation generated.'
|
82
|
+
end
|
83
|
+
|
84
|
+
def replay_request(env)
|
85
|
+
puts "Replaying request: #{env.inspect}"
|
86
|
+
call(env)
|
87
|
+
end
|
88
|
+
|
89
|
+
def headless_mode!
|
90
|
+
@headless = true
|
91
|
+
puts 'Headless API mode enabled.'
|
92
|
+
end
|
93
|
+
|
94
|
+
def repl!
|
95
|
+
require 'irb'
|
96
|
+
puts 'Starting Rubelith REPL...'
|
97
|
+
IRB.start
|
98
|
+
end
|
99
|
+
|
100
|
+
def integrate_with(service, config = {})
|
101
|
+
@integrations ||= {}
|
102
|
+
@integrations[service] = config
|
103
|
+
puts "Integrated with #{service}."
|
104
|
+
end
|
105
|
+
# --- Advanced Features ---
|
106
|
+
def hot_reload!
|
107
|
+
# Stub: Reload code/assets in development
|
108
|
+
puts 'Hot reloading enabled.'
|
109
|
+
end
|
110
|
+
|
111
|
+
def service_container
|
112
|
+
@services ||= {}
|
113
|
+
end
|
114
|
+
|
115
|
+
def register_service(name, instance)
|
116
|
+
service_container[name] = instance
|
117
|
+
puts "Service '#{name}' registered."
|
118
|
+
end
|
119
|
+
|
120
|
+
def background_job(job_class, *args)
|
121
|
+
Thread.new { job_class.new.perform(*args) }
|
122
|
+
puts "Background job enqueued: #{job_class}"
|
123
|
+
end
|
124
|
+
|
125
|
+
def rate_limit!(key, limit: 100, period: 60)
|
126
|
+
@rate_limits ||= {}
|
127
|
+
now = Time.now.to_i / period
|
128
|
+
@rate_limits[key] ||= { count: 0, window: now }
|
129
|
+
if @rate_limits[key][:window] != now
|
130
|
+
@rate_limits[key] = { count: 1, window: now }
|
131
|
+
else
|
132
|
+
@rate_limits[key][:count] += 1
|
133
|
+
end
|
134
|
+
allowed = @rate_limits[key][:count] <= limit
|
135
|
+
puts "Rate limit for '#{key}': #{allowed ? 'allowed' : 'blocked'}"
|
136
|
+
allowed
|
137
|
+
end
|
138
|
+
|
139
|
+
def audit_log(event, data = {})
|
140
|
+
@audit_logs ||= []
|
141
|
+
@audit_logs << { event: event, data: data, timestamp: Time.now }
|
142
|
+
puts "Audit log: #{event}"
|
143
|
+
end
|
144
|
+
|
145
|
+
def render_error_page(status)
|
146
|
+
puts "Rendering error page for status #{status}"
|
147
|
+
"<h1>Error #{status}</h1>"
|
148
|
+
end
|
149
|
+
|
150
|
+
def upload_file(file, dest)
|
151
|
+
File.write(dest, file.read)
|
152
|
+
puts "File uploaded to #{dest}"
|
153
|
+
end
|
154
|
+
|
155
|
+
def oauth_login(provider)
|
156
|
+
puts "OAuth2 login with #{provider} (stub)"
|
157
|
+
end
|
158
|
+
|
159
|
+
def graphql_query(*)
|
160
|
+
puts "GraphQL query executed (stub)"
|
161
|
+
{}
|
162
|
+
end
|
163
|
+
|
164
|
+
def generate_admin_panel!
|
165
|
+
puts 'Admin panel generated (stub).'
|
166
|
+
end
|
167
|
+
|
168
|
+
def health_check!
|
169
|
+
puts 'Health check passed.'
|
170
|
+
true
|
171
|
+
end
|
172
|
+
|
173
|
+
def enable_multi_tenancy!
|
174
|
+
puts 'Multi-tenancy enabled.'
|
175
|
+
end
|
176
|
+
|
177
|
+
def search(query)
|
178
|
+
puts "Search executed for: #{query} (stub)"
|
179
|
+
[]
|
180
|
+
end
|
181
|
+
|
182
|
+
def send_notification(type, message)
|
183
|
+
puts "Notification sent: [#{type}] #{message}"
|
184
|
+
end
|
185
|
+
|
186
|
+
def set_template_engine(engine)
|
187
|
+
@template_engine = engine
|
188
|
+
puts "Template engine set to #{engine}"
|
189
|
+
end
|
190
|
+
|
191
|
+
def generate_static_site!
|
192
|
+
puts 'Static site generated (stub).'
|
193
|
+
end
|
194
|
+
|
195
|
+
def api_throttle!(key, limit: 100, period: 60)
|
196
|
+
rate_limit!(key, limit: limit, period: period)
|
197
|
+
end
|
198
|
+
|
199
|
+
def add_cli_generator(name, &block)
|
200
|
+
@cli_generators ||= {}
|
201
|
+
@cli_generators[name] = block
|
202
|
+
puts "CLI generator '#{name}' added."
|
203
|
+
end
|
204
|
+
|
205
|
+
def feature_flag(name, enabled)
|
206
|
+
@feature_flags ||= {}
|
207
|
+
@feature_flags[name] = enabled
|
208
|
+
puts "Feature flag '#{name}' set to #{enabled}"
|
209
|
+
end
|
210
|
+
|
211
|
+
def containerize!
|
212
|
+
puts 'Containerization support enabled (stub).'
|
213
|
+
end
|
214
|
+
def schedule_tasks!
|
215
|
+
# Example: Run scheduled jobs (stub)
|
216
|
+
puts 'Scheduled tasks executed.'
|
217
|
+
end
|
218
|
+
|
219
|
+
def process_assets!
|
220
|
+
assets_dir = File.join(Dir.pwd, 'public', 'assets')
|
221
|
+
Dir.mkdir(assets_dir) unless Dir.exist?(assets_dir)
|
222
|
+
# Stub: Minify, bundle, version assets
|
223
|
+
puts 'Assets processed.'
|
224
|
+
end
|
225
|
+
|
226
|
+
def start_websockets!
|
227
|
+
# Stub: Start WebSocket server
|
228
|
+
puts 'WebSocket server started.'
|
229
|
+
end
|
230
|
+
|
231
|
+
def enable_api_mode!
|
232
|
+
# Stub: Enable API mode
|
233
|
+
puts 'API mode enabled.'
|
234
|
+
end
|
235
|
+
|
236
|
+
def manage_translations!
|
237
|
+
# Stub: Update translations
|
238
|
+
puts 'Translations updated.'
|
239
|
+
end
|
240
|
+
|
241
|
+
def monitor_app!
|
242
|
+
# Stub: Start monitoring
|
243
|
+
puts 'Monitoring active.'
|
244
|
+
end
|
245
|
+
|
246
|
+
def track_analytics!
|
247
|
+
# Stub: Track analytics
|
248
|
+
puts 'Analytics tracked.'
|
249
|
+
end
|
250
|
+
|
251
|
+
def manage_sessions!
|
252
|
+
# Stub: Manage sessions
|
253
|
+
puts 'Sessions managed.'
|
254
|
+
end
|
255
|
+
def seed!
|
256
|
+
seed_path = File.expand_path('../../db/seeds.rb', __dir__)
|
257
|
+
if File.exist?(seed_path)
|
258
|
+
require seed_path
|
259
|
+
puts 'Database seeded.'
|
260
|
+
else
|
261
|
+
puts 'No seeds.rb file found.'
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
def clear_cache!
|
266
|
+
@cache = {} if instance_variable_defined?(:@cache)
|
267
|
+
cache_file = File.join(Dir.pwd, 'tmp', 'config_cache.yml')
|
268
|
+
File.delete(cache_file) if File.exist?(cache_file)
|
269
|
+
puts 'Cache cleared.'
|
270
|
+
end
|
271
|
+
|
272
|
+
def generate_docs!
|
273
|
+
docs_path = File.join(Dir.pwd, 'docs')
|
274
|
+
Dir.mkdir(docs_path) unless Dir.exist?(docs_path)
|
275
|
+
File.write(File.join(docs_path, 'README.md'), "# Rubelith Documentation\n\nAuto-generated docs.")
|
276
|
+
puts 'Documentation generated.'
|
277
|
+
end
|
278
|
+
|
279
|
+
def trigger_event!(event_name, data = {})
|
280
|
+
@events ||= []
|
281
|
+
@events << { name: event_name, data: data, timestamp: Time.now }
|
282
|
+
puts "Event '#{event_name}' triggered."
|
283
|
+
end
|
284
|
+
|
285
|
+
require "rack" # rubocop:disable Style/StringLiterals
|
286
|
+
require "zeitwerk" # rubocop:disable Style/StringLiterals
|
287
|
+
require_relative "../../core/router" # rubocop:disable Style/StringLiterals
|
288
|
+
|
289
|
+
module Rubelith
|
290
|
+
class Application # rubocop:disable Metrics/ClassLength,Style/Documentation
|
291
|
+
# LithBlade rendering
|
292
|
+
def render_lithblade(template_path, context = {})
|
293
|
+
LithBlade.render(template_path, context)
|
294
|
+
end
|
295
|
+
# --- Powerful Middleware System ---
|
296
|
+
class MiddlewareStack
|
297
|
+
def initialize
|
298
|
+
@stack = []
|
299
|
+
end
|
300
|
+
def use(mw) # rubocop:disable Layout/EmptyLineBetweenDefs,Naming/MethodParameterName
|
301
|
+
@stack << mw
|
302
|
+
end
|
303
|
+
def call(env) # rubocop:disable Layout/EmptyLineBetweenDefs
|
304
|
+
@stack.reverse.inject(->(e) { Rubelith.application.router.dispatch(e) }) do |app, mw|
|
305
|
+
mw.respond_to?(:call) ? ->(e) { mw.call(e, app) } : app
|
306
|
+
end.call(env)
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
# --- Powerful Auth Functionality ---
|
311
|
+
require 'jwt'
|
312
|
+
require 'bcrypt'
|
313
|
+
|
314
|
+
def generate_jwt(payload, secret = ENV['RUBELITH_JWT_SECRET'])
|
315
|
+
raise 'JWT secret must be set in ENV for production!' unless secret && secret != 'rubelith_secret'
|
316
|
+
JWT.encode(payload, secret, 'HS256')
|
317
|
+
end
|
318
|
+
|
319
|
+
def decode_jwt(token, secret = ENV['RUBELITH_JWT_SECRET'])
|
320
|
+
return nil unless secret && secret != 'rubelith_secret'
|
321
|
+
JWT.decode(token, secret, true, algorithm: 'HS256')[0]
|
322
|
+
rescue JWT::DecodeError
|
323
|
+
nil
|
324
|
+
end
|
325
|
+
|
326
|
+
def hash_password(password)
|
327
|
+
BCrypt::Password.create(password)
|
328
|
+
end
|
329
|
+
|
330
|
+
def valid_password?(hash, password)
|
331
|
+
BCrypt::Password.new(hash) == password
|
332
|
+
end
|
333
|
+
|
334
|
+
def current_user(env)
|
335
|
+
token = env['HTTP_AUTHORIZATION']&.split(' ')&.last
|
336
|
+
payload = decode_jwt(token)
|
337
|
+
@models['User']&.find_by_id(payload['user_id']) if payload && @models['User']
|
338
|
+
end
|
339
|
+
|
340
|
+
def authorize!(env, roles: [:user])
|
341
|
+
user = current_user(env)
|
342
|
+
return false unless user
|
343
|
+
|
344
|
+
(user.roles & roles).any?
|
345
|
+
end
|
346
|
+
|
347
|
+
# --- Powerful Sass Engine Integration ---
|
348
|
+
require 'sassc'
|
349
|
+
|
350
|
+
def compile_sass(scss_path)
|
351
|
+
# Validate path to prevent directory traversal
|
352
|
+
abs_path = File.expand_path(scss_path, Dir.pwd)
|
353
|
+
return '' unless abs_path.start_with?(File.expand_path('public/', Dir.pwd))
|
354
|
+
engine = SassC::Engine.new(File.read(abs_path), syntax: :scss)
|
355
|
+
engine.render
|
356
|
+
end
|
357
|
+
|
358
|
+
def serve_sass(env)
|
359
|
+
req = Rack::Request.new(env)
|
360
|
+
if req.path_info.end_with?('.css')
|
361
|
+
scss_path = File.expand_path(File.join('public', req.path_info.sub(/\.css$/, '.scss')), Dir.pwd)
|
362
|
+
if File.exist?(scss_path)
|
363
|
+
css = compile_sass(scss_path)
|
364
|
+
return [200, { 'Content-Type' => 'text/css' }, [css]]
|
365
|
+
end
|
366
|
+
end
|
367
|
+
nil
|
368
|
+
end
|
369
|
+
|
370
|
+
# Sass middleware is now optional. To enable, add:
|
371
|
+
# use lambda { |env, app| result = serve_sass(env); result || app.call(env) }
|
372
|
+
def t(key, locale: :en)
|
373
|
+
require 'yaml'
|
374
|
+
@translations ||= {}
|
375
|
+
unless @translations[locale]
|
376
|
+
path = File.expand_path("../../config/locales/#{locale}.yml", __dir__)
|
377
|
+
@translations[locale] = File.exist?(path) ? YAML.load_file(path) : {}
|
378
|
+
end
|
379
|
+
@translations[locale][key.to_s] || key.to_s
|
380
|
+
end
|
381
|
+
|
382
|
+
def register_plugin(plugin)
|
383
|
+
@plugins ||= []
|
384
|
+
@plugins << plugin
|
385
|
+
plugin.load(self) if plugin.respond_to?(:load)
|
386
|
+
end
|
387
|
+
|
388
|
+
def run_tests
|
389
|
+
require 'minitest'
|
390
|
+
puts 'Running tests...'
|
391
|
+
Minitest.run
|
392
|
+
end
|
393
|
+
|
394
|
+
def cache(key, value = nil, expires_in: 3600) # rubocop:disable Metrics/MethodLength
|
395
|
+
require 'monitor'
|
396
|
+
@cache_mutex ||= Monitor.new
|
397
|
+
@cache ||= {}
|
398
|
+
@cache_mutex.synchronize do
|
399
|
+
if value
|
400
|
+
@cache[key] = { value: value, expires_at: Time.now + expires_in }
|
401
|
+
else
|
402
|
+
entry = @cache[key]
|
403
|
+
if entry && entry[:expires_at] > Time.now
|
404
|
+
entry[:value]
|
405
|
+
else
|
406
|
+
@cache.delete(key)
|
407
|
+
nil
|
408
|
+
end
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
def cluster(nodes)
|
414
|
+
@cluster_nodes ||= []
|
415
|
+
@cluster_nodes.concat(nodes).uniq!
|
416
|
+
end
|
417
|
+
|
418
|
+
def log(message, level: :info)
|
419
|
+
levels = { debug: 0, info: 1, warn: 2, error: 3 }
|
420
|
+
@log_level ||= :debug
|
421
|
+
return unless levels[level] >= levels[@log_level]
|
422
|
+
|
423
|
+
puts "[#{level.upcase}] #{Time.now}: #{message}"
|
424
|
+
end
|
425
|
+
|
426
|
+
def monitor(event, data = {})
|
427
|
+
@monitor_events ||= []
|
428
|
+
@monitor_events << { event: event, data: data, timestamp: Time.now }
|
429
|
+
# Integrate with external monitoring tools here
|
430
|
+
end
|
431
|
+
|
432
|
+
def track_analytics(event, data = {})
|
433
|
+
@analytics_events ||= []
|
434
|
+
@analytics_events << { event: event, data: data, timestamp: Time.now }
|
435
|
+
# Integrate with analytics providers here
|
436
|
+
end
|
437
|
+
|
438
|
+
def authenticate(req)
|
439
|
+
# Simple session-based authentication
|
440
|
+
session = req.env['rack.session'] || {}
|
441
|
+
user_id = session[:user_id]
|
442
|
+
@models['User']&.find_by_id(user_id) if user_id
|
443
|
+
end
|
444
|
+
|
445
|
+
def authorize(user, action, _resource)
|
446
|
+
# Simple role-based authorization
|
447
|
+
return false unless user
|
448
|
+
|
449
|
+
roles = user.respond_to?(:roles) ? user.roles : [:guest]
|
450
|
+
# Example: allow admin all actions
|
451
|
+
roles.include?(:admin) || action == :read
|
452
|
+
end
|
453
|
+
|
454
|
+
def protect_from_csrf(req, res)
|
455
|
+
session = req.env['rack.session'] || {}
|
456
|
+
token = req.params['_csrf']
|
457
|
+
valid = token && session[:csrf_token] == token
|
458
|
+
res.status = 403 unless valid
|
459
|
+
valid
|
460
|
+
end
|
461
|
+
|
462
|
+
def protect_from_xss(content)
|
463
|
+
# Basic sanitizer: remove script tags
|
464
|
+
content.gsub(%r{<script.*?>.*?</script>}im, '[removed]')
|
465
|
+
end
|
466
|
+
|
467
|
+
def enqueue(job_class, *args)
|
468
|
+
@job_queue ||= Queue.new
|
469
|
+
Thread.new do
|
470
|
+
job_class.new.perform(*args) if job_class.respond_to?(:new)
|
471
|
+
end
|
472
|
+
@job_queue << { job: job_class, args: args }
|
473
|
+
end
|
474
|
+
|
475
|
+
def deliver_mail(mailer_class, method, *args)
|
476
|
+
require 'net/smtp'
|
477
|
+
mailer = mailer_class.new
|
478
|
+
mail = mailer.send(method, *args) if mailer.respond_to?(method)
|
479
|
+
if mail.respond_to?(:to_s)
|
480
|
+
# Example SMTP delivery (customize as needed)
|
481
|
+
Net::SMTP.start('localhost', 25) do |smtp|
|
482
|
+
smtp.send_message mail.to_s, mail.from, mail.to
|
483
|
+
end
|
484
|
+
end
|
485
|
+
mail
|
486
|
+
end
|
487
|
+
|
488
|
+
def migrate! # rubocop:disable Metrics/MethodLength
|
489
|
+
migrations_path = File.expand_path('../../db/migrations', __dir__)
|
490
|
+
return unless Dir.exist?(migrations_path)
|
491
|
+
|
492
|
+
Dir[File.join(migrations_path, '*.rb')].sort.each do |file|
|
493
|
+
require file
|
494
|
+
migration_class = begin
|
495
|
+
Object.const_get(camelize(File.basename(file, '.rb')))
|
496
|
+
rescue StandardError
|
497
|
+
nil
|
498
|
+
end
|
499
|
+
migration_class.new.change if migration_class.respond_to?(:change)
|
500
|
+
end
|
501
|
+
end
|
502
|
+
|
503
|
+
def define_relation(model, relation_type, related_model, options = {})
|
504
|
+
@relations ||= {}
|
505
|
+
@relations[model] ||= []
|
506
|
+
@relations[model] << { type: relation_type, related: related_model, options: options }
|
507
|
+
end
|
508
|
+
|
509
|
+
def validate(model, field, validator, options = {})
|
510
|
+
@validations ||= {}
|
511
|
+
@validations[model] ||= []
|
512
|
+
@validations[model] << { field: field, validator: validator, options: options }
|
513
|
+
end
|
514
|
+
attr_reader :config, :middleware_stack, :models, :router
|
515
|
+
|
516
|
+
def initialize
|
517
|
+
@config = load_config
|
518
|
+
@middleware_stack = MiddlewareStack.new
|
519
|
+
@models = {}
|
520
|
+
@router = Rubelith::Router.new
|
521
|
+
load_models
|
522
|
+
end
|
523
|
+
|
524
|
+
def load_models # rubocop:disable Metrics/MethodLength
|
525
|
+
models_path = File.expand_path('../../app/models', __dir__)
|
526
|
+
return unless Dir.exist?(models_path)
|
527
|
+
|
528
|
+
Dir[File.join(models_path, '*.rb')].each do |file|
|
529
|
+
require file
|
530
|
+
model_name = File.basename(file, '.rb')
|
531
|
+
@models[camelize(model_name)] = begin
|
532
|
+
Object.const_get(camelize(model_name))
|
533
|
+
rescue StandardError
|
534
|
+
nil
|
535
|
+
end
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
def call(env)
|
540
|
+
@middleware_stack.call(env)
|
541
|
+
end
|
542
|
+
|
543
|
+
def route(method, path, to: nil, &block)
|
544
|
+
action = block || to
|
545
|
+
# Wrap block to always accept 3 args (req, res, _params)
|
546
|
+
if action.is_a?(Proc) && action.arity < 3
|
547
|
+
wrapped = ->(req, res, _params = {}) { action.call(req, res) }
|
548
|
+
@router.add_route(method, path, wrapped)
|
549
|
+
else
|
550
|
+
@router.add_route(method, path, action)
|
551
|
+
end
|
552
|
+
end
|
553
|
+
|
554
|
+
def use(mw)
|
555
|
+
@middleware_stack.use(mw)
|
556
|
+
end
|
557
|
+
|
558
|
+
# build_middleware_stack is now handled by MiddlewareStack
|
559
|
+
|
560
|
+
# Removed legacy dispatch/invoke_action. Use @router for all routing.
|
561
|
+
|
562
|
+
def before_dispatch(req, res)
|
563
|
+
# Hook for custom logic before dispatch
|
564
|
+
end
|
565
|
+
|
566
|
+
def after_dispatch(req, res)
|
567
|
+
# Hook for custom logic after dispatch
|
568
|
+
end
|
569
|
+
|
570
|
+
def camelize(str)
|
571
|
+
str.split('_').map(&:capitalize).join
|
572
|
+
end
|
573
|
+
|
574
|
+
private
|
575
|
+
|
576
|
+
require 'yaml'
|
577
|
+
def load_config
|
578
|
+
config_path = File.expand_path('../../config/application.yml', __dir__)
|
579
|
+
config = {}
|
580
|
+
if File.exist?(config_path)
|
581
|
+
config_data = YAML.safe_load(File.read(config_path), permitted_classes: [Symbol]) rescue {}
|
582
|
+
config.merge!(config_data) if config_data.is_a?(Hash)
|
583
|
+
end
|
584
|
+
config
|
585
|
+
end
|
586
|
+
|
587
|
+
def setup_autoloader
|
588
|
+
# Zeitwerk loader is set up in lib/rubelith.rb
|
589
|
+
end
|
590
|
+
end
|
591
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# Crystallis ORM entry point for Rubelith
|
2
|
+
require_relative '../../core/crystallis/base'
|
3
|
+
require_relative '../../core/crystallis/migration'
|
4
|
+
require_relative '../../core/crystallis/relation'
|
5
|
+
require_relative '../../core/crystallis/schema'
|
6
|
+
require_relative '../../core/crystallis/validators'
|
7
|
+
|
8
|
+
module Rubelith
|
9
|
+
module Crystallis
|
10
|
+
# Crystallis ORM is loaded via its core modules
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# lib/rubelith/version.rb
|
2
|
+
|
3
|
+
module Rubelith
|
4
|
+
module Version
|
5
|
+
MAJOR = 0
|
6
|
+
MINOR = 1
|
7
|
+
PATCH = 0
|
8
|
+
PRERELEASE = nil
|
9
|
+
|
10
|
+
def self.to_s
|
11
|
+
[MAJOR, MINOR, PATCH].join('.') + (PRERELEASE ? "-#{PRERELEASE}" : "")
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.bump(type = :patch)
|
15
|
+
case type
|
16
|
+
when :major
|
17
|
+
self.const_set(:MAJOR, MAJOR + 1)
|
18
|
+
self.const_set(:MINOR, 0)
|
19
|
+
self.const_set(:PATCH, 0)
|
20
|
+
when :minor
|
21
|
+
self.const_set(:MINOR, MINOR + 1)
|
22
|
+
self.const_set(:PATCH, 0)
|
23
|
+
when :patch
|
24
|
+
self.const_set(:PATCH, PATCH + 1)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.changelog
|
29
|
+
@changelog ||= []
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.add_change(description)
|
33
|
+
changelog << { version: to_s, description: description, timestamp: Time.now }
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.upgrade_to(version)
|
37
|
+
# Placeholder for upgrade logic
|
38
|
+
# Example: upgrade_to('1.2.0')
|
39
|
+
# Would run migrations, update configs, etc.
|
40
|
+
"Upgraded to #{version}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.downgrade_to(version)
|
44
|
+
# Placeholder for downgrade logic
|
45
|
+
# Example: downgrade_to('0.9.0')
|
46
|
+
# Would rollback migrations, restore configs, etc.
|
47
|
+
"Downgraded to #{version}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
VERSION = Version.to_s
|
52
|
+
end
|