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.
Files changed (181) hide show
  1. checksums.yaml +4 -4
  2. data/bin/mxrb +455 -10
  3. data/docs/de-DE/README.md +1 -0
  4. data/docs/de-DE/oql-sql.md +42 -0
  5. data/docs/de-DE/project-structure.md +201 -2
  6. data/docs/de-DE/runtime-ruby.md +104 -0
  7. data/docs/de-DE/runtime-schema-migrations.md +44 -0
  8. data/docs/de-DE/validation-matrix.md +42 -4
  9. data/docs/en-US/README.md +1 -0
  10. data/docs/en-US/oql-sql.md +41 -0
  11. data/docs/en-US/project-structure.md +192 -2
  12. data/docs/en-US/runtime-ruby.md +100 -0
  13. data/docs/en-US/runtime-schema-migrations.md +41 -0
  14. data/docs/en-US/validation-matrix.md +39 -4
  15. data/docs/pt-BR/README.md +1 -0
  16. data/docs/pt-BR/oql-sql.md +41 -0
  17. data/docs/pt-BR/project-structure.md +197 -2
  18. data/docs/pt-BR/runtime-ruby.md +108 -0
  19. data/docs/pt-BR/runtime-schema-migrations.md +40 -0
  20. data/docs/pt-BR/validation-matrix.md +42 -5
  21. data/lib/mxrb/cli/help.rb +426 -0
  22. data/lib/mxrb/cli/release.rb +137 -0
  23. data/lib/mxrb/compiler/compatibility_analyzer.rb +1 -1
  24. data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +8 -1
  25. data/lib/mxrb/compiler/domain_document_compiler.rb +3 -0
  26. data/lib/mxrb/compiler/gallery_bundle_compiler.rb +11 -1
  27. data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +76 -19
  28. data/lib/mxrb/compiler/legacy_custom_widget_compiler.rb +159 -0
  29. data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +43 -7
  30. data/lib/mxrb/compiler/legacy_page_builder.rb +1248 -23
  31. data/lib/mxrb/compiler/page_bundle_compiler.rb +13 -4
  32. data/lib/mxrb/compiler/web_bundle_builder.rb +5 -1
  33. data/lib/mxrb/compiler/web_shell_materializer.rb +42 -2
  34. data/lib/mxrb/compiler/widget_package_extractor.rb +3 -0
  35. data/lib/mxrb/domain_diagram/lifecycle.rb +272 -0
  36. data/lib/mxrb/domain_diagram.rb +409 -0
  37. data/lib/mxrb/dsl/builder.rb +43 -9
  38. data/lib/mxrb/environment.rb +226 -0
  39. data/lib/mxrb/exporter.rb +89 -15
  40. data/lib/mxrb/http/server.rb +107 -0
  41. data/lib/mxrb/initializer.rb +80 -9
  42. data/lib/mxrb/integrity/validator.rb +14 -0
  43. data/lib/mxrb/io/bson_codec.rb +42 -1
  44. data/lib/mxrb/io/mpr_file.rb +146 -3
  45. data/lib/mxrb/model/attribute.rb +3 -0
  46. data/lib/mxrb/model/entity.rb +28 -2
  47. data/lib/mxrb/model/microflow.rb +3 -1
  48. data/lib/mxrb/model/page.rb +126 -15
  49. data/lib/mxrb/model/project.rb +3 -0
  50. data/lib/mxrb/modeler/catalog.rb +178 -0
  51. data/lib/mxrb/oql/reverse_translator.rb +394 -0
  52. data/lib/mxrb/oql/server.rb +4 -8
  53. data/lib/mxrb/oql.rb +23 -1
  54. data/lib/mxrb/project_lifecycle.rb +5 -2
  55. data/lib/mxrb/ruby_app/exporter.rb +1749 -0
  56. data/lib/mxrb/ruby_app/preset.rb +506 -0
  57. data/lib/mxrb/ruby_app/session_manager.rb +122 -0
  58. data/lib/mxrb/ruby_app.rb +1361 -0
  59. data/lib/mxrb/runtime/access_control.rb +408 -0
  60. data/lib/mxrb/runtime/native.rb +1015 -93
  61. data/lib/mxrb/runtime/scheduler.rb +396 -0
  62. data/lib/mxrb/runtime/schema_migrator.rb +598 -0
  63. data/lib/mxrb/runtime/shared_store.rb +335 -0
  64. data/lib/mxrb/runtime/sqlite_store.rb +729 -0
  65. data/lib/mxrb/scaffold/generator.rb +11 -3
  66. data/lib/mxrb/uml/activity_diagram.rb +118 -0
  67. data/lib/mxrb/uml/class_diagram.rb +118 -0
  68. data/lib/mxrb/uml/sequence_diagram.rb +112 -0
  69. data/lib/mxrb/uml/server.rb +146 -0
  70. data/lib/mxrb/uml/support.rb +35 -0
  71. data/lib/mxrb/version.rb +1 -1
  72. data/lib/mxrb/web_ui/assets/abnfDiagram-N423BO3Z-C4JD3cEC.js +1 -0
  73. data/lib/mxrb/web_ui/assets/arc-3Z53kgFp.js +1 -0
  74. data/lib/mxrb/web_ui/assets/architecture-TIHT7OUA-BStBTLea.js +1 -0
  75. data/lib/mxrb/web_ui/assets/architectureDiagram-T3A2C74G-DBwqnM4J.js +36 -0
  76. data/lib/mxrb/web_ui/assets/array-BifhSqXX.js +1 -0
  77. data/lib/mxrb/web_ui/assets/blockDiagram-VBNYF7ZC-DLtuHt-j.js +132 -0
  78. data/lib/mxrb/web_ui/assets/c4Diagram-5PPSVZJV-ConSk_pe.js +10 -0
  79. data/lib/mxrb/web_ui/assets/channel-BhPvD7Dt.js +1 -0
  80. data/lib/mxrb/web_ui/assets/chunk-2GRJ4B5K-D2HpWsf_.js +1 -0
  81. data/lib/mxrb/web_ui/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  82. data/lib/mxrb/web_ui/assets/chunk-4I5QYGJK-CiIdNwaF.js +1 -0
  83. data/lib/mxrb/web_ui/assets/chunk-5RXB4S5H-Bu_AaEkE.js +231 -0
  84. data/lib/mxrb/web_ui/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  85. data/lib/mxrb/web_ui/assets/chunk-6Q2QTUOP-wQkNmt_X.js +88 -0
  86. data/lib/mxrb/web_ui/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
  87. data/lib/mxrb/web_ui/assets/chunk-GF5L2VYU-G8MmiBzU.js +206 -0
  88. data/lib/mxrb/web_ui/assets/chunk-I66GZJ75-CulcrLPh.js +127 -0
  89. data/lib/mxrb/web_ui/assets/chunk-JQJVKLGR-ukjzcdlL.js +156 -0
  90. data/lib/mxrb/web_ui/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  91. data/lib/mxrb/web_ui/assets/chunk-KBJHAD2P-Wm35lYov.js +1 -0
  92. data/lib/mxrb/web_ui/assets/chunk-KEIR6QF5-BICK3FdT.js +161 -0
  93. data/lib/mxrb/web_ui/assets/chunk-NSK5VX7P-D2nYrCY4.js +2 -0
  94. data/lib/mxrb/web_ui/assets/chunk-QR6OTTB3-BrdIzg4Q.js +62 -0
  95. data/lib/mxrb/web_ui/assets/chunk-RYQCIY6F-CCfZMcW6.js +1 -0
  96. data/lib/mxrb/web_ui/assets/chunk-UBXNYLIW-BPEVYsFK.js +1 -0
  97. data/lib/mxrb/web_ui/assets/chunk-W5SLKNZC-2E6OPhA4.js +1 -0
  98. data/lib/mxrb/web_ui/assets/chunk-WRU74C26-CBIvOyWp.js +70 -0
  99. data/lib/mxrb/web_ui/assets/chunk-XXDRQBXY-pH58XAyl.js +1 -0
  100. data/lib/mxrb/web_ui/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  101. data/lib/mxrb/web_ui/assets/classDiagram-JCYQIIEL-xiqo4D89.js +1 -0
  102. data/lib/mxrb/web_ui/assets/classDiagram-v2-OCEON4UE-xiqo4D89.js +1 -0
  103. data/lib/mxrb/web_ui/assets/cose-bilkent-JH36ORCC-t_flPpii.js +1 -0
  104. data/lib/mxrb/web_ui/assets/cynefin-VYW2F7L2-BIqktTEv.js +1 -0
  105. data/lib/mxrb/web_ui/assets/cynefinDiagram-MW4NZA55-C2d--lut.js +62 -0
  106. data/lib/mxrb/web_ui/assets/cytoscape.esm-B-NFISlW.js +321 -0
  107. data/lib/mxrb/web_ui/assets/dagre-Buvkdvvj.js +1 -0
  108. data/lib/mxrb/web_ui/assets/dagre-VZM6K2ZE-CSz2_tb5.js +4 -0
  109. data/lib/mxrb/web_ui/assets/defaultLocale-BFoDCU3G.js +1 -0
  110. data/lib/mxrb/web_ui/assets/diagram-7IWD3JNH-DCt7u7V_.js +30 -0
  111. data/lib/mxrb/web_ui/assets/diagram-B4RE2ZJO-DqZBqkMu.js +3 -0
  112. data/lib/mxrb/web_ui/assets/diagram-LBJQPF4R-ClIYbIsI.js +24 -0
  113. data/lib/mxrb/web_ui/assets/diagram-Q27KOJAE-DFxhvthJ.js +24 -0
  114. data/lib/mxrb/web_ui/assets/diagram-UB23O5K3-BSUTXptv.js +41 -0
  115. data/lib/mxrb/web_ui/assets/dist-D2qOEbeJ.js +1 -0
  116. data/lib/mxrb/web_ui/assets/domain-mGOZ1KCG.css +1 -0
  117. data/lib/mxrb/web_ui/assets/domain-qsjXhy4v.js +1 -0
  118. data/lib/mxrb/web_ui/assets/ebnfDiagram-BXEA7PRR-B5reICEi.js +1 -0
  119. data/lib/mxrb/web_ui/assets/erDiagram-JOGREHBK-Co5Mxn2p.js +85 -0
  120. data/lib/mxrb/web_ui/assets/eventmodeling-45OFAUF4-C1ylhSks.js +1 -0
  121. data/lib/mxrb/web_ui/assets/flowDiagram-UKHOOZJN-CHXaJIaS.js +1 -0
  122. data/lib/mxrb/web_ui/assets/ganttDiagram-PKOTCBZU-DPPPaWaz.js +292 -0
  123. data/lib/mxrb/web_ui/assets/gitGraph-TEB2WS4Q-qS77ywA9.js +1 -0
  124. data/lib/mxrb/web_ui/assets/gitGraphDiagram-DS77QQ5N-DpAv6-BT.js +106 -0
  125. data/lib/mxrb/web_ui/assets/graphlib-DS17s2tU.js +1 -0
  126. data/lib/mxrb/web_ui/assets/info-DKCQHKI2-C9VD1Y4e.js +1 -0
  127. data/lib/mxrb/web_ui/assets/infoDiagram-6WML65LV-Bk1kbcCy.js +2 -0
  128. data/lib/mxrb/web_ui/assets/init-C-OQMol4.js +1 -0
  129. data/lib/mxrb/web_ui/assets/ishikawaDiagram-WSZJBQD7-0gcISBFR.js +70 -0
  130. data/lib/mxrb/web_ui/assets/journeyDiagram-NVQOT4AX-Dt0RQnlh.js +139 -0
  131. data/lib/mxrb/web_ui/assets/jsx-runtime-DV0a5kSb.js +9 -0
  132. data/lib/mxrb/web_ui/assets/kanban-definition-27J2QSJJ-DIPQM7w9.js +89 -0
  133. data/lib/mxrb/web_ui/assets/katex-CXMH3UgJ.js +257 -0
  134. data/lib/mxrb/web_ui/assets/line-C0rYD-tL.js +1 -0
  135. data/lib/mxrb/web_ui/assets/linear-BPPxWorj.js +1 -0
  136. data/lib/mxrb/web_ui/assets/map-BaFkSB1l.js +1 -0
  137. data/lib/mxrb/web_ui/assets/mermaid-parser.core-B_UPzTxa.js +7 -0
  138. data/lib/mxrb/web_ui/assets/mindmap-definition-FAOFIHXS-CdJRSMuk.js +96 -0
  139. data/lib/mxrb/web_ui/assets/modeler-BNqHthwk.css +1 -0
  140. data/lib/mxrb/web_ui/assets/modeler-CQmqflbg.js +1 -0
  141. data/lib/mxrb/web_ui/assets/ordinal-BDEzSJ7C.js +1 -0
  142. data/lib/mxrb/web_ui/assets/packet-7NZHBO7P-CwQ4NSBs.js +1 -0
  143. data/lib/mxrb/web_ui/assets/path-fybaL0A-.js +1 -0
  144. data/lib/mxrb/web_ui/assets/pegDiagram-VL7TDLO6-D40S7ppx.js +1 -0
  145. data/lib/mxrb/web_ui/assets/pie-RZYD4A2V-CJroHMKh.js +1 -0
  146. data/lib/mxrb/web_ui/assets/pieDiagram-7S7Q4E2Y-QB54__BV.js +39 -0
  147. data/lib/mxrb/web_ui/assets/quadrantDiagram-CIZ2JOQS-DBhDS6ag.js +7 -0
  148. data/lib/mxrb/web_ui/assets/radar-I7S5WNFK-DdIUwd2d.js +1 -0
  149. data/lib/mxrb/web_ui/assets/railroad-3IZDKUUU-B1Lw6Xyq.js +1 -0
  150. data/lib/mxrb/web_ui/assets/railroad-abnf-AHOZXSZD-CQgQoeak.js +1 -0
  151. data/lib/mxrb/web_ui/assets/railroad-ebnf-EBAXGLYW-D7qEQ2x8.js +1 -0
  152. data/lib/mxrb/web_ui/assets/railroad-peg-LSFZ7HO6-C005Vxbb.js +1 -0
  153. data/lib/mxrb/web_ui/assets/railroadDiagram-AXF67PYL-BBP2qk7M.js +1 -0
  154. data/lib/mxrb/web_ui/assets/requirementDiagram-LRYGKXZP-D2cLsAoI.js +84 -0
  155. data/lib/mxrb/web_ui/assets/rough.esm-Dy-Kn_BL.js +1 -0
  156. data/lib/mxrb/web_ui/assets/sankeyDiagram-W5VNT64P-Bpi3FiP2.js +40 -0
  157. data/lib/mxrb/web_ui/assets/sequenceDiagram-SI44F4Z6-DCRcDlVe.js +162 -0
  158. data/lib/mxrb/web_ui/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
  159. data/lib/mxrb/web_ui/assets/src-CvxfUak2.js +1 -0
  160. data/lib/mxrb/web_ui/assets/stateDiagram-OKZ733FA-C2GCEoxl.js +1 -0
  161. data/lib/mxrb/web_ui/assets/stateDiagram-v2-UEYNNEHI-D2gWgedh.js +1 -0
  162. data/lib/mxrb/web_ui/assets/swimlanes-SLNWSIFB-CRI9irEW.js +2 -0
  163. data/lib/mxrb/web_ui/assets/swimlanesDiagram-ULZ7WXOC-C5Wb_-hZ.js +8 -0
  164. data/lib/mxrb/web_ui/assets/theme-_mxc7BdY.css +1 -0
  165. data/lib/mxrb/web_ui/assets/timeline-definition-Z64GVDOM-B1-mNg-h.js +120 -0
  166. data/lib/mxrb/web_ui/assets/treeView-QDETBFTQ-7CTvIwKq.js +1 -0
  167. data/lib/mxrb/web_ui/assets/treemap-6X3UGDF4-CtL22pq5.js +1 -0
  168. data/lib/mxrb/web_ui/assets/uml-C3KAFGTq.js +51 -0
  169. data/lib/mxrb/web_ui/assets/uml-Dn-qd01X.css +1 -0
  170. data/lib/mxrb/web_ui/assets/vennDiagram-T6HMQDX7-BzVnUEAy.js +34 -0
  171. data/lib/mxrb/web_ui/assets/wardley-OPB4EBWU-JNi-QNag.js +1 -0
  172. data/lib/mxrb/web_ui/assets/wardleyDiagram-T6FBY63Y-BUwmAQ-B.js +78 -0
  173. data/lib/mxrb/web_ui/assets/xychartDiagram-ELKLHX3M-DEQS8sKp.js +7 -0
  174. data/lib/mxrb/web_ui/domain.html +15 -0
  175. data/lib/mxrb/web_ui/modeler.html +16 -0
  176. data/lib/mxrb/web_ui/uml.html +31 -0
  177. data/lib/mxrb/web_ui.rb +89 -0
  178. data/lib/mxrb/widget_certification.rb +208 -0
  179. data/lib/mxrb/writer.rb +200 -24
  180. data/lib/mxrb.rb +26 -1
  181. 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
- def initialize(mpr_path, output_dir)
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 = architecture&.fetch(:microflows, [])&.find { _1[:name] == flow.name }
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 = architecture&.fetch(:nanoflows, [])&.find { _1[:name] == flow.name }
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
- unique_filenames(entities)
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
- suffix = 2
878
+ sequence = 2
837
879
  while used[candidate]
838
- candidate = "#{base}_#{suffix}.rb"
839
- suffix += 1
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 #{symbol(flow.name)} do
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)}: #{symbol(source.fetch(:name))}"
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
- type = widget.fetch(:type)
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