mendix-ruby-bridge 0.1.1
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 +7 -0
- data/.mxcli-version +2 -0
- data/LICENSE +21 -0
- data/README.md +77 -0
- data/bin/git-mendix +4 -0
- data/bin/mendix-apply +145 -0
- data/bin/mendix-desktop +26 -0
- data/bin/mendix-git +121 -0
- data/bin/mendix-ruby +580 -0
- data/bin/mxcli +20 -0
- data/bin/setup-tools +25 -0
- data/lib/mendix_bridge/backend_server.rb +1404 -0
- data/lib/mendix_bridge/change_planner.rb +594 -0
- data/lib/mendix_bridge/config.rb +89 -0
- data/lib/mendix_bridge/dependency_index.rb +188 -0
- data/lib/mendix_bridge/desktop_app.rb +121 -0
- data/lib/mendix_bridge/document_parser.rb +120 -0
- data/lib/mendix_bridge/domain_parser.rb +103 -0
- data/lib/mendix_bridge/dsl.rb +540 -0
- data/lib/mendix_bridge/enumeration_parser.rb +32 -0
- data/lib/mendix_bridge/git_workflow.rb +321 -0
- data/lib/mendix_bridge/html_viewer.rb +672 -0
- data/lib/mendix_bridge/importer.rb +415 -0
- data/lib/mendix_bridge/inventory.rb +93 -0
- data/lib/mendix_bridge/mdl_generator.rb +343 -0
- data/lib/mendix_bridge/microflow_parser.rb +131 -0
- data/lib/mendix_bridge/migration.rb +290 -0
- data/lib/mendix_bridge/migration_executor.rb +234 -0
- data/lib/mendix_bridge/model.rb +204 -0
- data/lib/mendix_bridge/page_parser.rb +95 -0
- data/lib/mendix_bridge/presenter.rb +35 -0
- data/lib/mendix_bridge/project_creator.rb +181 -0
- data/lib/mendix_bridge/ruby_inventory_generator.rb +63 -0
- data/lib/mendix_bridge/security_parser.rb +72 -0
- data/lib/mendix_bridge/snapshot_diff.rb +58 -0
- data/lib/mendix_bridge/validator.rb +46 -0
- data/lib/mendix_bridge/version.rb +5 -0
- data/lib/mendix_bridge/visual_entity_plan.rb +176 -0
- data/lib/mendix_bridge.rb +127 -0
- data/share/applications/mendix-ruby-bridge.desktop +12 -0
- data/share/icons/hicolor/128x128/apps/mendix-ruby-bridge.png +0 -0
- data/share/icons/hicolor/256x256/apps/mendix-ruby-bridge.png +0 -0
- data/share/icons/hicolor/32x32/apps/mendix-ruby-bridge.png +0 -0
- data/share/icons/hicolor/48x48/apps/mendix-ruby-bridge.png +0 -0
- data/share/icons/hicolor/512x512/apps/mendix-ruby-bridge.png +0 -0
- data/share/icons/hicolor/64x64/apps/mendix-ruby-bridge.png +0 -0
- data/web/dist/apple-touch-icon.png +0 -0
- data/web/dist/assets/index-BO6iPT1P.css +1 -0
- data/web/dist/assets/index-JGODw81W.js +27 -0
- data/web/dist/brand/mendix-ruby-bridge.png +0 -0
- data/web/dist/favicon.png +0 -0
- data/web/dist/icons.svg +24 -0
- data/web/dist/index.html +17 -0
- metadata +179 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MendixBridge
|
|
4
|
+
class DependencyIndex
|
|
5
|
+
Edge = Data.define(:from, :to, :kind, :path) do
|
|
6
|
+
def to_h
|
|
7
|
+
{ from:, to:, kind:, path: }
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
UNQUALIFIED_REFERENCE_KEYS = %w[
|
|
12
|
+
included_in_user_roles manageable_roles
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
KIND_KEYS = {
|
|
16
|
+
"calls" => "call",
|
|
17
|
+
"microflow_calls" => "microflow_call",
|
|
18
|
+
"nanoflow_calls" => "nanoflow_call",
|
|
19
|
+
"javascript_action_calls" => "javascript_action_call",
|
|
20
|
+
"page_links" => "page_link",
|
|
21
|
+
"layout" => "layout",
|
|
22
|
+
"structure" => "mapping_structure",
|
|
23
|
+
"generalization" => "generalization",
|
|
24
|
+
"from" => "association_source",
|
|
25
|
+
"to" => "association_target",
|
|
26
|
+
"execute_roles" => "execute_role",
|
|
27
|
+
"view_roles" => "view_role",
|
|
28
|
+
"module_roles" => "module_role",
|
|
29
|
+
"role" => "security_role",
|
|
30
|
+
"home_page" => "navigation_home",
|
|
31
|
+
"login_page" => "navigation_login",
|
|
32
|
+
"not_found_page" => "navigation_not_found",
|
|
33
|
+
"target" => "target",
|
|
34
|
+
"type" => "type_reference"
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
attr_reader :edges
|
|
38
|
+
|
|
39
|
+
def self.load(path, inventory:)
|
|
40
|
+
data = JSON.parse(File.read(path))
|
|
41
|
+
edges = data.fetch("edges").map do |edge|
|
|
42
|
+
Edge.new(
|
|
43
|
+
from: edge.fetch("from"),
|
|
44
|
+
to: edge.fetch("to"),
|
|
45
|
+
kind: edge.fetch("kind"),
|
|
46
|
+
path: edge.fetch("path")
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
new(inventory, edges:)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def initialize(inventory, edges: nil)
|
|
53
|
+
@inventory = inventory
|
|
54
|
+
@names = inventory.elements.filter_map(&:qualified_name).to_set
|
|
55
|
+
@qualified_names = @names.select { |name| name.include?(".") }.to_set
|
|
56
|
+
@edges = (edges || build_edges).freeze
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def dependencies(name, transitive: false)
|
|
60
|
+
traverse(name, direction: :forward, transitive:)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def dependents(name, transitive: false)
|
|
64
|
+
traverse(name, direction: :reverse, transitive:)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def callers(name, transitive: false)
|
|
68
|
+
select_edges(
|
|
69
|
+
dependents(name, transitive:),
|
|
70
|
+
%w[call microflow_call nanoflow_call javascript_action_call]
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def callees(name, transitive: false)
|
|
75
|
+
select_edges(
|
|
76
|
+
dependencies(name, transitive:),
|
|
77
|
+
%w[call microflow_call nanoflow_call javascript_action_call]
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def impact(name)
|
|
82
|
+
dependents(name, transitive: true)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def to_h
|
|
86
|
+
{
|
|
87
|
+
schema_version: 1,
|
|
88
|
+
nodes: @names.length,
|
|
89
|
+
edges: edges.map(&:to_h)
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def build_edges
|
|
96
|
+
found = []
|
|
97
|
+
@inventory.elements.each do |element|
|
|
98
|
+
next unless element.qualified_name && element.details
|
|
99
|
+
|
|
100
|
+
walk(
|
|
101
|
+
element.details.reject { |key, _value| %w[mdl raw].include?(key) },
|
|
102
|
+
source: element.qualified_name,
|
|
103
|
+
path: [],
|
|
104
|
+
found:
|
|
105
|
+
)
|
|
106
|
+
scan_mdl(element, found)
|
|
107
|
+
end
|
|
108
|
+
found.uniq { |edge| [edge.from, edge.to, edge.kind, edge.path] }
|
|
109
|
+
.sort_by { |edge| [edge.from, edge.to, edge.kind, edge.path] }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def walk(value, source:, path:, found:)
|
|
113
|
+
case value
|
|
114
|
+
when Hash
|
|
115
|
+
value.each do |key, child|
|
|
116
|
+
walk(child, source:, path: [*path, key.to_s], found:)
|
|
117
|
+
end
|
|
118
|
+
when Array
|
|
119
|
+
value.each_with_index do |child, index|
|
|
120
|
+
walk(child, source:, path: [*path, index.to_s], found:)
|
|
121
|
+
end
|
|
122
|
+
when String
|
|
123
|
+
references(value, path.last).each do |target|
|
|
124
|
+
add_edge(source, target, path, found)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def references(value, key)
|
|
130
|
+
matches = value.scan(/[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*/)
|
|
131
|
+
.select { |name| @qualified_names.include?(name) }
|
|
132
|
+
if UNQUALIFIED_REFERENCE_KEYS.include?(key) && @names.include?(value)
|
|
133
|
+
matches << value
|
|
134
|
+
end
|
|
135
|
+
matches.uniq
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def scan_mdl(element, found)
|
|
139
|
+
mdl = element.details["mdl"]
|
|
140
|
+
return unless mdl
|
|
141
|
+
|
|
142
|
+
references(mdl, nil).each do |target|
|
|
143
|
+
add_edge(element.qualified_name, target, ["mdl"], found, kind: "mdl_reference")
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def add_edge(source, target, path, found, kind: nil)
|
|
148
|
+
return if source == target
|
|
149
|
+
|
|
150
|
+
key = path.reverse.find { |part| !part.match?(/\A\d+\z/) }
|
|
151
|
+
found << Edge.new(
|
|
152
|
+
from: source,
|
|
153
|
+
to: target,
|
|
154
|
+
kind: kind || KIND_KEYS.fetch(key, "reference"),
|
|
155
|
+
path: path.join(".")
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def traverse(name, direction:, transitive:)
|
|
160
|
+
raise ArgumentError, "unknown inventory element: #{name}" unless @names.include?(name)
|
|
161
|
+
|
|
162
|
+
selected = []
|
|
163
|
+
frontier = [name]
|
|
164
|
+
visited = Set[name]
|
|
165
|
+
loop do
|
|
166
|
+
current = frontier.shift
|
|
167
|
+
break unless current
|
|
168
|
+
|
|
169
|
+
matches = edges.select do |edge|
|
|
170
|
+
direction == :forward ? edge.from == current : edge.to == current
|
|
171
|
+
end
|
|
172
|
+
matches.each do |edge|
|
|
173
|
+
selected << edge
|
|
174
|
+
neighbor = direction == :forward ? edge.to : edge.from
|
|
175
|
+
next unless transitive && visited.add?(neighbor)
|
|
176
|
+
|
|
177
|
+
frontier << neighbor
|
|
178
|
+
end
|
|
179
|
+
break unless transitive || frontier.any?
|
|
180
|
+
end
|
|
181
|
+
selected.uniq { |edge| [edge.from, edge.to, edge.kind, edge.path] }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def select_edges(selected, kinds)
|
|
185
|
+
selected.select { |edge| kinds.include?(edge.kind) }
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "timeout"
|
|
5
|
+
|
|
6
|
+
module MendixBridge
|
|
7
|
+
class DesktopApp
|
|
8
|
+
APP_TITLE = "Mendix Ruby Bridge"
|
|
9
|
+
WINDOW_WIDTH = 1280
|
|
10
|
+
WINDOW_HEIGHT = 800
|
|
11
|
+
READY_TIMEOUT = 15
|
|
12
|
+
|
|
13
|
+
def initialize(inventory_dir:, bridge_dir: nil)
|
|
14
|
+
@inventory_dir = File.expand_path(inventory_dir)
|
|
15
|
+
@bridge_dir = bridge_dir || File.expand_path("../..", __dir__)
|
|
16
|
+
@server_thread = nil
|
|
17
|
+
@backend = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run
|
|
21
|
+
require_gtk!
|
|
22
|
+
configure_env!
|
|
23
|
+
|
|
24
|
+
port = free_port
|
|
25
|
+
start_backend(port)
|
|
26
|
+
wait_for_backend(port)
|
|
27
|
+
|
|
28
|
+
Gtk.init
|
|
29
|
+
window = build_window(port)
|
|
30
|
+
window.show_all
|
|
31
|
+
Gtk.main
|
|
32
|
+
ensure
|
|
33
|
+
@backend&.shutdown
|
|
34
|
+
@server_thread&.kill
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def configure_env!
|
|
40
|
+
# WebKit2GTK crashes on Wayland with a protocol error; XWayland is stable.
|
|
41
|
+
# GPU compositing also fails on XWayland with some drivers (GBM buffer errors).
|
|
42
|
+
ENV["GDK_BACKEND"] ||= "x11"
|
|
43
|
+
ENV["WEBKIT_DISABLE_COMPOSITING_MODE"] ||= "1"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def require_gtk!
|
|
47
|
+
require "gtk3"
|
|
48
|
+
require "webkit2-gtk"
|
|
49
|
+
rescue LoadError => e
|
|
50
|
+
abort <<~MSG
|
|
51
|
+
mendix-desktop requires GTK3 and WebKit2 Ruby bindings.
|
|
52
|
+
|
|
53
|
+
Install them with:
|
|
54
|
+
gem install gtk3 webkit2-gtk
|
|
55
|
+
|
|
56
|
+
Or install the system packages:
|
|
57
|
+
Ubuntu/Debian: sudo apt install ruby-gtk3 gir1.2-webkit2-4.0
|
|
58
|
+
Arch/Omarchy: sudo pacman -S ruby-gtk3 webkit2gtk
|
|
59
|
+
|
|
60
|
+
Original error: #{e.message}
|
|
61
|
+
MSG
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def free_port
|
|
65
|
+
server = TCPServer.new("127.0.0.1", 0)
|
|
66
|
+
port = server.addr[1]
|
|
67
|
+
server.close
|
|
68
|
+
port
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def start_backend(port)
|
|
72
|
+
web_root = File.join(@bridge_dir, "web", "dist")
|
|
73
|
+
mxcli = File.join(@bridge_dir, "bin", "mxcli")
|
|
74
|
+
|
|
75
|
+
@server_thread = Thread.new do
|
|
76
|
+
@backend = BackendServer.new(
|
|
77
|
+
inventory_dir: @inventory_dir,
|
|
78
|
+
web_root: web_root,
|
|
79
|
+
mxcli: mxcli,
|
|
80
|
+
bind: "127.0.0.1",
|
|
81
|
+
port: port
|
|
82
|
+
)
|
|
83
|
+
@backend.start
|
|
84
|
+
rescue => e
|
|
85
|
+
warn "mendix-desktop: backend error: #{e.message}"
|
|
86
|
+
end
|
|
87
|
+
@server_thread.abort_on_exception = false
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def wait_for_backend(port)
|
|
91
|
+
Timeout.timeout(READY_TIMEOUT) do
|
|
92
|
+
loop do
|
|
93
|
+
TCPSocket.new("127.0.0.1", port).close
|
|
94
|
+
break
|
|
95
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
|
|
96
|
+
sleep 0.1
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
rescue Timeout::Error
|
|
100
|
+
abort "mendix-desktop: backend did not start within #{READY_TIMEOUT}s"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def build_window(port)
|
|
104
|
+
window = Gtk::Window.new
|
|
105
|
+
window.title = APP_TITLE
|
|
106
|
+
window.set_default_size(WINDOW_WIDTH, WINDOW_HEIGHT)
|
|
107
|
+
window.set_window_position(:center)
|
|
108
|
+
|
|
109
|
+
webview = WebKit2Gtk::WebView.new
|
|
110
|
+
webview.load_uri("http://127.0.0.1:#{port}")
|
|
111
|
+
|
|
112
|
+
window.add(webview)
|
|
113
|
+
window.signal_connect("destroy") do
|
|
114
|
+
@backend&.shutdown
|
|
115
|
+
Gtk.main_quit
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
window
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MendixBridge
|
|
4
|
+
class DocumentParser
|
|
5
|
+
class << self
|
|
6
|
+
def parse(type, description)
|
|
7
|
+
mdl = description.fetch("mdl")
|
|
8
|
+
details = case type
|
|
9
|
+
when "constant" then parse_constant(mdl)
|
|
10
|
+
when "javaaction" then parse_java_action(mdl)
|
|
11
|
+
when "importmapping", "exportmapping" then parse_mapping(mdl)
|
|
12
|
+
when "layout", "snippet" then parse_ui_document(type, mdl)
|
|
13
|
+
when "navprofile" then parse_navigation(mdl)
|
|
14
|
+
else {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
details.merge("mdl" => mdl, "parse_status" => "parsed")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def parse_constant(mdl)
|
|
23
|
+
{
|
|
24
|
+
"data_type" => mdl[/^\s*type\s+(.+)$/i, 1]&.strip,
|
|
25
|
+
"default" => unquote(mdl[/^\s*default\s+(.+)$/i, 1]&.strip),
|
|
26
|
+
"folder" => quoted_setting(mdl, "folder")
|
|
27
|
+
}.compact
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def parse_java_action(mdl)
|
|
31
|
+
header = mdl.match(
|
|
32
|
+
/\bjava\s+action\s+[\w.]+\s*\((?<parameters>.*?)\)\s*returns\s+(?<returns>\S+)/im
|
|
33
|
+
)
|
|
34
|
+
{
|
|
35
|
+
"parameters" => parse_action_parameters(header&.[](:parameters).to_s),
|
|
36
|
+
"return_type" => header&.[](:returns),
|
|
37
|
+
"source_available" => !mdl.include?("source not available")
|
|
38
|
+
}.compact
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def parse_action_parameters(source)
|
|
42
|
+
source.lines.filter_map do |line|
|
|
43
|
+
match = line.strip.delete_suffix(",").match(
|
|
44
|
+
/\A(\w+):\s*(.+?)(?:\s+(not null))?(?:\s*--\s*(.*))?\z/i
|
|
45
|
+
)
|
|
46
|
+
next unless match
|
|
47
|
+
|
|
48
|
+
{
|
|
49
|
+
"name" => match[1],
|
|
50
|
+
"type" => match[2].strip,
|
|
51
|
+
"required" => !match[3].nil?,
|
|
52
|
+
"description" => match[4]
|
|
53
|
+
}.compact
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def parse_mapping(mdl)
|
|
58
|
+
{
|
|
59
|
+
"direction" => mdl[/\bcreate\s+(import|export)\s+mapping\b/i, 1]&.downcase,
|
|
60
|
+
"structure" => mdl[/\bwith\s+(?:json|xml)\s+structure\s+([\w.]+)/i, 1],
|
|
61
|
+
"entities" => mdl.scan(
|
|
62
|
+
/^\s*(?:create\s+)?([\w.]+)\s*\{\s*$/
|
|
63
|
+
).flatten.uniq,
|
|
64
|
+
"attribute_mappings" => mdl.scan(
|
|
65
|
+
/^\s*(\w+)\s*=\s*([\w.]+)\s*,?\s*$/
|
|
66
|
+
).map { |attribute, member| { "attribute" => attribute, "member" => member } }
|
|
67
|
+
}.compact
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def parse_ui_document(type, mdl)
|
|
71
|
+
{
|
|
72
|
+
"document_type" => type,
|
|
73
|
+
"layout_type" => mdl[/^--\s*Layout Type:\s*(.+)$/i, 1]&.strip,
|
|
74
|
+
"folder" => quoted_setting(mdl, "Folder"),
|
|
75
|
+
"studio_pro_only" => mdl.match?(/cannot be created via MDL/i)
|
|
76
|
+
}.compact
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def parse_navigation(mdl)
|
|
80
|
+
default_home = mdl.lines.filter_map do |line|
|
|
81
|
+
match = line.match(/^\s*home\s+page\s+([\w.]+)(?:\s+for\s+[\w.]+)?/i)
|
|
82
|
+
match[1] if match && !line.match?(/\s+for\s+[\w.]+/i)
|
|
83
|
+
end.first
|
|
84
|
+
{
|
|
85
|
+
"kind" => mdl[/^--\s*Kind:\s*(.+)$/i, 1]&.strip,
|
|
86
|
+
"home_page" => default_home,
|
|
87
|
+
"role_home_pages" => mdl.scan(
|
|
88
|
+
/^\s*home\s+page\s+([\w.]+)\s+for\s+([\w.]+)/i
|
|
89
|
+
).map { |page, role| { "role" => role, "page" => page } },
|
|
90
|
+
"login_page" => mdl[/^\s*login\s+page\s+([\w.]+)/i, 1],
|
|
91
|
+
"not_found_page" => mdl[/^\s*not\s+found\s+page\s+([\w.]+)/i, 1],
|
|
92
|
+
"menu_groups" => mdl.scan(
|
|
93
|
+
/^\s*menu\s+'((?:''|[^'])+)'\s*\(/i
|
|
94
|
+
).flatten.map { |caption| caption.gsub("''", "'") },
|
|
95
|
+
"menu_items" => mdl.scan(
|
|
96
|
+
/^\s*menu\s+item\s+'((?:''|[^'])+)'\s+(page|microflow|nanoflow)\s+([\w.]+)\s*;/i
|
|
97
|
+
).map do |caption, action, target|
|
|
98
|
+
{
|
|
99
|
+
"caption" => caption.gsub("''", "'"),
|
|
100
|
+
"action" => action.downcase,
|
|
101
|
+
"target" => target
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
}.compact
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def quoted_setting(mdl, name)
|
|
108
|
+
value = mdl[/^\s*#{Regexp.escape(name)}\s+'((?:''|[^'])*)'/i, 1]
|
|
109
|
+
value&.gsub("''", "'")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def unquote(value)
|
|
113
|
+
return unless value
|
|
114
|
+
return value unless value.start_with?("'") && value.end_with?("'")
|
|
115
|
+
|
|
116
|
+
value[1...-1].gsub("''", "'")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MendixBridge
|
|
4
|
+
class DomainParser
|
|
5
|
+
class << self
|
|
6
|
+
def parse(description)
|
|
7
|
+
case description.fetch("type")
|
|
8
|
+
when "entity" then parse_entity(description)
|
|
9
|
+
when "association" then parse_association(description)
|
|
10
|
+
else { "mdl" => description["mdl"] }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def parse_entity(description)
|
|
17
|
+
mdl = description.fetch("mdl")
|
|
18
|
+
declaration = mdl.match(
|
|
19
|
+
/\b(?<kind>persistent|non-persistent|external)\s+entity\s+(?<name>[\w.]+)(?:\s+extends\s+(?<generalization>[\w.]+))?\s*\((?<body>.*?)\);/im
|
|
20
|
+
)
|
|
21
|
+
return { "mdl" => mdl, "parse_status" => "unsupported" } unless declaration
|
|
22
|
+
|
|
23
|
+
{
|
|
24
|
+
"kind" => declaration[:kind].downcase,
|
|
25
|
+
"persistable" => declaration[:kind].casecmp?("persistent"),
|
|
26
|
+
"generalization" => declaration[:generalization],
|
|
27
|
+
"attributes" => parse_attributes(declaration[:body]),
|
|
28
|
+
"access_rules" => parse_access_rules(mdl),
|
|
29
|
+
"mdl" => mdl,
|
|
30
|
+
"parse_status" => "parsed"
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def parse_attributes(body)
|
|
35
|
+
body.lines.filter_map do |line|
|
|
36
|
+
declaration = line.strip.delete_suffix(",")
|
|
37
|
+
next if declaration.empty? || declaration.start_with?("@", "--")
|
|
38
|
+
|
|
39
|
+
match = declaration.match(/\A(?<name>\w+):\s*(?<rest>.+)\z/)
|
|
40
|
+
next unless match
|
|
41
|
+
|
|
42
|
+
rest = match[:rest]
|
|
43
|
+
default = rest[/\s+default\s+(.+)\z/i, 1]
|
|
44
|
+
type = rest.sub(/\s+default\s+.+\z/i, "").sub(/\s+not\s+null\z/i, "")
|
|
45
|
+
|
|
46
|
+
{
|
|
47
|
+
"name" => match[:name],
|
|
48
|
+
"type" => type,
|
|
49
|
+
"required" => rest.match?(/\s+not\s+null(?:\s|\z)/i),
|
|
50
|
+
"default" => default,
|
|
51
|
+
"declaration" => declaration
|
|
52
|
+
}.compact
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def parse_association(description)
|
|
57
|
+
mdl = description.fetch("mdl")
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
"from" => mdl[/^from\s+(\S+)\s+to\s+\S+/i, 1],
|
|
61
|
+
"to" => mdl[/^from\s+\S+\s+to\s+(\S+)/i, 1],
|
|
62
|
+
"association_type" => mdl[/^type\s+([^;\s]+)/i, 1],
|
|
63
|
+
"owner" => mdl[/^owner\s+([^;\s]+)/i, 1],
|
|
64
|
+
"storage" => mdl[/^storage\s+([^;\s]+)/i, 1],
|
|
65
|
+
"delete_behavior" => mdl[/^delete_behavior\s+([^;]+)/i, 1],
|
|
66
|
+
"mdl" => mdl,
|
|
67
|
+
"parse_status" => "parsed"
|
|
68
|
+
}.compact
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def parse_access_rules(mdl)
|
|
72
|
+
mdl.lines.filter_map do |line|
|
|
73
|
+
match = line.strip.match(
|
|
74
|
+
/\Agrant\s+(?<role>[\w.]+)\s+on\s+(?<entity>[\w.]+)\s+\((?<permissions>.*)\)(?:\s+where\s+'(?<xpath>.*)')?;\z/i
|
|
75
|
+
)
|
|
76
|
+
next unless match
|
|
77
|
+
|
|
78
|
+
permissions = match[:permissions]
|
|
79
|
+
{
|
|
80
|
+
"role" => match[:role],
|
|
81
|
+
"create" => permission?(permissions, "create"),
|
|
82
|
+
"delete" => permission?(permissions, "delete"),
|
|
83
|
+
"read" => attribute_permission(permissions, "read"),
|
|
84
|
+
"write" => attribute_permission(permissions, "write"),
|
|
85
|
+
"xpath" => match[:xpath]&.gsub("''", "'")
|
|
86
|
+
}.compact
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def permission?(permissions, name)
|
|
91
|
+
permissions.match?(/(?:\A|,\s*)#{Regexp.escape(name)}(?:,|\z)/i)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def attribute_permission(permissions, name)
|
|
95
|
+
match = permissions.match(/\b#{Regexp.escape(name)}\s+(\*|\([^)]*\))/i)
|
|
96
|
+
return [] unless match
|
|
97
|
+
return "*" if match[1] == "*"
|
|
98
|
+
|
|
99
|
+
match[1].delete_prefix("(").delete_suffix(")").split(",").map(&:strip)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|