mxrb 0.1.2 → 0.1.4
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/bin/mxrb +458 -10
- data/docs/de-DE/README.md +1 -0
- data/docs/de-DE/oql-sql.md +42 -0
- data/docs/de-DE/project-structure.md +201 -2
- data/docs/de-DE/runtime-ruby.md +104 -0
- data/docs/de-DE/runtime-schema-migrations.md +44 -0
- data/docs/de-DE/validation-matrix.md +42 -4
- data/docs/en-US/README.md +1 -0
- data/docs/en-US/oql-sql.md +41 -0
- data/docs/en-US/project-structure.md +192 -2
- data/docs/en-US/runtime-ruby.md +100 -0
- data/docs/en-US/runtime-schema-migrations.md +41 -0
- data/docs/en-US/validation-matrix.md +39 -4
- data/docs/pt-BR/README.md +1 -0
- data/docs/pt-BR/entity-dsl.md +8 -0
- data/docs/pt-BR/oql-sql.md +41 -0
- data/docs/pt-BR/project-structure.md +197 -2
- data/docs/pt-BR/runtime-ruby.md +108 -0
- data/docs/pt-BR/runtime-schema-migrations.md +40 -0
- data/docs/pt-BR/validation-matrix.md +42 -5
- data/docs/pt-BR/writing.md +28 -0
- data/lib/mxrb/cli/help.rb +426 -0
- data/lib/mxrb/cli/release.rb +137 -0
- data/lib/mxrb/compiler/compatibility_analyzer.rb +1 -1
- data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +8 -1
- data/lib/mxrb/compiler/domain_document_compiler.rb +3 -0
- data/lib/mxrb/compiler/gallery_bundle_compiler.rb +11 -1
- data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +76 -19
- data/lib/mxrb/compiler/legacy_custom_widget_compiler.rb +159 -0
- data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +43 -7
- data/lib/mxrb/compiler/legacy_page_builder.rb +1248 -23
- data/lib/mxrb/compiler/packager.rb +27 -13
- data/lib/mxrb/compiler/page_bundle_compiler.rb +13 -4
- data/lib/mxrb/compiler/portable_packager.rb +16 -11
- data/lib/mxrb/compiler/web_bundle_builder.rb +5 -1
- data/lib/mxrb/compiler/web_shell_materializer.rb +42 -2
- data/lib/mxrb/compiler/widget_package_extractor.rb +3 -0
- data/lib/mxrb/domain_diagram/lifecycle.rb +272 -0
- data/lib/mxrb/domain_diagram.rb +409 -0
- data/lib/mxrb/dsl/builder.rb +62 -11
- data/lib/mxrb/environment.rb +226 -0
- data/lib/mxrb/exporter.rb +265 -45
- data/lib/mxrb/http/server.rb +107 -0
- data/lib/mxrb/initializer.rb +80 -9
- data/lib/mxrb/integrity/validator.rb +32 -7
- data/lib/mxrb/io/bson_codec.rb +42 -1
- data/lib/mxrb/io/mpr_file.rb +146 -3
- data/lib/mxrb/model/attribute.rb +3 -0
- data/lib/mxrb/model/entity.rb +44 -2
- data/lib/mxrb/model/microflow.rb +3 -1
- data/lib/mxrb/model/module.rb +71 -0
- data/lib/mxrb/model/page.rb +126 -15
- data/lib/mxrb/model/project.rb +3 -0
- data/lib/mxrb/modeler/catalog.rb +178 -0
- data/lib/mxrb/official_marketplace/module_package_importer.rb +23 -12
- data/lib/mxrb/official_marketplace/widget_package_installer.rb +15 -11
- data/lib/mxrb/official_marketplace.rb +9 -3
- data/lib/mxrb/oql/reverse_translator.rb +394 -0
- data/lib/mxrb/oql/server.rb +4 -8
- data/lib/mxrb/oql.rb +32 -4
- data/lib/mxrb/progress.rb +242 -0
- data/lib/mxrb/project_lifecycle.rb +5 -2
- data/lib/mxrb/ruby_app/exporter.rb +1749 -0
- data/lib/mxrb/ruby_app/preset.rb +506 -0
- data/lib/mxrb/ruby_app/session_manager.rb +122 -0
- data/lib/mxrb/ruby_app.rb +1361 -0
- data/lib/mxrb/runtime/access_control.rb +408 -0
- data/lib/mxrb/runtime/native.rb +1015 -93
- data/lib/mxrb/runtime/scheduler.rb +396 -0
- data/lib/mxrb/runtime/schema_migrator.rb +598 -0
- data/lib/mxrb/runtime/shared_store.rb +335 -0
- data/lib/mxrb/runtime/sqlite_store.rb +729 -0
- data/lib/mxrb/scaffold/generator.rb +11 -3
- data/lib/mxrb/team_server.rb +10 -7
- data/lib/mxrb/uml/activity_diagram.rb +118 -0
- data/lib/mxrb/uml/class_diagram.rb +118 -0
- data/lib/mxrb/uml/sequence_diagram.rb +112 -0
- data/lib/mxrb/uml/server.rb +146 -0
- data/lib/mxrb/uml/support.rb +35 -0
- data/lib/mxrb/version.rb +1 -1
- data/lib/mxrb/web_ui/assets/abnfDiagram-N423BO3Z-C4JD3cEC.js +1 -0
- data/lib/mxrb/web_ui/assets/arc-3Z53kgFp.js +1 -0
- data/lib/mxrb/web_ui/assets/architecture-TIHT7OUA-BStBTLea.js +1 -0
- data/lib/mxrb/web_ui/assets/architectureDiagram-T3A2C74G-DBwqnM4J.js +36 -0
- data/lib/mxrb/web_ui/assets/array-BifhSqXX.js +1 -0
- data/lib/mxrb/web_ui/assets/blockDiagram-VBNYF7ZC-DLtuHt-j.js +132 -0
- data/lib/mxrb/web_ui/assets/c4Diagram-5PPSVZJV-ConSk_pe.js +10 -0
- data/lib/mxrb/web_ui/assets/channel-BhPvD7Dt.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-2GRJ4B5K-D2HpWsf_.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-4I5QYGJK-CiIdNwaF.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-5RXB4S5H-Bu_AaEkE.js +231 -0
- data/lib/mxrb/web_ui/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- data/lib/mxrb/web_ui/assets/chunk-6Q2QTUOP-wQkNmt_X.js +88 -0
- data/lib/mxrb/web_ui/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-GF5L2VYU-G8MmiBzU.js +206 -0
- data/lib/mxrb/web_ui/assets/chunk-I66GZJ75-CulcrLPh.js +127 -0
- data/lib/mxrb/web_ui/assets/chunk-JQJVKLGR-ukjzcdlL.js +156 -0
- data/lib/mxrb/web_ui/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-KBJHAD2P-Wm35lYov.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-KEIR6QF5-BICK3FdT.js +161 -0
- data/lib/mxrb/web_ui/assets/chunk-NSK5VX7P-D2nYrCY4.js +2 -0
- data/lib/mxrb/web_ui/assets/chunk-QR6OTTB3-BrdIzg4Q.js +62 -0
- data/lib/mxrb/web_ui/assets/chunk-RYQCIY6F-CCfZMcW6.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-UBXNYLIW-BPEVYsFK.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-W5SLKNZC-2E6OPhA4.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-WRU74C26-CBIvOyWp.js +70 -0
- data/lib/mxrb/web_ui/assets/chunk-XXDRQBXY-pH58XAyl.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- data/lib/mxrb/web_ui/assets/classDiagram-JCYQIIEL-xiqo4D89.js +1 -0
- data/lib/mxrb/web_ui/assets/classDiagram-v2-OCEON4UE-xiqo4D89.js +1 -0
- data/lib/mxrb/web_ui/assets/cose-bilkent-JH36ORCC-t_flPpii.js +1 -0
- data/lib/mxrb/web_ui/assets/cynefin-VYW2F7L2-BIqktTEv.js +1 -0
- data/lib/mxrb/web_ui/assets/cynefinDiagram-MW4NZA55-C2d--lut.js +62 -0
- data/lib/mxrb/web_ui/assets/cytoscape.esm-B-NFISlW.js +321 -0
- data/lib/mxrb/web_ui/assets/dagre-Buvkdvvj.js +1 -0
- data/lib/mxrb/web_ui/assets/dagre-VZM6K2ZE-CSz2_tb5.js +4 -0
- data/lib/mxrb/web_ui/assets/defaultLocale-BFoDCU3G.js +1 -0
- data/lib/mxrb/web_ui/assets/diagram-7IWD3JNH-DCt7u7V_.js +30 -0
- data/lib/mxrb/web_ui/assets/diagram-B4RE2ZJO-DqZBqkMu.js +3 -0
- data/lib/mxrb/web_ui/assets/diagram-LBJQPF4R-ClIYbIsI.js +24 -0
- data/lib/mxrb/web_ui/assets/diagram-Q27KOJAE-DFxhvthJ.js +24 -0
- data/lib/mxrb/web_ui/assets/diagram-UB23O5K3-BSUTXptv.js +41 -0
- data/lib/mxrb/web_ui/assets/dist-D2qOEbeJ.js +1 -0
- data/lib/mxrb/web_ui/assets/domain-mGOZ1KCG.css +1 -0
- data/lib/mxrb/web_ui/assets/domain-qsjXhy4v.js +1 -0
- data/lib/mxrb/web_ui/assets/ebnfDiagram-BXEA7PRR-B5reICEi.js +1 -0
- data/lib/mxrb/web_ui/assets/erDiagram-JOGREHBK-Co5Mxn2p.js +85 -0
- data/lib/mxrb/web_ui/assets/eventmodeling-45OFAUF4-C1ylhSks.js +1 -0
- data/lib/mxrb/web_ui/assets/flowDiagram-UKHOOZJN-CHXaJIaS.js +1 -0
- data/lib/mxrb/web_ui/assets/ganttDiagram-PKOTCBZU-DPPPaWaz.js +292 -0
- data/lib/mxrb/web_ui/assets/gitGraph-TEB2WS4Q-qS77ywA9.js +1 -0
- data/lib/mxrb/web_ui/assets/gitGraphDiagram-DS77QQ5N-DpAv6-BT.js +106 -0
- data/lib/mxrb/web_ui/assets/graphlib-DS17s2tU.js +1 -0
- data/lib/mxrb/web_ui/assets/info-DKCQHKI2-C9VD1Y4e.js +1 -0
- data/lib/mxrb/web_ui/assets/infoDiagram-6WML65LV-Bk1kbcCy.js +2 -0
- data/lib/mxrb/web_ui/assets/init-C-OQMol4.js +1 -0
- data/lib/mxrb/web_ui/assets/ishikawaDiagram-WSZJBQD7-0gcISBFR.js +70 -0
- data/lib/mxrb/web_ui/assets/journeyDiagram-NVQOT4AX-Dt0RQnlh.js +139 -0
- data/lib/mxrb/web_ui/assets/jsx-runtime-DV0a5kSb.js +9 -0
- data/lib/mxrb/web_ui/assets/kanban-definition-27J2QSJJ-DIPQM7w9.js +89 -0
- data/lib/mxrb/web_ui/assets/katex-CXMH3UgJ.js +257 -0
- data/lib/mxrb/web_ui/assets/line-C0rYD-tL.js +1 -0
- data/lib/mxrb/web_ui/assets/linear-BPPxWorj.js +1 -0
- data/lib/mxrb/web_ui/assets/map-BaFkSB1l.js +1 -0
- data/lib/mxrb/web_ui/assets/mermaid-parser.core-B_UPzTxa.js +7 -0
- data/lib/mxrb/web_ui/assets/mindmap-definition-FAOFIHXS-CdJRSMuk.js +96 -0
- data/lib/mxrb/web_ui/assets/modeler-BNqHthwk.css +1 -0
- data/lib/mxrb/web_ui/assets/modeler-CQmqflbg.js +1 -0
- data/lib/mxrb/web_ui/assets/ordinal-BDEzSJ7C.js +1 -0
- data/lib/mxrb/web_ui/assets/packet-7NZHBO7P-CwQ4NSBs.js +1 -0
- data/lib/mxrb/web_ui/assets/path-fybaL0A-.js +1 -0
- data/lib/mxrb/web_ui/assets/pegDiagram-VL7TDLO6-D40S7ppx.js +1 -0
- data/lib/mxrb/web_ui/assets/pie-RZYD4A2V-CJroHMKh.js +1 -0
- data/lib/mxrb/web_ui/assets/pieDiagram-7S7Q4E2Y-QB54__BV.js +39 -0
- data/lib/mxrb/web_ui/assets/quadrantDiagram-CIZ2JOQS-DBhDS6ag.js +7 -0
- data/lib/mxrb/web_ui/assets/radar-I7S5WNFK-DdIUwd2d.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-3IZDKUUU-B1Lw6Xyq.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-abnf-AHOZXSZD-CQgQoeak.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-ebnf-EBAXGLYW-D7qEQ2x8.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-peg-LSFZ7HO6-C005Vxbb.js +1 -0
- data/lib/mxrb/web_ui/assets/railroadDiagram-AXF67PYL-BBP2qk7M.js +1 -0
- data/lib/mxrb/web_ui/assets/requirementDiagram-LRYGKXZP-D2cLsAoI.js +84 -0
- data/lib/mxrb/web_ui/assets/rough.esm-Dy-Kn_BL.js +1 -0
- data/lib/mxrb/web_ui/assets/sankeyDiagram-W5VNT64P-Bpi3FiP2.js +40 -0
- data/lib/mxrb/web_ui/assets/sequenceDiagram-SI44F4Z6-DCRcDlVe.js +162 -0
- data/lib/mxrb/web_ui/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
- data/lib/mxrb/web_ui/assets/src-CvxfUak2.js +1 -0
- data/lib/mxrb/web_ui/assets/stateDiagram-OKZ733FA-C2GCEoxl.js +1 -0
- data/lib/mxrb/web_ui/assets/stateDiagram-v2-UEYNNEHI-D2gWgedh.js +1 -0
- data/lib/mxrb/web_ui/assets/swimlanes-SLNWSIFB-CRI9irEW.js +2 -0
- data/lib/mxrb/web_ui/assets/swimlanesDiagram-ULZ7WXOC-C5Wb_-hZ.js +8 -0
- data/lib/mxrb/web_ui/assets/theme-_mxc7BdY.css +1 -0
- data/lib/mxrb/web_ui/assets/timeline-definition-Z64GVDOM-B1-mNg-h.js +120 -0
- data/lib/mxrb/web_ui/assets/treeView-QDETBFTQ-7CTvIwKq.js +1 -0
- data/lib/mxrb/web_ui/assets/treemap-6X3UGDF4-CtL22pq5.js +1 -0
- data/lib/mxrb/web_ui/assets/uml-C3KAFGTq.js +51 -0
- data/lib/mxrb/web_ui/assets/uml-Dn-qd01X.css +1 -0
- data/lib/mxrb/web_ui/assets/vennDiagram-T6HMQDX7-BzVnUEAy.js +34 -0
- data/lib/mxrb/web_ui/assets/wardley-OPB4EBWU-JNi-QNag.js +1 -0
- data/lib/mxrb/web_ui/assets/wardleyDiagram-T6FBY63Y-BUwmAQ-B.js +78 -0
- data/lib/mxrb/web_ui/assets/xychartDiagram-ELKLHX3M-DEQS8sKp.js +7 -0
- data/lib/mxrb/web_ui/domain.html +15 -0
- data/lib/mxrb/web_ui/modeler.html +16 -0
- data/lib/mxrb/web_ui/uml.html +31 -0
- data/lib/mxrb/web_ui.rb +89 -0
- data/lib/mxrb/widget_certification.rb +208 -0
- data/lib/mxrb/writer.rb +278 -41
- data/lib/mxrb.rb +38 -8
- metadata +157 -5
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Mxrb
|
|
7
|
+
module RubyApp
|
|
8
|
+
# Adds an opt-in conventional Ruby framework around the generic MXRB Rack
|
|
9
|
+
# backend. Presets never alter the default Ruby mode and never own Mendix
|
|
10
|
+
# domain metadata; the Ruby application manifest remains authoritative.
|
|
11
|
+
# rubocop:disable Metrics
|
|
12
|
+
class Preset
|
|
13
|
+
NAMES = %i[flymetothemoon onrails].freeze
|
|
14
|
+
MARKER_PATH = File.join('config', 'mxrb_stack.rb')
|
|
15
|
+
|
|
16
|
+
def self.apply!(root, name)
|
|
17
|
+
new(root, name).apply!
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.detect(root)
|
|
21
|
+
path = File.join(root, MARKER_PATH)
|
|
22
|
+
return unless File.file?(path)
|
|
23
|
+
|
|
24
|
+
File.read(path)[/MXRB_RUBY_STACK\s*=\s*['"](flymetothemoon|onrails)['"]/, 1]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.manifest(name)
|
|
28
|
+
selected = name.to_s
|
|
29
|
+
{
|
|
30
|
+
'preset' => selected, 'rack' => true, 'server' => 'puma',
|
|
31
|
+
'web_framework' => selected == 'onrails' ? 'rails' : 'sinatra',
|
|
32
|
+
'orm' => 'active_record', 'test' => 'rspec', 'tasks' => 'rake'
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def initialize(root, name)
|
|
37
|
+
@root = File.expand_path(root)
|
|
38
|
+
@name = name.to_sym
|
|
39
|
+
raise ArgumentError, "Ruby stack must be #{NAMES.join(' or ')}" unless NAMES.include?(@name)
|
|
40
|
+
|
|
41
|
+
@manifest = Manifest.load(@root)
|
|
42
|
+
@namespace = ruby_constant(@manifest.data.dig('project', 'name'))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def apply!
|
|
46
|
+
ensure_gems
|
|
47
|
+
write(MARKER_PATH, marker_source, replace_baseline: true)
|
|
48
|
+
name == :onrails ? write_rails : write_flymetothemoon
|
|
49
|
+
update_manifest
|
|
50
|
+
append_readme
|
|
51
|
+
root
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
attr_reader :root, :name, :manifest, :namespace
|
|
57
|
+
|
|
58
|
+
def ensure_gems
|
|
59
|
+
path = File.join(root, 'Gemfile')
|
|
60
|
+
source = File.file?(path) ? File.read(path).rstrip : "source 'https://rubygems.org'"
|
|
61
|
+
gems.each do |declaration|
|
|
62
|
+
gem_name = declaration[/gem ['"]([^'"]+)/, 1]
|
|
63
|
+
source += "\n#{declaration}" unless source.match?(/gem ['"]#{Regexp.escape(gem_name)}['"]/)
|
|
64
|
+
end
|
|
65
|
+
File.binwrite(path, "#{source}\n")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def gems
|
|
69
|
+
common = [
|
|
70
|
+
"gem 'rack', '~> 3.1'", "gem 'puma', '~> 8.0'",
|
|
71
|
+
"gem 'activerecord', '~> 8.0'", "gem 'sqlite3', '~> 2.0'",
|
|
72
|
+
"gem 'rake', '~> 13.2'", "gem 'rspec', '~> 3.13'",
|
|
73
|
+
"gem 'rack-test', '~> 2.2'"
|
|
74
|
+
]
|
|
75
|
+
if name == :onrails
|
|
76
|
+
common + ["gem 'rails', '~> 8.0'", "gem 'rspec-rails', '~> 7.1'"]
|
|
77
|
+
else
|
|
78
|
+
common + ["gem 'sinatra', '~> 4.1'"]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def write_flymetothemoon
|
|
83
|
+
write('config.ru', fly_config_ru)
|
|
84
|
+
write('config/environment.rb', fly_environment)
|
|
85
|
+
write('config/puma.rb', puma_config)
|
|
86
|
+
write('config/database.yml', database_yml)
|
|
87
|
+
write('app/models/application_record.rb', application_record)
|
|
88
|
+
write('app/web/application.rb', sinatra_application)
|
|
89
|
+
write('db/migrate/.keep', '')
|
|
90
|
+
write('Rakefile', fly_rakefile)
|
|
91
|
+
write('.rspec', "--require spec_helper\n--format documentation\n")
|
|
92
|
+
write('spec/spec_helper.rb', fly_spec_helper)
|
|
93
|
+
write('spec/requests/health_spec.rb', fly_health_spec)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def write_rails
|
|
97
|
+
write('config/boot.rb', rails_boot)
|
|
98
|
+
write('config/application.rb', rails_application, replace_baseline: true)
|
|
99
|
+
write('config/environment.rb', rails_environment)
|
|
100
|
+
write('config/environments/development.rb', rails_development)
|
|
101
|
+
write('config/environments/test.rb', rails_test)
|
|
102
|
+
write('config/routes.rb', rails_routes)
|
|
103
|
+
write('config/puma.rb', puma_config)
|
|
104
|
+
write('config/database.yml', database_yml)
|
|
105
|
+
write('config.ru', rails_config_ru)
|
|
106
|
+
write('app/models/application_record.rb', application_record)
|
|
107
|
+
write('app/controllers/application_controller.rb', rails_application_controller)
|
|
108
|
+
write('app/controllers/health_controller.rb', rails_health_controller)
|
|
109
|
+
write('db/migrate/.keep', '')
|
|
110
|
+
write('Rakefile', rails_rakefile)
|
|
111
|
+
write('bin/rails', rails_bin)
|
|
112
|
+
FileUtils.chmod(0o755, File.join(root, 'bin', 'rails'))
|
|
113
|
+
write('.rspec', "--require spec_helper\n--format documentation\n")
|
|
114
|
+
write('spec/spec_helper.rb', rails_spec_helper)
|
|
115
|
+
write('spec/rails_helper.rb', rails_helper)
|
|
116
|
+
write('spec/requests/health_spec.rb', rails_health_spec)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def update_manifest
|
|
120
|
+
path = File.join(root, MANIFEST_PATH)
|
|
121
|
+
data = JSON.parse(File.read(path))
|
|
122
|
+
data['ruby_stack'] = self.class.manifest(name)
|
|
123
|
+
File.binwrite(path, JSON.pretty_generate(data) << "\n")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def append_readme
|
|
127
|
+
path = File.join(root, 'README.md')
|
|
128
|
+
text = File.file?(path) ? File.read(path) : ''
|
|
129
|
+
heading = name == :onrails ? 'Rails preset' : 'Fly Me to the Moon preset'
|
|
130
|
+
return if text.include?("## #{heading}")
|
|
131
|
+
|
|
132
|
+
text += <<~MARKDOWN
|
|
133
|
+
|
|
134
|
+
## #{heading}
|
|
135
|
+
|
|
136
|
+
This opt-in stack runs the MXRB backend through Rack and Puma while
|
|
137
|
+
React + Vite remains the browser client. ActiveRecord owns only Ruby
|
|
138
|
+
tables declared through its migrations; Mendix entities remain owned
|
|
139
|
+
by the MXRB manifest so round-trips never have two schema authorities.
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
bundle install
|
|
143
|
+
bundle exec rake db:migrate
|
|
144
|
+
bundle exec rspec
|
|
145
|
+
bundle exec mxrb run .
|
|
146
|
+
```
|
|
147
|
+
MARKDOWN
|
|
148
|
+
File.binwrite(path, text)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def write(relative, contents, replace_baseline: false)
|
|
152
|
+
path = File.join(root, relative)
|
|
153
|
+
return if File.file?(path) && !(replace_baseline && baseline_file?(path))
|
|
154
|
+
|
|
155
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
156
|
+
File.binwrite(path, contents)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def baseline_file?(path)
|
|
160
|
+
return true if path.end_with?(MARKER_PATH)
|
|
161
|
+
|
|
162
|
+
File.read(path).include?('MXRB_APPLICATION_ROOT')
|
|
163
|
+
rescue Errno::ENOENT
|
|
164
|
+
true
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def ruby_constant(value)
|
|
168
|
+
result = value.to_s.split(/[^A-Za-z0-9]+/).reject(&:empty?).map do |part|
|
|
169
|
+
part[0].upcase + part[1..].to_s
|
|
170
|
+
end.join
|
|
171
|
+
result = "Application#{result}" if result.empty? || result.match?(/\A\d/)
|
|
172
|
+
result
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def marker_source
|
|
176
|
+
<<~RUBY
|
|
177
|
+
# frozen_string_literal: true
|
|
178
|
+
|
|
179
|
+
MXRB_RUBY_STACK = '#{name}' unless defined?(MXRB_RUBY_STACK)
|
|
180
|
+
RUBY
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def puma_config
|
|
184
|
+
<<~RUBY
|
|
185
|
+
# frozen_string_literal: true
|
|
186
|
+
|
|
187
|
+
bind "tcp://\#{ENV.fetch('HOST', '127.0.0.1')}:\#{ENV.fetch('MXRB_SERVER_PORT', '9292')}"
|
|
188
|
+
threads_count = Integer(ENV.fetch('RAILS_MAX_THREADS', '5'))
|
|
189
|
+
threads threads_count, threads_count
|
|
190
|
+
workers 0
|
|
191
|
+
environment ENV.fetch('RACK_ENV', ENV.fetch('RAILS_ENV', 'development'))
|
|
192
|
+
RUBY
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def database_yml
|
|
196
|
+
<<~YAML
|
|
197
|
+
default: &default
|
|
198
|
+
adapter: sqlite3
|
|
199
|
+
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>
|
|
200
|
+
timeout: 5000
|
|
201
|
+
|
|
202
|
+
development:
|
|
203
|
+
<<: *default
|
|
204
|
+
database: db/development.sqlite3
|
|
205
|
+
|
|
206
|
+
test:
|
|
207
|
+
<<: *default
|
|
208
|
+
database: db/test.sqlite3
|
|
209
|
+
|
|
210
|
+
production:
|
|
211
|
+
<<: *default
|
|
212
|
+
database: <%= ENV.fetch("DATABASE_PATH", "db/production.sqlite3") %>
|
|
213
|
+
YAML
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def application_record
|
|
217
|
+
<<~RUBY
|
|
218
|
+
# frozen_string_literal: true
|
|
219
|
+
|
|
220
|
+
if defined?(ActiveRecord::Base) && !defined?(ApplicationRecord)
|
|
221
|
+
class ApplicationRecord < ActiveRecord::Base
|
|
222
|
+
primary_abstract_class
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
RUBY
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def fly_environment
|
|
229
|
+
<<~RUBY
|
|
230
|
+
# frozen_string_literal: true
|
|
231
|
+
|
|
232
|
+
require 'bundler/setup'
|
|
233
|
+
require 'yaml'
|
|
234
|
+
require 'erb'
|
|
235
|
+
require 'active_record'
|
|
236
|
+
require 'mxrb'
|
|
237
|
+
|
|
238
|
+
root = File.expand_path('..', __dir__)
|
|
239
|
+
environment = ENV.fetch('RACK_ENV', 'development')
|
|
240
|
+
settings = YAML.safe_load(ERB.new(File.read(File.join(root, 'config/database.yml'))).result,
|
|
241
|
+
aliases: true).fetch(environment)
|
|
242
|
+
ActiveRecord::Base.establish_connection(settings)
|
|
243
|
+
require File.join(root, 'app/models/application_record')
|
|
244
|
+
RUBY
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def sinatra_application
|
|
248
|
+
<<~RUBY
|
|
249
|
+
# frozen_string_literal: true
|
|
250
|
+
|
|
251
|
+
require 'json'
|
|
252
|
+
require 'sinatra/base'
|
|
253
|
+
|
|
254
|
+
module #{namespace}
|
|
255
|
+
module Web
|
|
256
|
+
class Application < Sinatra::Base
|
|
257
|
+
set :show_exceptions, false
|
|
258
|
+
|
|
259
|
+
get '/health' do
|
|
260
|
+
content_type :json
|
|
261
|
+
JSON.generate(ok: true, framework: 'sinatra')
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
RUBY
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def fly_config_ru
|
|
270
|
+
<<~RUBY
|
|
271
|
+
# frozen_string_literal: true
|
|
272
|
+
|
|
273
|
+
require_relative 'config/environment'
|
|
274
|
+
require_relative 'app/web/application'
|
|
275
|
+
|
|
276
|
+
map '/ruby' do
|
|
277
|
+
run #{namespace}::Web::Application
|
|
278
|
+
end
|
|
279
|
+
run Mxrb::RubyApp::RackAdapter.new(__dir__)
|
|
280
|
+
RUBY
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def fly_rakefile
|
|
284
|
+
<<~RUBY
|
|
285
|
+
# frozen_string_literal: true
|
|
286
|
+
|
|
287
|
+
require_relative 'config/environment'
|
|
288
|
+
require 'rake'
|
|
289
|
+
require 'active_record/tasks/database_tasks'
|
|
290
|
+
require 'rspec/core/rake_task'
|
|
291
|
+
|
|
292
|
+
namespace :db do
|
|
293
|
+
desc 'Run ActiveRecord migrations for Ruby-owned tables'
|
|
294
|
+
task :migrate do
|
|
295
|
+
ActiveRecord::MigrationContext.new('db/migrate').migrate
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
300
|
+
task default: :spec
|
|
301
|
+
RUBY
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def fly_spec_helper
|
|
305
|
+
<<~RUBY
|
|
306
|
+
# frozen_string_literal: true
|
|
307
|
+
|
|
308
|
+
ENV['RACK_ENV'] = 'test'
|
|
309
|
+
require_relative '../config/environment'
|
|
310
|
+
require 'rack/test'
|
|
311
|
+
require 'rspec'
|
|
312
|
+
|
|
313
|
+
RSpec.configure do |config|
|
|
314
|
+
config.include Rack::Test::Methods
|
|
315
|
+
end
|
|
316
|
+
RUBY
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def fly_health_spec
|
|
320
|
+
<<~RUBY
|
|
321
|
+
# frozen_string_literal: true
|
|
322
|
+
|
|
323
|
+
require 'spec_helper'
|
|
324
|
+
require_relative '../../app/web/application'
|
|
325
|
+
|
|
326
|
+
RSpec.describe 'Ruby web health' do
|
|
327
|
+
def app = #{namespace}::Web::Application
|
|
328
|
+
|
|
329
|
+
it 'answers through Sinatra' do
|
|
330
|
+
get '/health'
|
|
331
|
+
expect(last_response).to be_ok
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
RUBY
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def rails_boot
|
|
338
|
+
<<~RUBY
|
|
339
|
+
# frozen_string_literal: true
|
|
340
|
+
|
|
341
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
|
|
342
|
+
require 'bundler/setup'
|
|
343
|
+
RUBY
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def rails_application
|
|
347
|
+
<<~RUBY
|
|
348
|
+
# frozen_string_literal: true
|
|
349
|
+
|
|
350
|
+
require_relative 'boot'
|
|
351
|
+
require 'rails/all'
|
|
352
|
+
require 'mxrb'
|
|
353
|
+
Bundler.require(*Rails.groups)
|
|
354
|
+
|
|
355
|
+
module #{namespace}
|
|
356
|
+
class Application < Rails::Application
|
|
357
|
+
config.load_defaults 8.0
|
|
358
|
+
config.autoload_lib(ignore: %w[assets tasks]) if config.respond_to?(:autoload_lib)
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
RUBY
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def rails_environment
|
|
365
|
+
<<~RUBY
|
|
366
|
+
# frozen_string_literal: true
|
|
367
|
+
|
|
368
|
+
require_relative 'application'
|
|
369
|
+
Rails.application.initialize!
|
|
370
|
+
RUBY
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def rails_development
|
|
374
|
+
<<~RUBY
|
|
375
|
+
# frozen_string_literal: true
|
|
376
|
+
|
|
377
|
+
Rails.application.configure do
|
|
378
|
+
config.enable_reloading = true
|
|
379
|
+
config.eager_load = false
|
|
380
|
+
config.consider_all_requests_local = true
|
|
381
|
+
config.secret_key_base = ENV.fetch('SECRET_KEY_BASE', 'development-only-mxrb-secret')
|
|
382
|
+
end
|
|
383
|
+
RUBY
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def rails_test
|
|
387
|
+
<<~RUBY
|
|
388
|
+
# frozen_string_literal: true
|
|
389
|
+
|
|
390
|
+
Rails.application.configure do
|
|
391
|
+
config.enable_reloading = false
|
|
392
|
+
config.eager_load = false
|
|
393
|
+
config.consider_all_requests_local = true
|
|
394
|
+
config.secret_key_base = 'test-only-mxrb-secret'
|
|
395
|
+
end
|
|
396
|
+
RUBY
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def rails_routes
|
|
400
|
+
<<~RUBY
|
|
401
|
+
# frozen_string_literal: true
|
|
402
|
+
|
|
403
|
+
Rails.application.routes.draw do
|
|
404
|
+
get '/ruby/health', to: 'health#show'
|
|
405
|
+
mxrb_backend = Mxrb::RubyApp::RackAdapter.new(Rails.root.to_s)
|
|
406
|
+
match '/api', to: mxrb_backend, via: :all
|
|
407
|
+
match '/api/*path', to: mxrb_backend, via: :all
|
|
408
|
+
end
|
|
409
|
+
RUBY
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def rails_config_ru
|
|
413
|
+
<<~RUBY
|
|
414
|
+
# frozen_string_literal: true
|
|
415
|
+
|
|
416
|
+
require_relative 'config/environment'
|
|
417
|
+
map '/api' do
|
|
418
|
+
run Mxrb::RubyApp::RackAdapter.new(Rails.root.to_s)
|
|
419
|
+
end
|
|
420
|
+
run Rails.application
|
|
421
|
+
RUBY
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def rails_application_controller
|
|
425
|
+
<<~RUBY
|
|
426
|
+
# frozen_string_literal: true
|
|
427
|
+
|
|
428
|
+
class ApplicationController < ActionController::Base
|
|
429
|
+
end
|
|
430
|
+
RUBY
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def rails_health_controller
|
|
434
|
+
<<~RUBY
|
|
435
|
+
# frozen_string_literal: true
|
|
436
|
+
|
|
437
|
+
class HealthController < ApplicationController
|
|
438
|
+
def show
|
|
439
|
+
render json: { ok: true, framework: 'rails' }
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
RUBY
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def rails_rakefile
|
|
446
|
+
<<~RUBY
|
|
447
|
+
# frozen_string_literal: true
|
|
448
|
+
|
|
449
|
+
require_relative 'config/application'
|
|
450
|
+
Rails.application.load_tasks
|
|
451
|
+
RUBY
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def rails_bin
|
|
455
|
+
<<~RUBY
|
|
456
|
+
#!/usr/bin/env ruby
|
|
457
|
+
# frozen_string_literal: true
|
|
458
|
+
|
|
459
|
+
APP_PATH = File.expand_path('../config/application', __dir__)
|
|
460
|
+
require_relative '../config/boot'
|
|
461
|
+
require 'rails/commands'
|
|
462
|
+
RUBY
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def rails_spec_helper
|
|
466
|
+
<<~RUBY
|
|
467
|
+
# frozen_string_literal: true
|
|
468
|
+
|
|
469
|
+
require 'rspec'
|
|
470
|
+
RUBY
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def rails_helper
|
|
474
|
+
<<~RUBY
|
|
475
|
+
# frozen_string_literal: true
|
|
476
|
+
|
|
477
|
+
ENV['RAILS_ENV'] ||= 'test'
|
|
478
|
+
require_relative '../config/environment'
|
|
479
|
+
require 'rspec/rails'
|
|
480
|
+
RUBY
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def rails_health_spec
|
|
484
|
+
<<~RUBY
|
|
485
|
+
# frozen_string_literal: true
|
|
486
|
+
|
|
487
|
+
require 'rails_helper'
|
|
488
|
+
|
|
489
|
+
RSpec.describe 'Ruby web health', type: :request do
|
|
490
|
+
it 'answers through Rails' do
|
|
491
|
+
get '/ruby/health'
|
|
492
|
+
expect(response).to have_http_status(:ok)
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
it 'mounts the generic MXRB backend under /api' do
|
|
496
|
+
get '/api/health'
|
|
497
|
+
expect(response).to have_http_status(:ok)
|
|
498
|
+
expect(response.parsed_body).to include('ok' => true)
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
RUBY
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
# rubocop:enable Metrics
|
|
505
|
+
end
|
|
506
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'securerandom'
|
|
6
|
+
require 'time'
|
|
7
|
+
require_relative '../runtime/shared_store'
|
|
8
|
+
|
|
9
|
+
module Mxrb
|
|
10
|
+
module RubyApp
|
|
11
|
+
class AuthenticationError < StandardError; end
|
|
12
|
+
|
|
13
|
+
# Environment-backed identities and short-lived opaque sessions for the
|
|
14
|
+
# standalone Ruby server. Production deployments can put the same adapter
|
|
15
|
+
# behind their own Rack authentication and provide static bearer tokens.
|
|
16
|
+
class SessionManager
|
|
17
|
+
attr_reader :ttl
|
|
18
|
+
|
|
19
|
+
# rubocop:disable Metrics/ParameterLists
|
|
20
|
+
def initialize(access_control, users: ENV['MXRB_USERS_JSON'],
|
|
21
|
+
tokens: ENV['MXRB_AUTH_TOKENS'], ttl: ENV.fetch('MXRB_SESSION_TTL', '3600'),
|
|
22
|
+
clock: -> { Time.now.utc }, store: nil)
|
|
23
|
+
@access_control = access_control
|
|
24
|
+
@users = parse_map(users, 'MXRB_USERS_JSON')
|
|
25
|
+
@static_tokens = parse_map(tokens, 'MXRB_AUTH_TOKENS')
|
|
26
|
+
@ttl = Integer(ttl)
|
|
27
|
+
raise ArgumentError, 'session TTL must be positive' unless @ttl.positive?
|
|
28
|
+
|
|
29
|
+
@clock = clock
|
|
30
|
+
@store = store || Runtime::MemorySharedStore.new
|
|
31
|
+
end
|
|
32
|
+
# rubocop:enable Metrics/ParameterLists
|
|
33
|
+
|
|
34
|
+
def anonymous
|
|
35
|
+
@access_control.context
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def authenticate(header)
|
|
39
|
+
token = bearer_token(header)
|
|
40
|
+
return anonymous unless token
|
|
41
|
+
|
|
42
|
+
static = @static_tokens[token]
|
|
43
|
+
return context_for(static) if static
|
|
44
|
+
|
|
45
|
+
session = @store.read_session(token, now: @clock.call)
|
|
46
|
+
raise AuthenticationError, 'invalid or expired bearer token' unless session
|
|
47
|
+
|
|
48
|
+
context_for(session.identity)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def login(username, password)
|
|
52
|
+
identity = authenticated_identity(username, password)
|
|
53
|
+
|
|
54
|
+
token = SecureRandom.urlsafe_base64(32)
|
|
55
|
+
expires_at = @clock.call + ttl
|
|
56
|
+
profile = identity.slice('roles', 'user_roles', 'module_roles', 'attributes')
|
|
57
|
+
.merge('user' => username.to_s)
|
|
58
|
+
context = context_for(profile)
|
|
59
|
+
@store.write_session(token:, identity: profile, expires_at:)
|
|
60
|
+
{ token:, expires_at: expires_at.iso8601, user: context.user, roles: context.user_roles }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def logout(header)
|
|
64
|
+
token = bearer_token(header)
|
|
65
|
+
token && @store.delete_session(token)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def authenticated_identity(username, password)
|
|
71
|
+
identity = @users[username.to_s]
|
|
72
|
+
unless identity && password_matches?(identity, password)
|
|
73
|
+
raise AuthenticationError, 'invalid username or password'
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
identity
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def context_for(raw)
|
|
80
|
+
value = raw.is_a?(Hash) ? raw : {}
|
|
81
|
+
@access_control.context(
|
|
82
|
+
user: value['user'], user_roles: value['roles'] || value['user_roles'],
|
|
83
|
+
module_roles: value['module_roles'], attributes: value['attributes'] || {}
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def bearer_token(header)
|
|
88
|
+
match = /\ABearer\s+(.+)\z/i.match(header.to_s.strip)
|
|
89
|
+
match && match[1]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def password_matches?(identity, supplied)
|
|
93
|
+
expected = identity['password_digest'].to_s
|
|
94
|
+
expected = "plain$#{identity['password']}" if expected.empty? && identity.key?('password')
|
|
95
|
+
scheme, value = expected.split('$', 2)
|
|
96
|
+
actual = case scheme
|
|
97
|
+
when 'sha256' then Digest::SHA256.hexdigest(supplied.to_s)
|
|
98
|
+
when 'plain' then supplied.to_s
|
|
99
|
+
else return false
|
|
100
|
+
end
|
|
101
|
+
secure_compare(actual, value.to_s)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def secure_compare(left, right)
|
|
105
|
+
return false unless left.bytesize == right.bytesize
|
|
106
|
+
|
|
107
|
+
left.bytes.zip(right.bytes).reduce(0) { |difference, pair| difference | (pair[0] ^ pair[1]) }.zero?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def parse_map(raw, name)
|
|
111
|
+
return {} if raw.nil? || raw.to_s.strip.empty?
|
|
112
|
+
|
|
113
|
+
parsed = JSON.parse(raw)
|
|
114
|
+
raise ArgumentError, "#{name} must contain a JSON object" unless parsed.is_a?(Hash)
|
|
115
|
+
|
|
116
|
+
parsed
|
|
117
|
+
rescue JSON::ParserError => e
|
|
118
|
+
raise ArgumentError, "invalid #{name}: #{e.message}"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|