rubyx-py 0.1.1-aarch64-linux → 0.2.1-aarch64-linux
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 +97 -28
- data/docs/assets/logo.png +0 -0
- data/lib/rubyx/3.1/rubyx.so +0 -0
- data/lib/rubyx/3.2/rubyx.so +0 -0
- data/lib/rubyx/3.3/rubyx.so +0 -0
- data/lib/rubyx/3.4/rubyx.so +0 -0
- data/lib/rubyx/railtie.rb +2 -1
- data/lib/rubyx/tasks/rubyx.rake +127 -0
- data/lib/rubyx/version.rb +2 -2
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 25cb549b764a4c233144af01c9fe0fc86709ba1c9c6bbc8ac01a4f60f030ffe6
|
|
4
|
+
data.tar.gz: 812f62a87103e7c52e2503054942645aaa6e9e5f5c0ddf63d20a484c6f9da1b2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ea4882957a0522567d2dbdfe7c9a64b0d7bfa23522928d7bb39d22c763b5d936d348b1dfa762e4197bd67174aa3d2e4a26928f0586c93bc267d7d56ca02a7563
|
|
7
|
+
data.tar.gz: 34b00a5cdcf9347ec8a812ac30ad7e6142ff435768e66d582c0e6125632a637ae136b813f1ffba4af3b3c19779674968e088f5014bfa9383a8c76a9c9be437bc
|
data/README.md
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
+
<img src="docs/assets/logo.png" alt="rubyx-py" width="200">
|
|
4
|
+
|
|
3
5
|
# Rubyx-py
|
|
4
6
|
|
|
5
7
|
**Call Python from Ruby. No microservices, no REST APIs, no serialization overhead.**
|
|
6
8
|
|
|
7
|
-
Powered by Rust for safety and performance. Built for Rails.
|
|
9
|
+
Powered by Rust for safety and performance. Built for Rails. Inspired by [Pythonx](https://github.com/livebook-dev/pythonx)
|
|
8
10
|
|
|
9
|
-
[](https://badge.fury.io/rb/rubyx)
|
|
11
|
+
[](https://badge.fury.io/rb/rubyx-py)
|
|
10
12
|
[](https://github.com/yinho999/rubyx/actions/workflows/ci.yml)
|
|
11
13
|
[](https://opensource.org/licenses/MIT)
|
|
12
14
|
[](https://www.ruby-lang.org)
|
|
13
15
|
[](https://www.rust-lang.org)
|
|
14
|
-
|
|
16
|
+
> *"Rubyx showed that the proxy-object pattern works beautifully for cross-language bridges in Rust, and its magnus + rb-sys architecture is exactly what [Boax](https://intertwingly.net/blog/2026/03/25/Calling-JavaScript-from-Ruby.html) uses."*
|
|
17
|
+
>
|
|
18
|
+
> — [Sam Ruby](https://en.wikipedia.org/wiki/Sam_Ruby), author of *Agile Web Development with Rails*
|
|
15
19
|
</div>
|
|
16
20
|
|
|
17
21
|
---
|
|
@@ -34,15 +38,17 @@ Rubyx.stream(llm.generate("Tell me about Ruby")).each { |token| print token }
|
|
|
34
38
|
# Non-blocking — Ruby stays free while Python works
|
|
35
39
|
future = Rubyx.async_await("model.predict(data)", data: [1, 2, 3])
|
|
36
40
|
do_other_work()
|
|
37
|
-
result = future.
|
|
41
|
+
result = future.await # GVL released during wait, reacquired when ready
|
|
38
42
|
```
|
|
39
43
|
|
|
40
44
|
### Built with non-blocking in mind
|
|
41
45
|
|
|
42
46
|
- **`Rubyx.stream`** / **`Rubyx.nb_stream`** — release Ruby's GVL during iteration, other threads and Fibers keep
|
|
43
47
|
running
|
|
44
|
-
- **`Rubyx.async_await`** — spawns Python on background threads, returns a `Future` immediately
|
|
45
|
-
|
|
48
|
+
- **`Rubyx.async_await`** — spawns Python on background threads, returns a `Future` immediately; `future.await` releases
|
|
49
|
+
the GVL while waiting, reacquires when ready
|
|
50
|
+
- **`Rubyx.await`** — GVL released while waiting; returns native Ruby types for primitives, `RubyxObject` for complex
|
|
51
|
+
Python objects
|
|
46
52
|
|
|
47
53
|
Ideal for LLM streaming, ML inference, data pipelines, and high-concurrency Rails apps.
|
|
48
54
|
|
|
@@ -88,20 +94,27 @@ dependencies = []
|
|
|
88
94
|
### 1. Sync — call a Python function
|
|
89
95
|
|
|
90
96
|
```python
|
|
91
|
-
# app/python/
|
|
92
|
-
def
|
|
93
|
-
return f"Hello, {name}!"
|
|
97
|
+
# app/python/example.py
|
|
98
|
+
def hello(name):
|
|
99
|
+
return f"Hello, {name}! From Python."
|
|
94
100
|
```
|
|
95
101
|
|
|
96
102
|
```ruby
|
|
103
|
+
|
|
97
104
|
class GreetingsController < ApplicationController
|
|
98
|
-
def
|
|
99
|
-
|
|
100
|
-
render json: { message: hello
|
|
105
|
+
def index
|
|
106
|
+
example = Rubyx.import('example')
|
|
107
|
+
render json: { message: example.hello(params[:name] || 'World').to_ruby }
|
|
101
108
|
end
|
|
102
109
|
end
|
|
103
110
|
```
|
|
104
111
|
|
|
112
|
+
```ruby
|
|
113
|
+
Rails.application.routes.draw do
|
|
114
|
+
root "greetings#index"
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
105
118
|
### 2. Streaming — iterate a Python generator
|
|
106
119
|
|
|
107
120
|
```python
|
|
@@ -112,6 +125,7 @@ def count_up(n):
|
|
|
112
125
|
```
|
|
113
126
|
|
|
114
127
|
```ruby
|
|
128
|
+
|
|
115
129
|
class CountController < ApplicationController
|
|
116
130
|
include ActionController::Live
|
|
117
131
|
|
|
@@ -141,14 +155,15 @@ async def delayed_greet(name, seconds=1):
|
|
|
141
155
|
```
|
|
142
156
|
|
|
143
157
|
```ruby
|
|
158
|
+
|
|
144
159
|
class TasksController < ApplicationController
|
|
145
160
|
def show
|
|
146
161
|
tasks = Rubyx.import('tasks')
|
|
147
162
|
|
|
148
163
|
# Non-blocking — returns a Future immediately
|
|
149
|
-
future = Rubyx.async_await(tasks.delayed_greet(params[:name], seconds: 2))
|
|
164
|
+
future = Rubyx.async_await(tasks.delayed_greet(params[:name] || 'World', seconds: 2))
|
|
150
165
|
do_other_work()
|
|
151
|
-
render json: { message: future.
|
|
166
|
+
render json: { message: future.await.to_ruby }
|
|
152
167
|
end
|
|
153
168
|
end
|
|
154
169
|
```
|
|
@@ -263,6 +278,7 @@ end
|
|
|
263
278
|
```
|
|
264
279
|
|
|
265
280
|
```ruby
|
|
281
|
+
|
|
266
282
|
class ChatController < ApplicationController
|
|
267
283
|
include ActionController::Live
|
|
268
284
|
|
|
@@ -317,7 +333,29 @@ Rubyx.eval("f'Hello, {name}!'", name: "World").to_ruby # => "Hello, World!"
|
|
|
317
333
|
Rubyx.eval("max(items)", items: [3, 1, 4, 1, 5]).to_ruby # => 5
|
|
318
334
|
```
|
|
319
335
|
|
|
320
|
-
Supports: Integer, Float, String, Symbol, Bool, nil, Array, Hash, and RubyxObject.
|
|
336
|
+
Supports: Integer, Float, String, Symbol, Bool, nil, Array, Hash, binary String (ASCII-8BIT), and RubyxObject.
|
|
337
|
+
|
|
338
|
+
## Bytes / Binary Data
|
|
339
|
+
|
|
340
|
+
Python `bytes` and `bytearray` convert to Ruby `String` with `ASCII-8BIT` encoding. Ruby binary strings (`.b`) convert to Python `bytes`:
|
|
341
|
+
|
|
342
|
+
```ruby
|
|
343
|
+
# Python bytes → Ruby
|
|
344
|
+
Rubyx.eval("b'hello'").to_ruby # => "hello" (ASCII-8BIT)
|
|
345
|
+
Rubyx.eval("bytearray(b'hello')").to_ruby # => "hello" (ASCII-8BIT)
|
|
346
|
+
|
|
347
|
+
# Ruby → Python
|
|
348
|
+
ctx = Rubyx.context
|
|
349
|
+
ctx.eval("type(data).__name__", data: "hello".b) # => "bytes"
|
|
350
|
+
ctx.eval("type(data).__name__", data: "hello") # => "str"
|
|
351
|
+
|
|
352
|
+
# Roundtrip binary data
|
|
353
|
+
ctx.eval("data", data: "\xff\x00\xfe".b).to_ruby # => "\xFF\x00\xFE" (ASCII-8BIT)
|
|
354
|
+
|
|
355
|
+
# Works with Python stdlib
|
|
356
|
+
ctx.eval("import base64")
|
|
357
|
+
ctx.eval("base64.b64encode(raw)", raw: "Hello".b).to_ruby # => "SGVsbG8=" (ASCII-8BIT)
|
|
358
|
+
```
|
|
321
359
|
|
|
322
360
|
## Python Objects
|
|
323
361
|
|
|
@@ -378,13 +416,13 @@ ctx = Rubyx.context
|
|
|
378
416
|
ctx.eval("import asyncio")
|
|
379
417
|
ctx.eval("async def fetch(url): ...")
|
|
380
418
|
|
|
381
|
-
#
|
|
419
|
+
# GVL released while waiting, reacquired when ready
|
|
382
420
|
result = ctx.await("fetch(url)", url: "https://example.com")
|
|
383
421
|
|
|
384
422
|
# Non-blocking (returns Future)
|
|
385
423
|
future = ctx.async_await("fetch(url)", url: "https://example.com")
|
|
386
424
|
do_other_stuff()
|
|
387
|
-
result = future.
|
|
425
|
+
result = future.await # GVL released during wait, reacquired when ready
|
|
388
426
|
future.ready? # check without blocking
|
|
389
427
|
```
|
|
390
428
|
|
|
@@ -433,17 +471,17 @@ svc.Analyzer([1, 2, 3]).summary.to_ruby # => {"count" => 3, "sum" => 6}
|
|
|
433
471
|
|
|
434
472
|
## API Reference
|
|
435
473
|
|
|
436
|
-
| Method | Description
|
|
437
|
-
|
|
438
|
-
| `Rubyx.uv_init(toml, **opts)` | Setup Python env and initialize
|
|
439
|
-
| `Rubyx.import(name)` | Import a Python module
|
|
440
|
-
| `Rubyx.eval(code, **globals)` | Evaluate Python code
|
|
441
|
-
| `Rubyx.await(code, **globals)` | Run async code (
|
|
442
|
-
| `Rubyx.async_await(code, **globals)` | Run async code (returns Future) |
|
|
443
|
-
| `Rubyx.stream(iterable)` | Stream a Python generator
|
|
444
|
-
| `Rubyx.nb_stream(iterable)` | Non-blocking stream (GVL-aware)
|
|
445
|
-
| `Rubyx.context` | Create isolated Python context
|
|
446
|
-
| `Rubyx.initialized?` | Check if Python is ready
|
|
474
|
+
| Method | Description |
|
|
475
|
+
|--------------------------------------|-----------------------------------------------|
|
|
476
|
+
| `Rubyx.uv_init(toml, **opts)` | Setup Python env and initialize |
|
|
477
|
+
| `Rubyx.import(name)` | Import a Python module |
|
|
478
|
+
| `Rubyx.eval(code, **globals)` | Evaluate Python code |
|
|
479
|
+
| `Rubyx.await(code, **globals)` | Run async code (GVL released while waiting) |
|
|
480
|
+
| `Rubyx.async_await(code, **globals)` | Run async code (non-blocking, returns Future) |
|
|
481
|
+
| `Rubyx.stream(iterable)` | Stream a Python generator |
|
|
482
|
+
| `Rubyx.nb_stream(iterable)` | Non-blocking stream (GVL-aware) |
|
|
483
|
+
| `Rubyx.context` | Create isolated Python context |
|
|
484
|
+
| `Rubyx.initialized?` | Check if Python is ready |
|
|
447
485
|
|
|
448
486
|
| RubyxObject | |
|
|
449
487
|
|--------------------------|-------------------------------|
|
|
@@ -456,6 +494,37 @@ svc.Analyzer([1, 2, 3]).summary.to_ruby # => {"count" => 3, "sum" => 6}
|
|
|
456
494
|
| `.callable?` | Check if callable |
|
|
457
495
|
| `.py_type` | Python type name |
|
|
458
496
|
|
|
497
|
+
## Type Conversion
|
|
498
|
+
|
|
499
|
+
| Python | Ruby | Notes |
|
|
500
|
+
|-----------------------|------------------------------|------------------------|
|
|
501
|
+
| `int` | `Integer` | |
|
|
502
|
+
| `float` | `Float` | |
|
|
503
|
+
| `str` | `String` (UTF-8) | |
|
|
504
|
+
| `bytes` | `String` (ASCII-8BIT) | binary data |
|
|
505
|
+
| `bytearray` | `String` (ASCII-8BIT) | binary data |
|
|
506
|
+
| `bool` | `true` / `false` | |
|
|
507
|
+
| `None` | `nil` | |
|
|
508
|
+
| `list` / `tuple` | `Array` | |
|
|
509
|
+
| `dict` | `Hash` | |
|
|
510
|
+
| `set` / `frozenset` | `Array` | |
|
|
511
|
+
| everything else | `RubyxObject` | proxy to Python object |
|
|
512
|
+
|
|
513
|
+
**Ruby → Python** (via globals/kwargs):
|
|
514
|
+
|
|
515
|
+
| Ruby | Python |
|
|
516
|
+
|--------------------------------|-------------|
|
|
517
|
+
| `Integer` | `int` |
|
|
518
|
+
| `Float` | `float` |
|
|
519
|
+
| `String` (UTF-8) | `str` |
|
|
520
|
+
| `String` (ASCII-8BIT / `.b`) | `bytes` |
|
|
521
|
+
| `true` / `false` | `bool` |
|
|
522
|
+
| `nil` | `None` |
|
|
523
|
+
| `Array` | `list` |
|
|
524
|
+
| `Hash` | `dict` |
|
|
525
|
+
| `Symbol` | `str` |
|
|
526
|
+
| `RubyxObject` | original |
|
|
527
|
+
|
|
459
528
|
## Requirements
|
|
460
529
|
|
|
461
530
|
- Ruby >= 3.0
|
|
Binary file
|
data/lib/rubyx/3.1/rubyx.so
CHANGED
|
Binary file
|
data/lib/rubyx/3.2/rubyx.so
CHANGED
|
Binary file
|
data/lib/rubyx/3.3/rubyx.so
CHANGED
|
Binary file
|
data/lib/rubyx/3.4/rubyx.so
CHANGED
|
Binary file
|
data/lib/rubyx/railtie.rb
CHANGED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
namespace :rubyx do
|
|
2
|
+
desc 'Initialize Python environment (downloads uv and Python if needed)'
|
|
3
|
+
task init: :environment do
|
|
4
|
+
Rubyx::Rails.init!
|
|
5
|
+
puts '[Rubyx] Python environment initialized successfully.'
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
desc 'Check Python environment health'
|
|
9
|
+
task check: :environment do
|
|
10
|
+
puts 'Checking Python environment...'
|
|
11
|
+
puts
|
|
12
|
+
|
|
13
|
+
# Check uv
|
|
14
|
+
system_uv = `which uv 2>/dev/null`.strip
|
|
15
|
+
uv_available = !system_uv.empty? && File.exist?(system_uv)
|
|
16
|
+
puts "uv available: #{uv_available ? "Yes (#{system_uv})" : 'No (will auto-download)'}"
|
|
17
|
+
|
|
18
|
+
begin
|
|
19
|
+
Rubyx::Rails.ensure_initialized!
|
|
20
|
+
puts 'Python initialized: Yes'
|
|
21
|
+
rescue => e
|
|
22
|
+
puts "Python initialized: No (#{e.message})"
|
|
23
|
+
puts
|
|
24
|
+
puts 'Run `rake rubyx:init` to initialize.'
|
|
25
|
+
exit 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
Rubyx.eval('1 + 1')
|
|
30
|
+
puts 'Basic eval: OK'
|
|
31
|
+
rescue => e
|
|
32
|
+
puts "Basic eval: FAILED (#{e.message})"
|
|
33
|
+
exit 1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
begin
|
|
37
|
+
gen = Rubyx.eval("import sys\niter([sys.version.split()[0]])")
|
|
38
|
+
version = Rubyx.stream(gen).first
|
|
39
|
+
puts "Import sys: OK (Python #{version})"
|
|
40
|
+
rescue => e
|
|
41
|
+
puts "Import sys: FAILED (#{e.message})"
|
|
42
|
+
exit 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
puts
|
|
46
|
+
puts 'All checks passed!'
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
desc 'Show Rubyx configuration and status'
|
|
50
|
+
task status: :environment do
|
|
51
|
+
config = Rubyx::Rails.configuration
|
|
52
|
+
|
|
53
|
+
puts 'Rubyx Status'
|
|
54
|
+
puts '=' * 40
|
|
55
|
+
|
|
56
|
+
puts "Initialized: #{Rubyx::Rails.initialized?}"
|
|
57
|
+
puts
|
|
58
|
+
|
|
59
|
+
puts 'Configuration:'
|
|
60
|
+
puts " pyproject_path: #{config.pyproject_path || '(not set)'}"
|
|
61
|
+
puts " pyproject_content: #{config.pyproject_content ? '(inline, %d bytes)' % config.pyproject_content.length : '(not set)'}"
|
|
62
|
+
puts " auto_init: #{config.auto_init}"
|
|
63
|
+
puts " force_reinit: #{config.force_reinit}"
|
|
64
|
+
puts " uv_version: #{config.uv_version}"
|
|
65
|
+
puts " debug: #{config.debug}"
|
|
66
|
+
puts " python_paths: #{config.python_paths.inspect}"
|
|
67
|
+
puts " uv_path: #{config.uv_path || '(auto-download)'}"
|
|
68
|
+
puts " uv_args: #{config.uv_args.inspect}"
|
|
69
|
+
puts
|
|
70
|
+
|
|
71
|
+
if config.pyproject_path
|
|
72
|
+
exists = File.exist?(config.pyproject_path.to_s)
|
|
73
|
+
puts "pyproject.toml exists: #{exists}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
if config.pyproject_path
|
|
77
|
+
venv_dir = File.join(File.dirname(config.pyproject_path.to_s), '.venv')
|
|
78
|
+
puts ".venv exists: #{Dir.exist?(venv_dir)}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
system_uv = `which uv 2>/dev/null`.strip
|
|
82
|
+
puts "System uv: #{!system_uv.empty? ? system_uv : '(not found)'}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
desc 'List installed Python packages'
|
|
86
|
+
task packages: :environment do
|
|
87
|
+
Rubyx::Rails.ensure_initialized!
|
|
88
|
+
|
|
89
|
+
gen = Rubyx.eval(<<~PY)
|
|
90
|
+
import pkg_resources
|
|
91
|
+
packages = sorted([f"{d.project_name}=={d.version}" for d in pkg_resources.working_set])
|
|
92
|
+
iter(packages)
|
|
93
|
+
PY
|
|
94
|
+
|
|
95
|
+
puts 'Installed Python packages:'
|
|
96
|
+
Rubyx.stream(gen).each { |pkg| puts " #{pkg}" }
|
|
97
|
+
rescue => e
|
|
98
|
+
begin
|
|
99
|
+
gen = Rubyx.eval(<<~PY)
|
|
100
|
+
from importlib.metadata import distributions
|
|
101
|
+
packages = sorted([f"{d.metadata['Name']}=={d.metadata['Version']}" for d in distributions()])
|
|
102
|
+
iter(packages)
|
|
103
|
+
PY
|
|
104
|
+
|
|
105
|
+
puts 'Installed Python packages:'
|
|
106
|
+
Rubyx.stream(gen).each { |pkg| puts " #{pkg}" }
|
|
107
|
+
rescue => e2
|
|
108
|
+
puts "Could not list packages: #{e2.message}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
desc 'Clear the Rubyx cache (re-download uv + Python on next init)'
|
|
113
|
+
task clear_cache: :environment do
|
|
114
|
+
cache_dir = File.join(
|
|
115
|
+
ENV.fetch('XDG_CACHE_HOME', File.join(Dir.home, '.cache')),
|
|
116
|
+
'rubyx'
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if Dir.exist?(cache_dir)
|
|
120
|
+
require 'fileutils'
|
|
121
|
+
FileUtils.rm_rf(cache_dir)
|
|
122
|
+
puts "[Rubyx] Cache cleared: #{cache_dir}"
|
|
123
|
+
else
|
|
124
|
+
puts '[Rubyx] No cache directory found.'
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
data/lib/rubyx/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubyx-py
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: aarch64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Naiker
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-05-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake-compiler
|
|
@@ -47,6 +47,7 @@ extensions: []
|
|
|
47
47
|
extra_rdoc_files: []
|
|
48
48
|
files:
|
|
49
49
|
- README.md
|
|
50
|
+
- docs/assets/logo.png
|
|
50
51
|
- ext/rubyx/src/python/sync_adapter.py
|
|
51
52
|
- lib/generators/rubyx/install_generator.rb
|
|
52
53
|
- lib/generators/rubyx/templates/example.py
|
|
@@ -62,6 +63,7 @@ files:
|
|
|
62
63
|
- lib/rubyx/error.rb
|
|
63
64
|
- lib/rubyx/rails.rb
|
|
64
65
|
- lib/rubyx/railtie.rb
|
|
66
|
+
- lib/rubyx/tasks/rubyx.rake
|
|
65
67
|
- lib/rubyx/uv.rb
|
|
66
68
|
- lib/rubyx/version.rb
|
|
67
69
|
homepage: https://github.com/yinho999/rubyx
|