railst 0.2.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.
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railst
4
+ module Environment
5
+ def self.toolchains_env
6
+ env = ENV.to_h
7
+
8
+ # Dynamically detect gem user bin, cargo bin, and local bin
9
+ gem_bin = begin
10
+ user_dir = `ruby -r rubygems -e "puts Gem.user_dir" 2>/dev/null`.strip
11
+ File.join(user_dir, "bin") unless user_dir.empty?
12
+ rescue StandardError
13
+ nil
14
+ end
15
+
16
+ cargo_bin = File.expand_path("~/.cargo/bin")
17
+ local_bin = File.expand_path("~/.local/bin")
18
+
19
+ current_paths = env.fetch("PATH", "").split(File::PATH_SEPARATOR)
20
+ [ cargo_bin, local_bin, gem_bin ].compact.each do |p|
21
+ current_paths.unshift(p) if File.directory?(p) && !current_paths.include?(p)
22
+ end
23
+ env["PATH"] = current_paths.join(File::PATH_SEPARATOR)
24
+
25
+ # Automatic LLVM / Clang detection for bindgen & rb-sys
26
+ unless env["LIBCLANG_PATH"]
27
+ candidate_lib_paths = [
28
+ File.expand_path("~/.local/lib"),
29
+ "/usr/lib/llvm-23/lib",
30
+ "/usr/lib/llvm-22/lib",
31
+ "/usr/lib/llvm-21/lib",
32
+ "/usr/lib/llvm-20/lib",
33
+ "/usr/lib/llvm-19/lib",
34
+ "/usr/lib/llvm-18/lib",
35
+ "/usr/lib",
36
+ "/usr/local/lib"
37
+ ]
38
+ found_lib = candidate_lib_paths.find do |dir|
39
+ File.directory?(dir) && !Dir[File.join(dir, "libclang.so*")].empty?
40
+ end
41
+ env["LIBCLANG_PATH"] = found_lib if found_lib
42
+ end
43
+
44
+ unless env["BINDGEN_EXTRA_CLANG_ARGS"]
45
+ candidate_include_paths = [
46
+ File.expand_path("~/.local/lib/clang/23/include"),
47
+ "/usr/lib/clang/23/include",
48
+ "/usr/lib/clang/22/include",
49
+ "/usr/lib/clang/21/include",
50
+ "/usr/lib/clang/20/include",
51
+ "/usr/lib/clang/19/include",
52
+ "/usr/lib/clang/18/include"
53
+ ]
54
+ found_inc = candidate_include_paths.find { |dir| File.directory?(dir) }
55
+ env["BINDGEN_EXTRA_CLANG_ARGS"] = "-I#{found_inc}" if found_inc
56
+ end
57
+
58
+ # Quiet down noisy glib / libvips system module warnings
59
+ env["VIPS_WARNING"] = "0"
60
+ env["G_MESSAGES_DEBUG"] = "none"
61
+
62
+ env
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,503 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "fileutils"
5
+
6
+ module Railst
7
+ module Templates
8
+ def self.inject_into(target_dir, source_project_root = nil)
9
+ target_path = Pathname.new(target_dir)
10
+
11
+ # 1. Check for template in monorepo packages/template or source root
12
+ monorepo_template = nil
13
+ if source_project_root
14
+ candidates = [
15
+ source_project_root.join("packages/template"),
16
+ source_project_root
17
+ ]
18
+ monorepo_template = candidates.find { |c| c.join("ext/rust_backend").exist? }
19
+ end
20
+
21
+ if monorepo_template
22
+ # Copy backend & support files from existing monorepo template
23
+ copy_tree(monorepo_template.join("ext/rust_backend"), target_path.join("ext/rust_backend"))
24
+ copy_file_safe(monorepo_template.join("app/models/concerns/rust_backed.rb"), target_path.join("app/models/concerns/rust_backed.rb"))
25
+ copy_file_safe(monorepo_template.join("config/initializers/rust_backend.rb"), target_path.join("config/initializers/rust_backend.rb"))
26
+ copy_file_safe(monorepo_template.join("lib/rust_backend.rb"), target_path.join("lib/rust_backend.rb"))
27
+ copy_file_safe(monorepo_template.join("lib/tasks/rust.rake"), target_path.join("lib/tasks/rust.rake"))
28
+ copy_tree(monorepo_template.join("lib/generators/rust"), target_path.join("lib/generators/rust"))
29
+ else
30
+ # Self-contained embedded template generation
31
+ write_embedded_templates(target_path)
32
+ end
33
+
34
+ # Update config/application.rb for autoloading
35
+ app_rb = target_path.join("config/application.rb")
36
+ if app_rb.exist?
37
+ code = app_rb.read
38
+ autoload_line = "config.autoload_lib(ignore: %w[assets tasks generators rust_backend])"
39
+ unless code.include?(autoload_line)
40
+ code.sub!(/class Application < Rails::Application\n/) do |m|
41
+ "#{m} #{autoload_line}\n"
42
+ end
43
+ app_rb.write(code)
44
+ end
45
+ end
46
+ end
47
+
48
+ def self.copy_tree(src, dst)
49
+ return unless src.exist?
50
+
51
+ FileUtils.mkdir_p(dst)
52
+ FileUtils.cp_r(File.join(src, "."), dst)
53
+ FileUtils.rm_rf(File.join(dst, "target"))
54
+ end
55
+
56
+ def self.copy_file_safe(src, dst)
57
+ return unless src.exist?
58
+
59
+ FileUtils.mkdir_p(dst.parent)
60
+ FileUtils.cp(src, dst)
61
+ end
62
+
63
+ def self.write_embedded_templates(target_path)
64
+ files = {
65
+ "ext/rust_backend/Cargo.toml" => cargo_toml_content,
66
+ "ext/rust_backend/extconf.rb" => extconf_rb_content,
67
+ "ext/rust_backend/src/lib.rs" => lib_rs_content,
68
+ "ext/rust_backend/src/gvl.rs" => gvl_rs_content,
69
+ "ext/rust_backend/src/dynamic_hash.rs" => dynamic_hash_rs_content,
70
+ "ext/rust_backend/src/data_processor.rs" => data_processor_rs_content,
71
+ "app/models/concerns/rust_backed.rb" => rust_backed_rb_content,
72
+ "config/initializers/rust_backend.rb" => initializer_content,
73
+ "lib/rust_backend.rb" => loader_content,
74
+ "lib/tasks/rust.rake" => rake_task_content
75
+ }
76
+
77
+ files.each do |rel_path, content|
78
+ full_path = target_path.join(rel_path)
79
+ FileUtils.mkdir_p(full_path.parent)
80
+ full_path.write(content)
81
+ end
82
+ end
83
+
84
+ def self.cargo_toml_content
85
+ <<~TOML
86
+ [package]
87
+ name = "rust_backend"
88
+ version = "0.1.0"
89
+ edition = "2021"
90
+
91
+ [lib]
92
+ crate-type = ["cdylib"]
93
+
94
+ [dependencies]
95
+ magnus = "0.9"
96
+ rb-sys = "0.9"
97
+ rayon = "1.10"
98
+ serde = { version = "1.0", features = ["derive"] }
99
+ serde_json = "1.0"
100
+ TOML
101
+ end
102
+
103
+ def self.extconf_rb_content
104
+ <<~RUBY
105
+ # frozen_string_literal: true
106
+
107
+ require "mkmf"
108
+ require "rb_sys/mkmf"
109
+
110
+ create_rust_makefile("rust_backend/rust_backend")
111
+ RUBY
112
+ end
113
+
114
+ def self.lib_rs_content
115
+ <<~RUST
116
+ use magnus::{Error, Ruby};
117
+
118
+ pub mod dynamic_hash;
119
+ pub mod data_processor;
120
+ pub mod gvl;
121
+
122
+ #[magnus::init]
123
+ fn init(ruby: &Ruby) -> Result<(), Error> {
124
+ let module = ruby.define_module("RustBackend")?;
125
+
126
+ module.define_singleton_method(
127
+ "process_batch",
128
+ magnus::function!(data_processor::process_batch, 2),
129
+ )?;
130
+
131
+ module.define_singleton_method(
132
+ "aggregate_metrics",
133
+ magnus::function!(data_processor::aggregate_metrics, 2),
134
+ )?;
135
+
136
+ module.define_singleton_method(
137
+ "transform_record",
138
+ magnus::function!(data_processor::transform_record, 2),
139
+ )?;
140
+
141
+ Ok(())
142
+ }
143
+ RUST
144
+ end
145
+
146
+ def self.gvl_rs_content
147
+ <<~RUST
148
+ use magnus::rb_sys::rb_thread_call_without_gvl;
149
+ use std::os::raw::c_void;
150
+
151
+ extern "C" fn nogvl_trampoline<F, R>(ptr: *mut c_void) -> *mut c_void
152
+ where
153
+ F: FnOnce() -> R,
154
+ {
155
+ let f = unsafe { Box::from_raw(ptr as *mut F) };
156
+ let result = f();
157
+ Box::into_raw(Box::new(result)) as *mut c_void
158
+ }
159
+
160
+ pub fn call_without_gvl<F, R>(f: F) -> R
161
+ where
162
+ F: FnOnce() -> R + Send,
163
+ R: Send,
164
+ {
165
+ let boxed_f = Box::new(f);
166
+ let ptr = Box::into_raw(boxed_f) as *mut c_void;
167
+
168
+ let res_ptr = unsafe {
169
+ rb_thread_call_without_gvl(
170
+ Some(nogvl_trampoline::<F, R>),
171
+ ptr,
172
+ None,
173
+ std::ptr::null_mut(),
174
+ )
175
+ };
176
+
177
+ let boxed_res = unsafe { Box::from_raw(res_ptr as *mut R) };
178
+ *boxed_res
179
+ }
180
+ RUST
181
+ end
182
+
183
+ def self.dynamic_hash_rs_content
184
+ <<~RUST
185
+ use magnus::{value::ReprValue, Error, RArray, RHash, Ruby, Symbol, Value};
186
+ use serde_json::{Map, Number, Value as JsonValue};
187
+
188
+ pub fn rval_to_json(val: Value) -> JsonValue {
189
+ if val.is_nil() {
190
+ return JsonValue::Null;
191
+ }
192
+ if let Ok(b) = bool::try_convert(val) {
193
+ return JsonValue::Bool(b);
194
+ }
195
+ if let Ok(i) = i64::try_convert(val) {
196
+ return JsonValue::Number(Number::from(i));
197
+ }
198
+ if let Ok(f) = f64::try_convert(val) {
199
+ if let Some(n) = Number::from_f64(f) {
200
+ return JsonValue::Number(n);
201
+ }
202
+ }
203
+ if let Ok(s) = String::try_convert(val) {
204
+ return JsonValue::String(s);
205
+ }
206
+ if let Ok(sym) = Symbol::try_convert(val) {
207
+ if let Ok(s) = sym.name() {
208
+ return JsonValue::String(s.to_string());
209
+ }
210
+ }
211
+ if let Ok(hash) = RHash::try_convert(val) {
212
+ let mut map = Map::new();
213
+ let _ = hash.each(|k: Value, v: Value| {
214
+ let key_str = if let Ok(s) = String::try_convert(k) {
215
+ s
216
+ } else if let Ok(sym) = Symbol::try_convert(k) {
217
+ sym.name().map(|n| n.to_string()).unwrap_or_else(|_| k.to_string())
218
+ } else {
219
+ k.to_string()
220
+ };
221
+ map.insert(key_str, rval_to_json(v));
222
+ Ok(())
223
+ });
224
+ return JsonValue::Object(map);
225
+ }
226
+ if let Ok(arr) = RArray::try_convert(val) {
227
+ let mut vec = Vec::with_capacity(arr.len());
228
+ for item in arr.into_iter() {
229
+ vec.push(rval_to_json(item));
230
+ }
231
+ return JsonValue::Array(vec);
232
+ }
233
+ JsonValue::String(val.to_string())
234
+ }
235
+
236
+ pub fn json_to_rval(ruby: &Ruby, val: &JsonValue) -> Result<Value, Error> {
237
+ match val {
238
+ JsonValue::Null => Ok(ruby.qnil().as_value()),
239
+ JsonValue::Bool(b) => Ok(ruby.into_value(*b)),
240
+ JsonValue::Number(n) => {
241
+ if let Some(i) = n.as_i64() {
242
+ Ok(ruby.into_value(i))
243
+ } else if let Some(f) = n.as_f64() {
244
+ Ok(ruby.into_value(f))
245
+ } else {
246
+ Ok(ruby.into_value(n.to_string()))
247
+ }
248
+ }
249
+ JsonValue::String(s) => Ok(ruby.into_value(s.as_str())),
250
+ JsonValue::Array(arr) => {
251
+ let rarr = ruby.ary_new_capa(arr.len());
252
+ for item in arr {
253
+ rarr.push(json_to_rval(ruby, item)?)?;
254
+ }
255
+ Ok(rarr.as_value())
256
+ }
257
+ JsonValue::Object(map) => {
258
+ let rhash = ruby.hash_new();
259
+ for (k, v) in map {
260
+ let rkey = ruby.into_value(k.as_str());
261
+ let rval = json_to_rval(ruby, v)?;
262
+ rhash.aset(rkey, rval)?;
263
+ }
264
+ Ok(rhash.as_value())
265
+ }
266
+ }
267
+ }
268
+ RUST
269
+ end
270
+
271
+ def self.data_processor_rs_content
272
+ <<~RUST
273
+ use crate::dynamic_hash::{json_to_rval, rval_to_json};
274
+ use crate::gvl::call_without_gvl;
275
+ use magnus::{value::ReprValue, Error, RArray, RHash, Ruby, Value};
276
+ use rayon::prelude::*;
277
+ use serde_json::{json, Value as JsonValue};
278
+
279
+ pub fn transform_record(ruby: &Ruby, record: RHash, config: RHash) -> Result<Value, Error> {
280
+ let mut raw_record = rval_to_json(record.as_value());
281
+ let raw_config = rval_to_json(config.as_value());
282
+
283
+ let field = raw_config.get("compute_field").and_then(|v| v.as_str()).unwrap_or("value").to_string();
284
+ let mult = raw_config.get("multiplier").and_then(|v| v.as_f64()).unwrap_or(1.0);
285
+
286
+ if let JsonValue::Object(ref mut map) = raw_record {
287
+ let current_val = map.get(&field).and_then(|v| v.as_f64()).unwrap_or(0.0);
288
+ map.insert(format!("{}_computed", field), json!(current_val * mult));
289
+ map.insert("processed_by".to_string(), json!("rust_magnus"));
290
+ }
291
+
292
+ json_to_rval(ruby, &raw_record)
293
+ }
294
+
295
+ pub fn process_batch(ruby: &Ruby, records: RArray, config: RHash) -> Result<Value, Error> {
296
+ let raw_records: Vec<JsonValue> = records.into_iter().map(rval_to_json).collect();
297
+ let raw_config = rval_to_json(config.as_value());
298
+
299
+ let compute_field = raw_config
300
+ .get("compute_field")
301
+ .and_then(|v| v.as_str())
302
+ .unwrap_or("value")
303
+ .to_string();
304
+
305
+ let multiplier = raw_config
306
+ .get("multiplier")
307
+ .and_then(|v| v.as_f64())
308
+ .unwrap_or(1.0);
309
+
310
+ let offset = raw_config
311
+ .get("offset")
312
+ .and_then(|v| v.as_f64())
313
+ .unwrap_or(0.0);
314
+
315
+ let processed_records = call_without_gvl(move || {
316
+ raw_records
317
+ .into_par_iter()
318
+ .map(|mut rec| {
319
+ if let JsonValue::Object(ref mut map) = rec {
320
+ let current_val = map
321
+ .get(&compute_field)
322
+ .and_then(|v| {
323
+ v.as_f64().or_else(|| {
324
+ v.as_str().and_then(|s| s.parse::<f64>().ok())
325
+ })
326
+ })
327
+ .unwrap_or(0.0);
328
+
329
+ let computed = (current_val * multiplier) + offset;
330
+ map.insert(format!("{}_computed", compute_field), json!(computed));
331
+ map.insert("processed_by".to_string(), json!("rust_rayon_parallel"));
332
+ map.insert("gvl_released".to_string(), json!(true));
333
+ }
334
+ rec
335
+ })
336
+ .collect::<Vec<JsonValue>>()
337
+ });
338
+
339
+ let result_json = JsonValue::Array(processed_records);
340
+ json_to_rval(ruby, &result_json)
341
+ }
342
+
343
+ pub fn aggregate_metrics(ruby: &Ruby, records: RArray, metric_field: String) -> Result<Value, Error> {
344
+ let raw_records: Vec<JsonValue> = records.into_iter().map(rval_to_json).collect();
345
+ let target_field = metric_field.clone();
346
+
347
+ let (count, sum, min, max) = call_without_gvl(move || {
348
+ let values: Vec<f64> = raw_records
349
+ .into_par_iter()
350
+ .filter_map(|rec| {
351
+ if let JsonValue::Object(map) = rec {
352
+ map.get(&target_field).and_then(|v| {
353
+ v.as_f64().or_else(|| v.as_str().and_then(|s| s.parse::<f64>().ok()))
354
+ })
355
+ } else {
356
+ None
357
+ }
358
+ })
359
+ .collect();
360
+
361
+ if values.is_empty() {
362
+ (0, 0.0, 0.0, 0.0)
363
+ } else {
364
+ let count = values.len();
365
+ let sum: f64 = values.par_iter().sum();
366
+ let min = values.par_iter().copied().reduce(|| f64::INFINITY, f64::min);
367
+ let max = values.par_iter().copied().reduce(|| f64::NEG_INFINITY, f64::max);
368
+ (count, sum, min, max)
369
+ }
370
+ });
371
+
372
+ let avg = if count > 0 { sum / count as f64 } else { 0.0 };
373
+
374
+ let summary = json!({
375
+ "field": metric_field,
376
+ "count": count,
377
+ "sum": sum,
378
+ "avg": avg,
379
+ "min": min,
380
+ "max": max,
381
+ "parallel_engine": "rayon"
382
+ });
383
+
384
+ json_to_rval(ruby, &summary)
385
+ }
386
+ RUST
387
+ end
388
+
389
+ def self.rust_backed_rb_content
390
+ <<~RUBY
391
+ # frozen_string_literal: true
392
+
393
+ module RustBacked
394
+ extend ActiveSupport::Concern
395
+
396
+ included do
397
+ # ActiveSupport concern for models integrating with Magnus Rust extension
398
+ end
399
+
400
+ def process_with_rust
401
+ RustBackend.transform_record(attributes, { "multiplier" => 1.0 })
402
+ end
403
+
404
+ class_methods do
405
+ # Batch process dynamic records across CPU cores with Rayon and GVL released
406
+ def rust_batch_process(scope_or_hashes, config = {})
407
+ raw = if scope_or_hashes.respond_to?(:map)
408
+ scope_or_hashes.map { |r| r.respond_to?(:attributes) ? r.attributes : r }
409
+ else
410
+ scope_or_hashes
411
+ end
412
+ RustBackend.process_batch(raw, config.stringify_keys)
413
+ end
414
+
415
+ # High speed parallel statistical aggregation across CPU cores (GVL released)
416
+ def rust_aggregate(scope_or_hashes, field)
417
+ raw = if scope_or_hashes.respond_to?(:map)
418
+ scope_or_hashes.map { |r| r.respond_to?(:attributes) ? r.attributes : r }
419
+ else
420
+ scope_or_hashes
421
+ end
422
+ RustBackend.aggregate_metrics(raw, field.to_s)
423
+ end
424
+ end
425
+ end
426
+ RUBY
427
+ end
428
+
429
+ def self.initializer_content
430
+ <<~RUBY
431
+ # frozen_string_literal: true
432
+
433
+ require "rust_backend"
434
+
435
+ Rails.logger.info "[RustBackend] Magnus native Rust backend loaded successfully."
436
+ RUBY
437
+ end
438
+
439
+ def self.loader_content
440
+ <<~RUBY
441
+ # frozen_string_literal: true
442
+
443
+ module RustBackend
444
+ begin
445
+ require_relative "rust_backend/rust_backend"
446
+ rescue LoadError
447
+ require_relative "../ext/rust_backend/rust_backend"
448
+ end
449
+ end
450
+ RUBY
451
+ end
452
+
453
+ def self.rake_task_content
454
+ <<~RUBY
455
+ # frozen_string_literal: true
456
+
457
+ def rust_build_env
458
+ cargo_path = File.expand_path("~/.cargo/bin")
459
+ env = {
460
+ "LIBCLANG_PATH" => ENV["LIBCLANG_PATH"] || File.expand_path("~/.local/lib"),
461
+ "BINDGEN_EXTRA_CLANG_ARGS" => ENV["BINDGEN_EXTRA_CLANG_ARGS"] || "-I\#{File.expand_path('~/.local/lib/clang/23/include')}"
462
+ }
463
+ env["PATH"] = "\#{cargo_path}:\#{ENV['PATH']}" unless ENV["PATH"].include?(cargo_path)
464
+ env
465
+ end
466
+
467
+ namespace :rust do
468
+ desc "Build the Magnus Rust backend extension"
469
+ task :build do
470
+ puts "[RustBackend] Compiling Rust crate..."
471
+ crate_dir = Rails.root.join("ext/rust_backend")
472
+ target_dir = Rails.root.join("lib/rust_backend")
473
+ FileUtils.mkdir_p(target_dir)
474
+
475
+ system(rust_build_env, "cargo build --release", chdir: crate_dir.to_s, exception: true)
476
+
477
+ source_so = crate_dir.join("target/release/librust_backend.so")
478
+ dest_so = target_dir.join("rust_backend.so")
479
+
480
+ FileUtils.cp(source_so, dest_so)
481
+ puts "[RustBackend] Built and copied extension to \#{dest_so}"
482
+ end
483
+
484
+ desc "Run native Rust tests in ext/rust_backend"
485
+ task :test do
486
+ crate_dir = Rails.root.join("ext/rust_backend")
487
+ system(rust_build_env, "cargo test", chdir: crate_dir.to_s, exception: true)
488
+ end
489
+
490
+ desc "Clean compiled Rust artifacts"
491
+ task :clean do
492
+ crate_dir = Rails.root.join("ext/rust_backend")
493
+ system(rust_build_env, "cargo clean", chdir: crate_dir.to_s)
494
+ FileUtils.rm_f(Rails.root.join("lib/rust_backend/rust_backend.so"))
495
+ puts "[RustBackend] Cleaned Rust build artifacts."
496
+ end
497
+ end
498
+
499
+ task test: "rust:build"
500
+ RUBY
501
+ end
502
+ end
503
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railst
4
+ module Terminal
5
+ def self.color?
6
+ $stdout.tty? && ENV["NO_COLOR"].nil?
7
+ end
8
+
9
+ def self.badge(text, bg: :green, fg: :black)
10
+ return "[#{text}]" unless color?
11
+
12
+ bg_code = case bg
13
+ when :green then "42"
14
+ when :red then "41"
15
+ when :yellow then "43"
16
+ when :blue then "44"
17
+ when :cyan then "46"
18
+ when :magenta then "45"
19
+ else "40"
20
+ end
21
+
22
+ fg_code = case fg
23
+ when :black then "30"
24
+ when :white then "97"
25
+ when :bright_white then "1;37"
26
+ else "37"
27
+ end
28
+
29
+ "\e[#{fg_code};#{bg_code}m\e[1m #{text} \e[0m"
30
+ end
31
+
32
+ def self.bold(text)
33
+ color? ? "\e[1m#{text}\e[0m" : text.to_s
34
+ end
35
+
36
+ def self.dim(text)
37
+ color? ? "\e[90m#{text}\e[0m" : text.to_s
38
+ end
39
+
40
+ def self.cyan(text)
41
+ color? ? "\e[36m#{text}\e[0m" : text.to_s
42
+ end
43
+
44
+ def self.bold_cyan(text)
45
+ color? ? "\e[1;36m#{text}\e[0m" : text.to_s
46
+ end
47
+
48
+ def self.yellow(text)
49
+ color? ? "\e[33m#{text}\e[0m" : text.to_s
50
+ end
51
+
52
+ def self.bold_yellow(text)
53
+ color? ? "\e[1;33m#{text}\e[0m" : text.to_s
54
+ end
55
+
56
+ def self.red(text)
57
+ color? ? "\e[31m#{text}\e[0m" : text.to_s
58
+ end
59
+
60
+ def self.bold_red(text)
61
+ color? ? "\e[1;31m#{text}\e[0m" : text.to_s
62
+ end
63
+
64
+ def self.green(text)
65
+ color? ? "\e[32m#{text}\e[0m" : text.to_s
66
+ end
67
+
68
+ def self.bold_green(text)
69
+ color? ? "\e[1;32m#{text}\e[0m" : text.to_s
70
+ end
71
+
72
+ def self.magenta(text)
73
+ color? ? "\e[35m#{text}\e[0m" : text.to_s
74
+ end
75
+
76
+ def self.bold_magenta(text)
77
+ color? ? "\e[1;35m#{text}\e[0m" : text.to_s
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railst
4
+ VERSION = "0.2.0"
5
+ end
data/lib/railst.rb ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "railst/version"
4
+ require_relative "railst/terminal"
5
+ require_relative "railst/environment"
6
+ require_relative "railst/templates"
7
+ require_relative "railst/cli"
8
+
9
+ module Railst
10
+ end