mxrb 0.1.3 → 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 +455 -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/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/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/page_bundle_compiler.rb +13 -4
- 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 +43 -9
- data/lib/mxrb/environment.rb +226 -0
- data/lib/mxrb/exporter.rb +89 -15
- data/lib/mxrb/http/server.rb +107 -0
- data/lib/mxrb/initializer.rb +80 -9
- data/lib/mxrb/integrity/validator.rb +14 -0
- 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 +28 -2
- data/lib/mxrb/model/microflow.rb +3 -1
- 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/oql/reverse_translator.rb +394 -0
- data/lib/mxrb/oql/server.rb +4 -8
- data/lib/mxrb/oql.rb +23 -1
- 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/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 +200 -24
- data/lib/mxrb.rb +26 -1
- metadata +156 -5
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'monitor'
|
|
4
|
+
|
|
5
|
+
module Mxrb
|
|
6
|
+
# Immutable, stdlib-only environment configuration. Loading never mutates
|
|
7
|
+
# process ENV; apply and with are the explicit mutation boundaries.
|
|
8
|
+
class Environment # rubocop:disable Metrics/ClassLength
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
class InvalidName < ArgumentError; end
|
|
11
|
+
|
|
12
|
+
# Syntax error with location but never with the offending secret value.
|
|
13
|
+
class ParseError < Error
|
|
14
|
+
attr_reader :path, :line
|
|
15
|
+
|
|
16
|
+
def initialize(path, line, reason)
|
|
17
|
+
@path = path
|
|
18
|
+
@line = line
|
|
19
|
+
super("invalid environment file #{path}:#{line}: #{reason}")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
NAME_PATTERN = /\A[a-z][a-z0-9_-]*\z/
|
|
24
|
+
KEY_PATTERN = /\A[A-Za-z_][A-Za-z0-9_]*\z/
|
|
25
|
+
ALIASES = {
|
|
26
|
+
'dev' => 'development',
|
|
27
|
+
'development' => 'development',
|
|
28
|
+
'qa' => 'qa',
|
|
29
|
+
'homolog' => 'staging',
|
|
30
|
+
'homologation' => 'staging',
|
|
31
|
+
'staging' => 'staging',
|
|
32
|
+
'prod' => 'production',
|
|
33
|
+
'production' => 'production'
|
|
34
|
+
}.freeze
|
|
35
|
+
PROFILE_KEYS = %w[MXRB_ENV RACK_ENV RAILS_ENV].freeze
|
|
36
|
+
WITH_MONITOR = Monitor.new
|
|
37
|
+
UNSET = Object.new.freeze
|
|
38
|
+
|
|
39
|
+
attr_reader :name, :requested_name, :root, :sources
|
|
40
|
+
|
|
41
|
+
def self.load(name = nil, root: Dir.pwd, process: ENV)
|
|
42
|
+
new(name, root:, process:)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def initialize(name = nil, root: Dir.pwd, process: ENV)
|
|
46
|
+
@root = File.expand_path(root)
|
|
47
|
+
@process = stringify(process)
|
|
48
|
+
@requested_name = validate_name(name || detected_name)
|
|
49
|
+
@name = ALIASES.fetch(@requested_name, @requested_name)
|
|
50
|
+
@sources = existing_sources.freeze
|
|
51
|
+
@values = load_values.freeze
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def fetch(key, default = UNSET, &block)
|
|
55
|
+
normalized = key.to_s
|
|
56
|
+
return @values.fetch(normalized, &block) if block
|
|
57
|
+
return @values.fetch(normalized) if default.equal?(UNSET)
|
|
58
|
+
|
|
59
|
+
@values.fetch(normalized, default)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def [](key) = @values[key.to_s]
|
|
63
|
+
def key?(key) = @values.key?(key.to_s)
|
|
64
|
+
|
|
65
|
+
def to_h = @values.dup
|
|
66
|
+
|
|
67
|
+
# Applies to ENV by default only when explicitly called. overwrite: false
|
|
68
|
+
# preserves keys already present in the target.
|
|
69
|
+
def apply(target = ENV, overwrite: true)
|
|
70
|
+
@values.each do |key, value|
|
|
71
|
+
next if !overwrite && target.key?(key)
|
|
72
|
+
|
|
73
|
+
target[key] = value
|
|
74
|
+
end
|
|
75
|
+
target
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Applies the configuration for one block and restores the target even if
|
|
79
|
+
# the block raises. Calls are serialized because process ENV is global.
|
|
80
|
+
def with(target = ENV, overwrite: true)
|
|
81
|
+
WITH_MONITOR.synchronize do
|
|
82
|
+
snapshot = snapshot(target, overwrite:)
|
|
83
|
+
apply(target, overwrite:)
|
|
84
|
+
begin
|
|
85
|
+
yield self
|
|
86
|
+
ensure
|
|
87
|
+
restore(target, snapshot)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def inspect
|
|
93
|
+
"#<#{self.class} name=#{name.inspect} root=#{root.inspect} " \
|
|
94
|
+
"sources=#{sources.size} keys=#{@values.size}>"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def detected_name
|
|
100
|
+
PROFILE_KEYS.each do |key|
|
|
101
|
+
value = @process[key].to_s.strip
|
|
102
|
+
return value unless value.empty?
|
|
103
|
+
end
|
|
104
|
+
'development'
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def validate_name(raw)
|
|
108
|
+
candidate = raw.to_s.strip.downcase
|
|
109
|
+
raise InvalidName, "invalid environment name #{raw.inspect}" unless NAME_PATTERN.match?(candidate)
|
|
110
|
+
|
|
111
|
+
candidate.freeze
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def candidate_sources
|
|
115
|
+
[
|
|
116
|
+
File.join(root, '.env'),
|
|
117
|
+
File.join(root, ".env.#{name}"),
|
|
118
|
+
File.join(root, 'config', 'environments', "#{name}.env")
|
|
119
|
+
]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def existing_sources = candidate_sources.select { File.file?(_1) }
|
|
123
|
+
|
|
124
|
+
def load_values
|
|
125
|
+
loaded = sources.each_with_object({}) do |path, values|
|
|
126
|
+
values.update(Parser.new(path).parse)
|
|
127
|
+
end
|
|
128
|
+
immutable(loaded.merge(@process))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def stringify(values)
|
|
132
|
+
immutable(values.to_h.to_h { |key, value| [key.to_s, value.to_s] })
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def immutable(values)
|
|
136
|
+
values.to_h do |key, value|
|
|
137
|
+
[key.to_s.dup.freeze, value.to_s.dup.freeze]
|
|
138
|
+
end.freeze
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def snapshot(target, overwrite:)
|
|
142
|
+
keys = overwrite ? @values.keys : @values.keys.reject { target.key?(_1) }
|
|
143
|
+
keys.to_h do |key|
|
|
144
|
+
[key, target.key?(key) ? [:present, target[key]] : [:missing, nil]]
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def restore(target, snapshot)
|
|
149
|
+
snapshot.each do |key, (state, value)|
|
|
150
|
+
state == :present ? target[key] = value : target.delete(key)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# A deliberately non-shell parser: values are read as data and variable,
|
|
155
|
+
# command, or backtick expansion is never performed.
|
|
156
|
+
class Parser
|
|
157
|
+
def initialize(path)
|
|
158
|
+
@path = path
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def parse
|
|
162
|
+
File.foreach(@path, encoding: 'bom|utf-8').with_index(1).each_with_object({}) do |(line, number), result|
|
|
163
|
+
parse_line(line, number, result)
|
|
164
|
+
end
|
|
165
|
+
rescue ArgumentError, EncodingError => e
|
|
166
|
+
raise ParseError.new(@path, 1, e.message), cause: e
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
private
|
|
170
|
+
|
|
171
|
+
def parse_line(line, number, result)
|
|
172
|
+
source = line.strip
|
|
173
|
+
return if source.empty? || source.start_with?('#')
|
|
174
|
+
|
|
175
|
+
source = source.delete_prefix('export ').lstrip
|
|
176
|
+
key, raw = source.split('=', 2)
|
|
177
|
+
key = key.to_s.strip
|
|
178
|
+
error(number, 'expected KEY=VALUE') if raw.nil? || !KEY_PATTERN.match?(key)
|
|
179
|
+
result[key] = parse_value(raw.lstrip, number)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def parse_value(source, number)
|
|
183
|
+
return '' if source.empty?
|
|
184
|
+
return quoted_value(source, number, "'") if source.start_with?("'")
|
|
185
|
+
return quoted_value(source, number, '"') if source.start_with?('"')
|
|
186
|
+
|
|
187
|
+
source.sub(/\s+#.*\z/, '').rstrip
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def quoted_value(source, number, quote)
|
|
191
|
+
value, remainder = extract_quoted(source, quote)
|
|
192
|
+
error(number, 'unterminated quoted value') unless remainder
|
|
193
|
+
error(number, 'unexpected content after quoted value') unless remainder.strip.match?(/\A(?:#.*)?\z/)
|
|
194
|
+
|
|
195
|
+
quote == '"' ? unescape_double(value) : value
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def extract_quoted(source, quote) # rubocop:disable Metrics/MethodLength
|
|
199
|
+
escaped = false
|
|
200
|
+
source.each_char.with_index do |character, index|
|
|
201
|
+
if escaped
|
|
202
|
+
escaped = false
|
|
203
|
+
next
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
if character == '\\' && quote == '"'
|
|
207
|
+
escaped = true
|
|
208
|
+
elsif character == quote && index.positive?
|
|
209
|
+
return [source[1...index], source[(index + 1)..]]
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
[nil, nil]
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def unescape_double(value)
|
|
216
|
+
value.gsub(/\\([nrt"\\])/) do
|
|
217
|
+
{ 'n' => "\n", 'r' => "\r", 't' => "\t", '"' => '"', '\\' => '\\' }.fetch(Regexp.last_match(1))
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def error(line, reason)
|
|
222
|
+
raise ParseError.new(@path, line, reason)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
data/lib/mxrb/exporter.rb
CHANGED
|
@@ -8,6 +8,7 @@ require "digest"
|
|
|
8
8
|
module Mxrb
|
|
9
9
|
# Exports an MPR into an editable, layered Ruby source tree.
|
|
10
10
|
class Exporter
|
|
11
|
+
MODES = %i[mendix ruby].freeze
|
|
11
12
|
LAYERS = %w[domain application presentation infrastructure].freeze
|
|
12
13
|
MODULE_PROGRESS_WEIGHT = 10
|
|
13
14
|
MARKETPLACE_PROVENANCE = [
|
|
@@ -58,18 +59,25 @@ module Mxrb
|
|
|
58
59
|
Microflows$RestCallAction
|
|
59
60
|
].freeze
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
attr_reader :mode
|
|
63
|
+
|
|
64
|
+
def initialize(mpr_path, output_dir, mode: :mendix)
|
|
62
65
|
@mpr_path = File.expand_path(mpr_path)
|
|
63
66
|
@output_dir = File.expand_path(output_dir)
|
|
67
|
+
@mode = mode.to_sym
|
|
68
|
+
raise ArgumentError, "export mode must be mendix or ruby" unless MODES.include?(@mode)
|
|
64
69
|
end
|
|
65
70
|
|
|
66
71
|
def export!(parallel: true)
|
|
72
|
+
return export_ruby!(parallel:) if mode == :ruby
|
|
73
|
+
|
|
67
74
|
Progress.with("Exporting #{File.basename(@mpr_path)}") do |progress|
|
|
68
75
|
FileUtils.mkdir_p(@output_dir)
|
|
69
76
|
Mxrb.open(@mpr_path) do |project|
|
|
70
77
|
@architecture = project.architecture_definition
|
|
71
78
|
units = project.all_units
|
|
72
79
|
modules = project.modules
|
|
80
|
+
@inferred_public_artifacts = infer_public_artifacts(modules)
|
|
73
81
|
assets = project_asset_files
|
|
74
82
|
progress.update(
|
|
75
83
|
current: 0,
|
|
@@ -80,12 +88,13 @@ module Mxrb
|
|
|
80
88
|
progress.advance(detail: "project structure")
|
|
81
89
|
export_project_assets(assets, progress)
|
|
82
90
|
export_native_units(project, units, progress)
|
|
91
|
+
ruby_sources = export_ruby_app_sources(project)
|
|
83
92
|
export_security(project)
|
|
84
93
|
progress.advance(detail: "project security")
|
|
85
94
|
export_architecture_contracts(project)
|
|
86
95
|
progress.advance(detail: "navigation and design system")
|
|
87
96
|
export_modules(modules, progress, parallel:)
|
|
88
|
-
write_project(project)
|
|
97
|
+
write_project(project, ruby_sources:)
|
|
89
98
|
progress.advance(detail: "project.rb")
|
|
90
99
|
end
|
|
91
100
|
end
|
|
@@ -94,6 +103,12 @@ module Mxrb
|
|
|
94
103
|
|
|
95
104
|
private
|
|
96
105
|
|
|
106
|
+
def export_ruby!(parallel:)
|
|
107
|
+
sidecar = File.join(@output_dir, ".mxrb", "mendix")
|
|
108
|
+
self.class.new(@mpr_path, sidecar, mode: :mendix).export!(parallel:)
|
|
109
|
+
RubyApp::Exporter.new(@mpr_path, @output_dir, mendix_sidecar: sidecar).export!
|
|
110
|
+
end
|
|
111
|
+
|
|
97
112
|
def export_module(mod)
|
|
98
113
|
root = File.join(@output_dir, "modules", mod.name)
|
|
99
114
|
LAYERS.each { FileUtils.mkdir_p(File.join(root, _1)) }
|
|
@@ -186,6 +201,24 @@ module Mxrb
|
|
|
186
201
|
)
|
|
187
202
|
end
|
|
188
203
|
|
|
204
|
+
def export_ruby_app_sources(project)
|
|
205
|
+
files = project.mpr.ruby_app_sources
|
|
206
|
+
return false if files.empty?
|
|
207
|
+
|
|
208
|
+
payload = files.map do |file|
|
|
209
|
+
contents = file.fetch(:contents)
|
|
210
|
+
{
|
|
211
|
+
"path" => file.fetch(:path), "sha256" => file.fetch(:sha256),
|
|
212
|
+
"contents" => Base64.strict_encode64(contents), "mode" => file.fetch(:mode, 0o644)
|
|
213
|
+
}
|
|
214
|
+
end
|
|
215
|
+
write(
|
|
216
|
+
File.join(@output_dir, ".mxrb", "ruby_sources.json"),
|
|
217
|
+
JSON.pretty_generate("version" => 1, "files" => payload)
|
|
218
|
+
)
|
|
219
|
+
true
|
|
220
|
+
end
|
|
221
|
+
|
|
189
222
|
def export_modules(modules, progress, parallel:)
|
|
190
223
|
operation = lambda do |mod|
|
|
191
224
|
export_module(mod)
|
|
@@ -279,7 +312,7 @@ module Mxrb
|
|
|
279
312
|
paths = {}
|
|
280
313
|
grouped = mod.entities.group_by { entity_domain_route(_1) }
|
|
281
314
|
grouped.each do |route, entities|
|
|
282
|
-
unique_entity_filenames(entities).each do |id, filename|
|
|
315
|
+
unique_entity_filenames(entities, route:).each do |id, filename|
|
|
283
316
|
paths[id] = File.join(route, filename)
|
|
284
317
|
end
|
|
285
318
|
end
|
|
@@ -337,7 +370,10 @@ module Mxrb
|
|
|
337
370
|
mod.microflows.each do |flow|
|
|
338
371
|
layer, category = flow_location(flow.name)
|
|
339
372
|
relative = File.join(category, files.fetch(flow.id))
|
|
340
|
-
metadata =
|
|
373
|
+
metadata = flow_metadata(
|
|
374
|
+
mod.name, :microflow, flow.name,
|
|
375
|
+
architecture&.fetch(:microflows, [])&.find { _1[:name] == flow.name }
|
|
376
|
+
)
|
|
341
377
|
write(File.join(root, layer, relative), microflow_source(flow, metadata))
|
|
342
378
|
by_layer.fetch(layer) << relative
|
|
343
379
|
end
|
|
@@ -466,7 +502,10 @@ module Mxrb
|
|
|
466
502
|
architecture = architecture_module(mod.name)
|
|
467
503
|
flows.each do |flow|
|
|
468
504
|
relative = File.join("client_actions", files.fetch(flow.id))
|
|
469
|
-
metadata =
|
|
505
|
+
metadata = flow_metadata(
|
|
506
|
+
mod.name, :nanoflow, flow.name,
|
|
507
|
+
architecture&.fetch(:nanoflows, [])&.find { _1[:name] == flow.name }
|
|
508
|
+
)
|
|
470
509
|
write(File.join(presentation, relative), nanoflow_source(flow, metadata))
|
|
471
510
|
paths << relative
|
|
472
511
|
end
|
|
@@ -497,7 +536,7 @@ module Mxrb
|
|
|
497
536
|
end
|
|
498
537
|
end
|
|
499
538
|
|
|
500
|
-
def write_project(project)
|
|
539
|
+
def write_project(project, ruby_sources: false)
|
|
501
540
|
module_loads = project.modules.map do |mod|
|
|
502
541
|
%( evaluate File.join(__dir__, "modules", #{ruby(mod.name)}, "module.rb"))
|
|
503
542
|
end.join("\n")
|
|
@@ -513,6 +552,7 @@ module Mxrb
|
|
|
513
552
|
mendix_version #{ruby(project.mendix_version || "10.18.0")}
|
|
514
553
|
native_units File.join(__dir__, ".mxrb", "native_units.json")
|
|
515
554
|
project_assets File.join(__dir__, ".mxrb", "assets.json"), root: __dir__
|
|
555
|
+
#{' ruby_app_sources File.join(__dir__, ".mxrb", "ruby_sources.json")' if ruby_sources}
|
|
516
556
|
evaluate File.join(__dir__, ".mxrb", "native_units.rb")
|
|
517
557
|
evaluate File.join(__dir__, "app", "security", "security.rb")
|
|
518
558
|
evaluate File.join(__dir__, "app", "navigation", "navigation.rb")
|
|
@@ -824,19 +864,21 @@ module Mxrb
|
|
|
824
864
|
end
|
|
825
865
|
end
|
|
826
866
|
|
|
827
|
-
def unique_entity_filenames(entities)
|
|
828
|
-
|
|
867
|
+
def unique_entity_filenames(entities, route: nil)
|
|
868
|
+
suffix = route == "dtos" ? "_dto" : nil
|
|
869
|
+
unique_filenames(entities, suffix:)
|
|
829
870
|
end
|
|
830
871
|
|
|
831
|
-
def unique_filenames(entities)
|
|
872
|
+
def unique_filenames(entities, suffix: nil)
|
|
832
873
|
used = {}
|
|
833
874
|
entities.to_h do |entity|
|
|
834
875
|
base = underscore(entity.name)
|
|
876
|
+
base = "#{base}#{suffix}" if suffix && !base.end_with?(suffix)
|
|
835
877
|
candidate = "#{base}.rb"
|
|
836
|
-
|
|
878
|
+
sequence = 2
|
|
837
879
|
while used[candidate]
|
|
838
|
-
candidate = "#{base}_#{
|
|
839
|
-
|
|
880
|
+
candidate = "#{base}_#{sequence}.rb"
|
|
881
|
+
sequence += 1
|
|
840
882
|
end
|
|
841
883
|
used[candidate] = true
|
|
842
884
|
[entity.id, candidate]
|
|
@@ -864,6 +906,8 @@ module Mxrb
|
|
|
864
906
|
body << " return_type #{symbol(flow.return_type)}" if flow.return_type.is_a?(String)
|
|
865
907
|
body << " documentation #{ruby(flow.documentation)}" unless flow.documentation.to_s.empty?
|
|
866
908
|
body << " allow_concurrent_execution #{flow.allow_concurrent_execution ? 'true' : 'false'}"
|
|
909
|
+
apply_entity_access = flow.respond_to?(:apply_entity_access) && flow.apply_entity_access
|
|
910
|
+
body << " apply_entity_access #{apply_entity_access ? 'true' : 'false'}"
|
|
867
911
|
body << " mark_as_used #{flow.mark_as_used ? 'true' : 'false'}"
|
|
868
912
|
body << " excluded #{flow.excluded ? 'true' : 'false'}"
|
|
869
913
|
body << " allowed_roles #{flow.allowed_module_roles.map { symbol(_1) }.join(', ')}" unless flow.allowed_module_roles.empty?
|
|
@@ -883,10 +927,12 @@ module Mxrb
|
|
|
883
927
|
elsif objects.any?
|
|
884
928
|
body << " # Native body baseline retained: this graph shape has no typed DSL mapping yet"
|
|
885
929
|
end
|
|
930
|
+
declaration = [symbol(flow.name)]
|
|
931
|
+
declaration << "public: true" if metadata&.fetch(:public, false)
|
|
886
932
|
<<~RUBY
|
|
887
933
|
# frozen_string_literal: true
|
|
888
934
|
|
|
889
|
-
microflow #{
|
|
935
|
+
microflow #{declaration.join(', ')} do
|
|
890
936
|
#{body.join("\n")}
|
|
891
937
|
end
|
|
892
938
|
RUBY
|
|
@@ -927,7 +973,7 @@ module Mxrb
|
|
|
927
973
|
body << " allowed_roles #{page.allowed_module_roles.map { symbol(_1) }.join(', ')}" unless page.allowed_module_roles.empty?
|
|
928
974
|
deep = page_deep_structure(page)
|
|
929
975
|
if (source = metadata&.dig(:data_source) || page.data_source)
|
|
930
|
-
body << " data_source #{source.fetch(:kind)}: #{
|
|
976
|
+
body << " data_source #{source.fetch(:kind)}: #{reference(source.fetch(:name))}"
|
|
931
977
|
end
|
|
932
978
|
body << " deep_structure(#{native_ruby(deep, 2)})" if deep
|
|
933
979
|
metadata&.fetch(:events, [])&.each do |event|
|
|
@@ -1016,7 +1062,9 @@ module Mxrb
|
|
|
1016
1062
|
def render_widget(widget, indent)
|
|
1017
1063
|
pad = " " * indent
|
|
1018
1064
|
child_pad = " " * (indent + 2)
|
|
1019
|
-
|
|
1065
|
+
# Architecture metadata is serialized through JSON inside generated MPRs,
|
|
1066
|
+
# so a second export can surface widget type values as strings.
|
|
1067
|
+
type = widget.fetch(:type).to_sym
|
|
1020
1068
|
options = widget.fetch(:options, {})
|
|
1021
1069
|
|
|
1022
1070
|
# Snippet: snippet :name, from: "..."
|
|
@@ -1856,6 +1904,32 @@ module Mxrb
|
|
|
1856
1904
|
@architecture&.fetch(:modules, [])&.find { _1[:name] == name }
|
|
1857
1905
|
end
|
|
1858
1906
|
|
|
1907
|
+
def infer_public_artifacts(modules)
|
|
1908
|
+
modules.each_with_object({}) do |mod, public_artifacts|
|
|
1909
|
+
mod.pages.each do |page|
|
|
1910
|
+
source = page.data_source
|
|
1911
|
+
next unless source
|
|
1912
|
+
|
|
1913
|
+
target_module, target_name = source.fetch(:name).to_s.split(".", 2)
|
|
1914
|
+
next if target_name.to_s.empty? || target_module == mod.name
|
|
1915
|
+
|
|
1916
|
+
public_artifacts[[target_module, source.fetch(:kind).to_sym, target_name]] = true
|
|
1917
|
+
end
|
|
1918
|
+
end
|
|
1919
|
+
end
|
|
1920
|
+
|
|
1921
|
+
def flow_metadata(module_name, kind, name, metadata)
|
|
1922
|
+
result = metadata ? metadata.dup : {}
|
|
1923
|
+
if @inferred_public_artifacts&.include?([module_name, kind, name])
|
|
1924
|
+
result[:public] = true
|
|
1925
|
+
end
|
|
1926
|
+
result.empty? ? nil : result
|
|
1927
|
+
end
|
|
1928
|
+
|
|
1929
|
+
def reference(value)
|
|
1930
|
+
value.to_s.include?(".") ? ruby(value.to_s) : symbol(value)
|
|
1931
|
+
end
|
|
1932
|
+
|
|
1859
1933
|
def symbol(value)
|
|
1860
1934
|
value.to_s.to_sym.inspect
|
|
1861
1935
|
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'puma'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
6
|
+
module Mxrb
|
|
7
|
+
module Http
|
|
8
|
+
# Case-insensitive HTTP header collection used by the internal request API.
|
|
9
|
+
class Headers
|
|
10
|
+
def initialize(values = {})
|
|
11
|
+
@values = values.to_h { |name, value| [name.to_s.downcase, value.to_s] }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def [](name) = @values[name.to_s.downcase]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
Request = Data.define(:path, :request_method, :body, :query, :headers) do
|
|
18
|
+
def [](name) = headers[name]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Mutable response keeps the existing MXRB dispatchers independent of Rack.
|
|
22
|
+
class Response
|
|
23
|
+
attr_accessor :status, :body
|
|
24
|
+
attr_reader :headers
|
|
25
|
+
|
|
26
|
+
def initialize
|
|
27
|
+
@status = 200
|
|
28
|
+
@body = ''
|
|
29
|
+
@headers = {}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def [](name) = headers[name]
|
|
33
|
+
|
|
34
|
+
def []=(name, value)
|
|
35
|
+
headers[name] = value
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Small embedded-Puma adapter for MXRB's loopback-only HTTP services.
|
|
40
|
+
class Server
|
|
41
|
+
DEFAULT_MAX_THREADS = 5
|
|
42
|
+
|
|
43
|
+
attr_reader :host, :port, :puma
|
|
44
|
+
|
|
45
|
+
def initialize(host:, port:, logger: nil, max_threads: DEFAULT_MAX_THREADS, &dispatcher)
|
|
46
|
+
raise ArgumentError, 'HTTP dispatcher is required' unless dispatcher
|
|
47
|
+
|
|
48
|
+
@host = host.to_s
|
|
49
|
+
@port = Integer(port)
|
|
50
|
+
@dispatcher = dispatcher
|
|
51
|
+
@logger = logger
|
|
52
|
+
@max_threads = Integer(max_threads)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def start
|
|
56
|
+
@puma = Puma::Server.new(method(:call), nil, puma_options)
|
|
57
|
+
@puma.add_tcp_listener(host, port)
|
|
58
|
+
yield(@puma) if block_given?
|
|
59
|
+
@thread = @puma.run
|
|
60
|
+
@thread.join
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def shutdown = @puma&.stop
|
|
64
|
+
|
|
65
|
+
def call(environment)
|
|
66
|
+
request = request_from(environment)
|
|
67
|
+
response = Response.new
|
|
68
|
+
@dispatcher.call(request, response)
|
|
69
|
+
[response.status, response.headers, [response.body.to_s]]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def puma_options
|
|
75
|
+
{
|
|
76
|
+
min_threads: 0,
|
|
77
|
+
max_threads: @max_threads,
|
|
78
|
+
environment: 'production',
|
|
79
|
+
log_writer: @logger || Puma::LogWriter.null,
|
|
80
|
+
queue_requests: true
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def request_from(environment)
|
|
85
|
+
input = environment['rack.input']
|
|
86
|
+
body = input ? input.read.to_s : ''
|
|
87
|
+
input.rewind if input.respond_to?(:rewind)
|
|
88
|
+
Request.new(
|
|
89
|
+
path: "#{environment['SCRIPT_NAME']}#{environment.fetch('PATH_INFO', '/')}",
|
|
90
|
+
request_method: environment.fetch('REQUEST_METHOD', 'GET'),
|
|
91
|
+
body:,
|
|
92
|
+
query: URI.decode_www_form(environment['QUERY_STRING'].to_s).to_h,
|
|
93
|
+
headers: Headers.new(request_headers(environment))
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def request_headers(environment)
|
|
98
|
+
environment.each_with_object({}) do |(name, value), headers|
|
|
99
|
+
next unless name.start_with?('HTTP_')
|
|
100
|
+
|
|
101
|
+
header = name.delete_prefix('HTTP_').split('_').map(&:capitalize).join('-')
|
|
102
|
+
headers[header] = value
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|