mxrb 0.1.0
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/LICENSE +21 -0
- data/README.de-DE.md +108 -0
- data/README.md +172 -0
- data/README.pt-BR.md +109 -0
- data/bin/mxrb +1613 -0
- data/docs/README.md +10 -0
- data/docs/de-DE/README.md +24 -0
- data/docs/de-DE/architectural-patterns.md +26 -0
- data/docs/de-DE/architectural-standard.md +48 -0
- data/docs/de-DE/architecture.md +76 -0
- data/docs/de-DE/compiler.md +162 -0
- data/docs/de-DE/conventions.md +19 -0
- data/docs/de-DE/design-system.md +52 -0
- data/docs/de-DE/entity-dsl.md +53 -0
- data/docs/de-DE/native-runtime-quality-report.md +74 -0
- data/docs/de-DE/oql-sql.md +211 -0
- data/docs/de-DE/platform-api-integration.md +36 -0
- data/docs/de-DE/platform-operations.md +243 -0
- data/docs/de-DE/project-structure.md +32 -0
- data/docs/de-DE/ruby-first-roadmap.md +116 -0
- data/docs/de-DE/scaffolds.md +59 -0
- data/docs/de-DE/semantic-refactoring.md +56 -0
- data/docs/de-DE/team-server.md +33 -0
- data/docs/de-DE/validation-matrix.md +64 -0
- data/docs/de-DE/vetclinic-acceptance.md +24 -0
- data/docs/de-DE/writing.md +138 -0
- data/docs/en-US/README.md +24 -0
- data/docs/en-US/architectural-patterns.md +151 -0
- data/docs/en-US/architectural-standard.md +89 -0
- data/docs/en-US/architecture.md +170 -0
- data/docs/en-US/compiler.md +157 -0
- data/docs/en-US/conventions.md +82 -0
- data/docs/en-US/design-system.md +163 -0
- data/docs/en-US/entity-dsl.md +53 -0
- data/docs/en-US/native-runtime-quality-report.md +73 -0
- data/docs/en-US/oql-sql.md +201 -0
- data/docs/en-US/platform-api-integration.md +36 -0
- data/docs/en-US/platform-operations.md +244 -0
- data/docs/en-US/project-structure.md +49 -0
- data/docs/en-US/ruby-first-roadmap.md +186 -0
- data/docs/en-US/scaffolds.md +57 -0
- data/docs/en-US/semantic-refactoring.md +198 -0
- data/docs/en-US/team-server.md +32 -0
- data/docs/en-US/validation-matrix.md +257 -0
- data/docs/en-US/vetclinic-acceptance.md +23 -0
- data/docs/en-US/writing.md +655 -0
- data/docs/pt-BR/README.md +24 -0
- data/docs/pt-BR/architectural-patterns.md +25 -0
- data/docs/pt-BR/architectural-standard.md +533 -0
- data/docs/pt-BR/architecture.md +300 -0
- data/docs/pt-BR/compiler.md +162 -0
- data/docs/pt-BR/conventions.md +19 -0
- data/docs/pt-BR/design-system.md +65 -0
- data/docs/pt-BR/entity-dsl.md +116 -0
- data/docs/pt-BR/native-runtime-quality-report.md +73 -0
- data/docs/pt-BR/oql-sql.md +208 -0
- data/docs/pt-BR/platform-api-integration.md +36 -0
- data/docs/pt-BR/platform-operations.md +260 -0
- data/docs/pt-BR/project-structure.md +32 -0
- data/docs/pt-BR/ruby-first-roadmap.md +239 -0
- data/docs/pt-BR/scaffolds.md +104 -0
- data/docs/pt-BR/semantic-refactoring.md +55 -0
- data/docs/pt-BR/team-server.md +59 -0
- data/docs/pt-BR/validation-matrix.md +86 -0
- data/docs/pt-BR/vetclinic-acceptance.md +50 -0
- data/docs/pt-BR/writing.md +177 -0
- data/examples/sudoku_evaluation.rb +14 -0
- data/examples/sudoku_functional_test.rb +28 -0
- data/lib/mxrb/architecture/graph.rb +124 -0
- data/lib/mxrb/architecture/validator.rb +98 -0
- data/lib/mxrb/benchmark.rb +33 -0
- data/lib/mxrb/compare.rb +390 -0
- data/lib/mxrb/compiler/adapter.rb +96 -0
- data/lib/mxrb/compiler/artifact_document_compiler.rb +102 -0
- data/lib/mxrb/compiler/artifact_materializer.rb +53 -0
- data/lib/mxrb/compiler/client_model_materializer.rb +42 -0
- data/lib/mxrb/compiler/code_action_document_compiler.rb +64 -0
- data/lib/mxrb/compiler/code_action_materializer.rb +35 -0
- data/lib/mxrb/compiler/code_action_type_compiler.rb +32 -0
- data/lib/mxrb/compiler/combo_box_bundle_compiler.rb +314 -0
- data/lib/mxrb/compiler/compatibility_analyzer.rb +169 -0
- data/lib/mxrb/compiler/constants_materializer.rb +111 -0
- data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +290 -0
- data/lib/mxrb/compiler/database_connector_action_compiler.rb +280 -0
- data/lib/mxrb/compiler/deployment_asset_copier.rb +60 -0
- data/lib/mxrb/compiler/deployment_bootstrapper.rb +208 -0
- data/lib/mxrb/compiler/deployment_materializer.rb +43 -0
- data/lib/mxrb/compiler/deployment_metadata.rb +64 -0
- data/lib/mxrb/compiler/domain_document_compiler.rb +153 -0
- data/lib/mxrb/compiler/domain_model_materializer.rb +35 -0
- data/lib/mxrb/compiler/domain_security_compiler.rb +82 -0
- data/lib/mxrb/compiler/gallery_bundle_compiler.rb +190 -0
- data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +281 -0
- data/lib/mxrb/compiler/image_bundle_compiler.rb +216 -0
- data/lib/mxrb/compiler/java_proxy_generator.rb +509 -0
- data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +267 -0
- data/lib/mxrb/compiler/legacy_page_builder.rb +287 -0
- data/lib/mxrb/compiler/mda.rb +99 -0
- data/lib/mxrb/compiler/microflow_document_compiler.rb +85 -0
- data/lib/mxrb/compiler/microflow_materializer.rb +48 -0
- data/lib/mxrb/compiler/microflow_node_compiler.rb +250 -0
- data/lib/mxrb/compiler/model_package.rb +116 -0
- data/lib/mxrb/compiler/model_values.rb +67 -0
- data/lib/mxrb/compiler/nanoflow_program_compiler.rb +483 -0
- data/lib/mxrb/compiler/navigation_document_compiler.rb +93 -0
- data/lib/mxrb/compiler/packager.rb +100 -0
- data/lib/mxrb/compiler/page_bundle_builder.rb +56 -0
- data/lib/mxrb/compiler/page_bundle_compiler.rb +2004 -0
- data/lib/mxrb/compiler/page_document_compiler.rb +77 -0
- data/lib/mxrb/compiler/portable_packager.rb +356 -0
- data/lib/mxrb/compiler/project_jar_archive.rb +84 -0
- data/lib/mxrb/compiler/project_jar_builder.rb +121 -0
- data/lib/mxrb/compiler/project_materializer.rb +139 -0
- data/lib/mxrb/compiler/project_model_orderer.rb +52 -0
- data/lib/mxrb/compiler/runtime_data_types.rb +31 -0
- data/lib/mxrb/compiler/runtime_model_schema.rb +90 -0
- data/lib/mxrb/compiler/schemas/runtime-10.24.0.73019.json +1052 -0
- data/lib/mxrb/compiler/schemas/runtime-11.json +1215 -0
- data/lib/mxrb/compiler/schemas/runtime-6.10.8.json +1148 -0
- data/lib/mxrb/compiler/schemas/runtime-7.17.0.json +1144 -0
- data/lib/mxrb/compiler/schemas/runtime-7.5.0.json +1215 -0
- data/lib/mxrb/compiler/schemas/runtime-9.6.1.29396.json +868 -0
- data/lib/mxrb/compiler/schemas/system-model-10.24.0.73019.b64 +1135 -0
- data/lib/mxrb/compiler/schemas/system-model-11.12.1.b64 +815 -0
- data/lib/mxrb/compiler/schemas/system-model-6.10.8.b64 +497 -0
- data/lib/mxrb/compiler/schemas/system-model-7.17.0.b64 +519 -0
- data/lib/mxrb/compiler/schemas/system-model-7.5.0.b64 +504 -0
- data/lib/mxrb/compiler/schemas/system-model-9.6.1.29396.b64 +816 -0
- data/lib/mxrb/compiler/security_materializer.rb +104 -0
- data/lib/mxrb/compiler/settings_document_compiler.rb +45 -0
- data/lib/mxrb/compiler/settings_materializer.rb +25 -0
- data/lib/mxrb/compiler/source_model.rb +128 -0
- data/lib/mxrb/compiler/system_model_seed.rb +88 -0
- data/lib/mxrb/compiler/system_queue_materializer.rb +54 -0
- data/lib/mxrb/compiler/system_text_materializer.rb +49 -0
- data/lib/mxrb/compiler/translation_materializer.rb +95 -0
- data/lib/mxrb/compiler/web_bundle_builder.rb +155 -0
- data/lib/mxrb/compiler/web_list_data_source.rb +139 -0
- data/lib/mxrb/compiler/web_operation_compiler.rb +614 -0
- data/lib/mxrb/compiler/web_shell_materializer.rb +194 -0
- data/lib/mxrb/compiler/widget_package_extractor.rb +63 -0
- data/lib/mxrb/doctor.rb +116 -0
- data/lib/mxrb/dsl/builder.rb +1636 -0
- data/lib/mxrb/errors.rb +18 -0
- data/lib/mxrb/evaluation.rb +131 -0
- data/lib/mxrb/exporter.rb +1716 -0
- data/lib/mxrb/frontend/migrator.rb +775 -0
- data/lib/mxrb/functional.rb +307 -0
- data/lib/mxrb/github/annotator.rb +161 -0
- data/lib/mxrb/initializer.rb +257 -0
- data/lib/mxrb/integrity/validator.rb +136 -0
- data/lib/mxrb/io/bson_codec.rb +145 -0
- data/lib/mxrb/io/mpr_file.rb +558 -0
- data/lib/mxrb/io/mxunit_codec.rb +42 -0
- data/lib/mxrb/marketplace.rb +380 -0
- data/lib/mxrb/model/association.rb +98 -0
- data/lib/mxrb/model/attribute.rb +100 -0
- data/lib/mxrb/model/connector.rb +17 -0
- data/lib/mxrb/model/design_materializer.rb +133 -0
- data/lib/mxrb/model/design_migration.rb +126 -0
- data/lib/mxrb/model/design_system.rb +111 -0
- data/lib/mxrb/model/domain_model.rb +51 -0
- data/lib/mxrb/model/entity.rb +180 -0
- data/lib/mxrb/model/menu.rb +38 -0
- data/lib/mxrb/model/microflow.rb +100 -0
- data/lib/mxrb/model/module.rb +142 -0
- data/lib/mxrb/model/navigation.rb +127 -0
- data/lib/mxrb/model/page.rb +410 -0
- data/lib/mxrb/model/project.rb +187 -0
- data/lib/mxrb/model/unit.rb +48 -0
- data/lib/mxrb/module_initializer.rb +75 -0
- data/lib/mxrb/official_marketplace/content_api.rb +292 -0
- data/lib/mxrb/official_marketplace/dependency_resolver.rb +329 -0
- data/lib/mxrb/official_marketplace/lifecycle.rb +367 -0
- data/lib/mxrb/official_marketplace/module_package_importer.rb +318 -0
- data/lib/mxrb/official_marketplace/widget_package_installer.rb +414 -0
- data/lib/mxrb/official_marketplace.rb +720 -0
- data/lib/mxrb/oql/analyzer.rb +155 -0
- data/lib/mxrb/oql/index_advisor.rb +85 -0
- data/lib/mxrb/oql/plan_analyzer.rb +185 -0
- data/lib/mxrb/oql/server.rb +132 -0
- data/lib/mxrb/oql/sql_server_plan_analyzer.rb +190 -0
- data/lib/mxrb/oql/sql_server_workload_analyzer.rb +80 -0
- data/lib/mxrb/oql/workload_analyzer.rb +175 -0
- data/lib/mxrb/oql/workload_baseline.rb +60 -0
- data/lib/mxrb/oql.rb +424 -0
- data/lib/mxrb/project_lifecycle.rb +76 -0
- data/lib/mxrb/protocols/plan.rb +86 -0
- data/lib/mxrb/protocols.rb +123 -0
- data/lib/mxrb/runtime/database_workspace.rb +677 -0
- data/lib/mxrb/runtime/docker_executor.rb +41 -0
- data/lib/mxrb/runtime/docker_workspace.rb +29 -0
- data/lib/mxrb/runtime/executor.rb +169 -0
- data/lib/mxrb/runtime/java_locator.rb +26 -0
- data/lib/mxrb/runtime/native.rb +474 -0
- data/lib/mxrb/runtime/sql_server_database.rb +146 -0
- data/lib/mxrb/runtime/toolchain.rb +82 -0
- data/lib/mxrb/scaffold/cli.rb +113 -0
- data/lib/mxrb/scaffold/generator.rb +246 -0
- data/lib/mxrb/scaffold/help.rb +80 -0
- data/lib/mxrb/scaffold/page_templates.rb +54 -0
- data/lib/mxrb/scaffold/recipes.rb +198 -0
- data/lib/mxrb/scaffold/registry.rb +82 -0
- data/lib/mxrb/scaffold/templates.rb +723 -0
- data/lib/mxrb/scaffold/transaction.rb +84 -0
- data/lib/mxrb/schema/tables.rb +55 -0
- data/lib/mxrb/semantic/analyzer.rb +473 -0
- data/lib/mxrb/semantic/batch_plan.rb +68 -0
- data/lib/mxrb/semantic/domain_mutator.rb +419 -0
- data/lib/mxrb/semantic/embedder.rb +51 -0
- data/lib/mxrb/semantic/extractor.rb +290 -0
- data/lib/mxrb/semantic/index.rb +623 -0
- data/lib/mxrb/semantic/inliner.rb +216 -0
- data/lib/mxrb/semantic/mover.rb +153 -0
- data/lib/mxrb/semantic/onnx_embedder.rb +34 -0
- data/lib/mxrb/semantic/remover.rb +70 -0
- data/lib/mxrb/semantic/renamer.rb +211 -0
- data/lib/mxrb/semantic/tfidf_embedder.rb +46 -0
- data/lib/mxrb/semantic/vec_store.rb +75 -0
- data/lib/mxrb/team_server.rb +390 -0
- data/lib/mxrb/templates/project/10.24.0.73019.json +32 -0
- data/lib/mxrb/templates/project/11.12.1.json +32 -0
- data/lib/mxrb/templates/project/6.10.8.json +50 -0
- data/lib/mxrb/templates/project/7.17.0.json +50 -0
- data/lib/mxrb/templates/project/7.5.0.json +50 -0
- data/lib/mxrb/templates/project/9.6.1.29396.json +50 -0
- data/lib/mxrb/version.rb +5 -0
- data/lib/mxrb/widget_package.rb +338 -0
- data/lib/mxrb/widget_synchronizer.rb +31 -0
- data/lib/mxrb/writer.rb +3775 -0
- data/lib/mxrb.rb +188 -0
- data/marketplace/catalog.json +10 -0
- data/marketplace/modules/shared-kernel/domain/README.md +4 -0
- data/marketplace/modules/shared-kernel/module.rb +5 -0
- data/marketplace/modules/shared-kernel/mxrb-module.json +9 -0
- metadata +447 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "json"
|
|
6
|
+
require "net/http"
|
|
7
|
+
require "open3"
|
|
8
|
+
require "pathname"
|
|
9
|
+
require "tmpdir"
|
|
10
|
+
require "uri"
|
|
11
|
+
|
|
12
|
+
module Mxrb
|
|
13
|
+
module Marketplace
|
|
14
|
+
Entry = Data.define(:name, :version, :description, :source, :ref)
|
|
15
|
+
Installation = Data.define(:entry, :module_name, :destination, :digest)
|
|
16
|
+
|
|
17
|
+
class Catalog
|
|
18
|
+
DEFAULT_PATH = File.expand_path("../../marketplace/catalog.json", __dir__)
|
|
19
|
+
|
|
20
|
+
def initialize(source = nil)
|
|
21
|
+
@source = source || DEFAULT_PATH
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def entries
|
|
25
|
+
@entries ||= begin
|
|
26
|
+
payload = JSON.parse(read_source)
|
|
27
|
+
Array(payload.fetch("modules")).map do |item|
|
|
28
|
+
Entry.new(
|
|
29
|
+
item.fetch("name"), item.fetch("version"),
|
|
30
|
+
item.fetch("description", ""), item.fetch("source"),
|
|
31
|
+
item["ref"]
|
|
32
|
+
)
|
|
33
|
+
end.freeze
|
|
34
|
+
rescue JSON::ParserError, KeyError => e
|
|
35
|
+
raise MarketplaceError, "invalid marketplace catalog: #{e.message}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def search(query = nil)
|
|
40
|
+
term = query.to_s.downcase
|
|
41
|
+
return entries if term.empty?
|
|
42
|
+
|
|
43
|
+
entries.select do |entry|
|
|
44
|
+
[entry.name, entry.description].any? { _1.downcase.include?(term) }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def find(name)
|
|
49
|
+
entries.find { _1.name == name.to_s } ||
|
|
50
|
+
raise(MarketplaceError, "module #{name.inspect} was not found in the catalog")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def find_version(name, version)
|
|
54
|
+
entries.find { _1.name == name.to_s && _1.version == version.to_s } ||
|
|
55
|
+
raise(MarketplaceError, "module #{name.inspect} version #{version.inspect} was not found in the catalog")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def read_source
|
|
61
|
+
uri = URI.parse(@source.to_s)
|
|
62
|
+
return File.read(@source) unless uri.is_a?(URI::HTTP)
|
|
63
|
+
raise MarketplaceError, "marketplace catalogs must use HTTPS" unless uri.is_a?(URI::HTTPS)
|
|
64
|
+
|
|
65
|
+
response = Net::HTTP.get_response(uri)
|
|
66
|
+
raise MarketplaceError, "catalog request failed with HTTP #{response.code}" unless response.is_a?(Net::HTTPSuccess)
|
|
67
|
+
|
|
68
|
+
response.body
|
|
69
|
+
rescue URI::InvalidURIError, Errno::ENOENT => e
|
|
70
|
+
raise MarketplaceError, "cannot read marketplace catalog: #{e.message}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class Installer
|
|
75
|
+
MANIFEST = "mxrb-module.json"
|
|
76
|
+
|
|
77
|
+
def initialize(target:, catalog: Catalog.new)
|
|
78
|
+
@target = File.expand_path(target)
|
|
79
|
+
@catalog = catalog
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def install(identifier, version: nil)
|
|
83
|
+
install_package(identifier, version: version, replace: false)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def update(identifier, version: nil)
|
|
87
|
+
installed_module_name = resolve_installed_module_name(identifier)
|
|
88
|
+
install_package(
|
|
89
|
+
identifier,
|
|
90
|
+
version: version,
|
|
91
|
+
replace: true,
|
|
92
|
+
installed_module_name: installed_module_name
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def remove(identifier)
|
|
97
|
+
module_name = resolve_installed_module_name(identifier)
|
|
98
|
+
validate_no_dependents!(module_name)
|
|
99
|
+
destination = File.join(@target, "modules", module_name)
|
|
100
|
+
FileUtils.rm_rf(destination) if File.exist?(destination)
|
|
101
|
+
remove_from_lock(module_name)
|
|
102
|
+
module_name
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def install_package(identifier, version:, replace:, installed_module_name: nil)
|
|
108
|
+
entry, source = resolve(identifier, version: version)
|
|
109
|
+
Dir.mktmpdir("mxrb-module-") do |workspace|
|
|
110
|
+
package = materialize(source, entry, workspace)
|
|
111
|
+
manifest = load_manifest(package)
|
|
112
|
+
module_name = valid_name(manifest.fetch("module_name"))
|
|
113
|
+
destination = File.join(@target, "modules", module_name)
|
|
114
|
+
if installed_module_name && module_name != installed_module_name
|
|
115
|
+
raise MarketplaceError,
|
|
116
|
+
"module update cannot change its name from #{installed_module_name.inspect} to #{module_name.inspect}"
|
|
117
|
+
end
|
|
118
|
+
if File.exist?(destination) && !replace
|
|
119
|
+
raise MarketplaceError, "module destination already exists: #{destination}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
manifest_name = manifest["name"].to_s
|
|
123
|
+
canonical_entry = manifest_name.empty? ? entry : Entry.new(
|
|
124
|
+
manifest_name, entry.version, entry.description, entry.source, entry.ref
|
|
125
|
+
)
|
|
126
|
+
validate_mendix_version!(manifest)
|
|
127
|
+
validate_dependencies!(manifest)
|
|
128
|
+
files = package_files(package, manifest)
|
|
129
|
+
staging = File.join(@target, ".mxrb", "staging", "#{module_name}-#{Process.pid}")
|
|
130
|
+
backup = nil
|
|
131
|
+
FileUtils.mkdir_p(staging)
|
|
132
|
+
files.each { copy_entry(package, staging, _1) }
|
|
133
|
+
if replace && File.exist?(destination)
|
|
134
|
+
backup = File.join(@target, ".mxrb", "backup", "#{module_name}-#{Process.pid}")
|
|
135
|
+
FileUtils.mkdir_p(File.dirname(backup))
|
|
136
|
+
FileUtils.mv(destination, backup)
|
|
137
|
+
end
|
|
138
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
|
139
|
+
FileUtils.mv(staging, destination)
|
|
140
|
+
FileUtils.rm_rf(backup) if backup
|
|
141
|
+
backup = nil
|
|
142
|
+
digest = tree_digest(destination)
|
|
143
|
+
write_lock(canonical_entry, module_name, digest, dependencies: declared_dependencies(manifest))
|
|
144
|
+
Installation.new(entry, module_name, destination, digest)
|
|
145
|
+
rescue StandardError
|
|
146
|
+
FileUtils.mv(backup, destination) if backup && File.exist?(backup) && !File.exist?(destination)
|
|
147
|
+
raise
|
|
148
|
+
ensure
|
|
149
|
+
FileUtils.rm_rf(staging) if staging && File.exist?(staging)
|
|
150
|
+
FileUtils.rm_rf(backup) if backup && File.exist?(backup)
|
|
151
|
+
end
|
|
152
|
+
rescue KeyError, JSON::ParserError => e
|
|
153
|
+
raise MarketplaceError, "invalid module manifest: #{e.message}"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def resolve(identifier, version: nil)
|
|
157
|
+
path = File.expand_path(identifier.to_s)
|
|
158
|
+
return [local_entry(path), path] if File.directory?(path)
|
|
159
|
+
|
|
160
|
+
entry = version ? @catalog.find_version(identifier, version) : @catalog.find(identifier)
|
|
161
|
+
[entry, entry.source]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def resolve_installed_module_name(identifier)
|
|
165
|
+
lock_path = File.join(@target, ".mxrb", "modules.lock.json")
|
|
166
|
+
raise MarketplaceError, "no modules are installed (lock file missing)" unless File.file?(lock_path)
|
|
167
|
+
|
|
168
|
+
lock = JSON.parse(File.read(lock_path))
|
|
169
|
+
locked = lock.fetch("modules", {})
|
|
170
|
+
entry = locked.find { |_name, info| info["package"] == identifier.to_s }
|
|
171
|
+
unless entry
|
|
172
|
+
candidates = locked.select { |name, _| name.downcase == identifier.to_s.downcase }
|
|
173
|
+
entry = candidates.first
|
|
174
|
+
end
|
|
175
|
+
raise MarketplaceError, "module #{identifier.inspect} is not installed" unless entry
|
|
176
|
+
|
|
177
|
+
entry.first
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def remove_from_lock(module_name)
|
|
181
|
+
lock_path = File.join(@target, ".mxrb", "modules.lock.json")
|
|
182
|
+
return unless File.file?(lock_path)
|
|
183
|
+
|
|
184
|
+
lock = JSON.parse(File.read(lock_path))
|
|
185
|
+
lock.fetch("modules", {}).delete(module_name)
|
|
186
|
+
temporary = "#{lock_path}.tmp-#{Process.pid}"
|
|
187
|
+
File.write(temporary, JSON.pretty_generate(lock) << "\n")
|
|
188
|
+
File.rename(temporary, lock_path)
|
|
189
|
+
ensure
|
|
190
|
+
FileUtils.rm_f(temporary) if defined?(temporary) && temporary && File.exist?(temporary)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def local_entry(path)
|
|
194
|
+
Entry.new(File.basename(path), "local", "Local MXRB module", path, nil)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def materialize(source, entry, workspace)
|
|
198
|
+
return builtin_path(source) if source.start_with?("builtin:")
|
|
199
|
+
return File.expand_path(source) if File.directory?(source)
|
|
200
|
+
|
|
201
|
+
destination = File.join(workspace, "repository")
|
|
202
|
+
command = ["git", "clone", "--depth", "1"]
|
|
203
|
+
command += ["--branch", entry.ref] if entry.ref
|
|
204
|
+
command += [source, destination]
|
|
205
|
+
output, status = Open3.capture2e(*command)
|
|
206
|
+
raise MarketplaceError, "could not fetch #{entry.name}: #{output}" unless status.success?
|
|
207
|
+
|
|
208
|
+
destination
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def builtin_path(source)
|
|
212
|
+
slug = source.delete_prefix("builtin:")
|
|
213
|
+
unless slug.match?(/\A[a-z0-9]+(?:-[a-z0-9]+)*\z/)
|
|
214
|
+
raise MarketplaceError, "invalid built-in module slug #{slug.inspect}"
|
|
215
|
+
end
|
|
216
|
+
path = File.expand_path("../../marketplace/modules/#{slug}", __dir__)
|
|
217
|
+
raise MarketplaceError, "built-in module #{slug.inspect} is unavailable" unless File.directory?(path)
|
|
218
|
+
|
|
219
|
+
path
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def load_manifest(package)
|
|
223
|
+
JSON.parse(File.read(File.join(package, MANIFEST)))
|
|
224
|
+
rescue Errno::ENOENT => e
|
|
225
|
+
raise MarketplaceError, "module manifest is missing: #{e.message}"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def valid_name(value)
|
|
229
|
+
name = value.to_s
|
|
230
|
+
unless name.match?(/\A[A-Za-z][A-Za-z0-9_]*\z/)
|
|
231
|
+
raise MarketplaceError, "invalid module name #{name.inspect}"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
name
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def package_files(package, manifest)
|
|
238
|
+
files = Array(manifest["files"] || Dir.children(package) - [MANIFEST])
|
|
239
|
+
raise MarketplaceError, "module package has no files" if files.empty?
|
|
240
|
+
|
|
241
|
+
files.each do |relative|
|
|
242
|
+
path = Pathname.new(relative.to_s)
|
|
243
|
+
if path.absolute? || path.each_filename.any? { _1 == ".." }
|
|
244
|
+
raise MarketplaceError, "unsafe module path #{relative.inspect}"
|
|
245
|
+
end
|
|
246
|
+
raise MarketplaceError, "module file is missing: #{relative}" unless File.exist?(File.join(package, relative))
|
|
247
|
+
end
|
|
248
|
+
files
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def copy_entry(package, staging, relative)
|
|
252
|
+
source = File.join(package, relative)
|
|
253
|
+
destination = File.join(staging, relative)
|
|
254
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
|
255
|
+
FileUtils.cp_r(source, destination)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def tree_digest(path)
|
|
259
|
+
digest = Digest::SHA256.new
|
|
260
|
+
Dir.glob(File.join(path, "**", "*"), File::FNM_DOTMATCH).sort.each do |file|
|
|
261
|
+
next unless File.file?(file)
|
|
262
|
+
|
|
263
|
+
digest << file.delete_prefix("#{path}/") << "\0" << File.binread(file)
|
|
264
|
+
end
|
|
265
|
+
digest.hexdigest
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def declared_dependencies(manifest)
|
|
269
|
+
Array(manifest["dependencies"]).map do |dep|
|
|
270
|
+
name = dep.is_a?(Hash) ? dep["name"].to_s : dep.to_s
|
|
271
|
+
name unless name.empty?
|
|
272
|
+
end.compact
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def validate_dependencies!(manifest)
|
|
276
|
+
deps = declared_dependencies(manifest)
|
|
277
|
+
return if deps.empty?
|
|
278
|
+
|
|
279
|
+
lock_path = File.join(@target, ".mxrb", "modules.lock.json")
|
|
280
|
+
installed_packages = if File.file?(lock_path)
|
|
281
|
+
JSON.parse(File.read(lock_path)).fetch("modules", {}).values.map { _1["package"] }
|
|
282
|
+
else
|
|
283
|
+
[]
|
|
284
|
+
end
|
|
285
|
+
missing = deps.reject { |dep| installed_packages.include?(dep) }
|
|
286
|
+
return if missing.empty?
|
|
287
|
+
|
|
288
|
+
raise MarketplaceError,
|
|
289
|
+
"missing dependencies: #{missing.join(', ')} — install them first"
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def validate_mendix_version!(manifest)
|
|
293
|
+
required = manifest["mendix_version"].to_s.strip
|
|
294
|
+
return if required.empty?
|
|
295
|
+
|
|
296
|
+
project_version = detect_project_mendix_version
|
|
297
|
+
return if project_version.nil?
|
|
298
|
+
|
|
299
|
+
unless mendix_version_compatible?(project_version, required)
|
|
300
|
+
raise MarketplaceError,
|
|
301
|
+
"module requires Mendix #{required}, but the project uses #{project_version}"
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def detect_project_mendix_version
|
|
306
|
+
mpr_files = Dir.glob(File.join(@target, "*.mpr"))
|
|
307
|
+
return nil if mpr_files.empty?
|
|
308
|
+
|
|
309
|
+
require_relative "../mxrb/io/mpr_file"
|
|
310
|
+
mpr = IO::MprFile.open(mpr_files.first, readonly: true)
|
|
311
|
+
mpr.mendix_version
|
|
312
|
+
rescue StandardError
|
|
313
|
+
nil
|
|
314
|
+
ensure
|
|
315
|
+
mpr&.close
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def mendix_version_compatible?(project_version, required)
|
|
319
|
+
proj_parts = project_version.to_s.split(".").map(&:to_i)
|
|
320
|
+
req_str = required.to_s.strip
|
|
321
|
+
|
|
322
|
+
case req_str
|
|
323
|
+
when /\A(\d+)\.x\z/
|
|
324
|
+
proj_parts[0] == ::Regexp.last_match(1).to_i
|
|
325
|
+
when /\A>=\s*(.+)\z/
|
|
326
|
+
version_gte?(proj_parts, ::Regexp.last_match(1).strip.split(".").map(&:to_i))
|
|
327
|
+
when /\A~>\s*(.+)\z/
|
|
328
|
+
base = ::Regexp.last_match(1).strip.split(".").map(&:to_i)
|
|
329
|
+
version_gte?(proj_parts, base) &&
|
|
330
|
+
proj_parts[0, base.size - 1] == base[0, base.size - 1]
|
|
331
|
+
else
|
|
332
|
+
project_version.start_with?(req_str.sub(/\.\*\z/, ""))
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def version_gte?(a, b)
|
|
337
|
+
a.zip(b).each do |av, bv|
|
|
338
|
+
av ||= 0
|
|
339
|
+
bv ||= 0
|
|
340
|
+
return true if av > bv
|
|
341
|
+
return false if av < bv
|
|
342
|
+
end
|
|
343
|
+
true
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def validate_no_dependents!(module_name)
|
|
347
|
+
lock_path = File.join(@target, ".mxrb", "modules.lock.json")
|
|
348
|
+
return unless File.file?(lock_path)
|
|
349
|
+
|
|
350
|
+
lock = JSON.parse(File.read(lock_path))
|
|
351
|
+
package_name = lock.dig("modules", module_name, "package")
|
|
352
|
+
return unless package_name
|
|
353
|
+
|
|
354
|
+
dependents = lock.fetch("modules", {}).filter_map do |name, info|
|
|
355
|
+
name if name != module_name && Array(info["dependencies"]).include?(package_name)
|
|
356
|
+
end
|
|
357
|
+
return if dependents.empty?
|
|
358
|
+
|
|
359
|
+
raise MarketplaceError,
|
|
360
|
+
"cannot remove #{module_name.inspect}: #{dependents.join(', ')} depend on it"
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def write_lock(entry, module_name, digest, dependencies: [])
|
|
364
|
+
lock_path = File.join(@target, ".mxrb", "modules.lock.json")
|
|
365
|
+
FileUtils.mkdir_p(File.dirname(lock_path))
|
|
366
|
+
lock = File.file?(lock_path) ? JSON.parse(File.read(lock_path)) : { "modules" => {} }
|
|
367
|
+
lock["modules"][module_name] = {
|
|
368
|
+
"package" => entry.name, "version" => entry.version,
|
|
369
|
+
"source" => entry.source, "ref" => entry.ref, "sha256" => digest,
|
|
370
|
+
"dependencies" => dependencies.empty? ? nil : dependencies
|
|
371
|
+
}.compact
|
|
372
|
+
temporary = "#{lock_path}.tmp-#{Process.pid}"
|
|
373
|
+
File.write(temporary, JSON.pretty_generate(lock) << "\n")
|
|
374
|
+
File.rename(temporary, lock_path)
|
|
375
|
+
ensure
|
|
376
|
+
FileUtils.rm_f(temporary) if temporary && File.exist?(temporary)
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Mxrb
|
|
6
|
+
module Model
|
|
7
|
+
# Associations are embedded in DomainModel BSON — NOT separate Unit rows.
|
|
8
|
+
# $Type: DomainModels$Association
|
|
9
|
+
#
|
|
10
|
+
# CRITICAL GOTCHA (from reverse engineering):
|
|
11
|
+
# ParentID = entity FROM (the one with the FK) — counter-intuitive naming!
|
|
12
|
+
# ChildID = entity TO (the referenced entity)
|
|
13
|
+
# MemberAccess should be added ONLY to the FROM entity (ParentPointer), not ChildID.
|
|
14
|
+
class Association
|
|
15
|
+
attr_accessor :id, :name, :documentation,
|
|
16
|
+
:from_entity_id, # ParentID in BSON (FROM entity — has the FK)
|
|
17
|
+
:to_entity_id, # ChildID in BSON (TO entity — referenced)
|
|
18
|
+
:association_type, :owner, :storage_format,
|
|
19
|
+
:delete_behavior, :export_level
|
|
20
|
+
|
|
21
|
+
TYPES = %i[Reference ReferenceSet].freeze
|
|
22
|
+
OWNERS = %i[Default Both].freeze
|
|
23
|
+
STORAGE_FMTS = %i[Column Table].freeze
|
|
24
|
+
|
|
25
|
+
def self.from_bson(doc, _domain_model_id = nil, _mpr = nil)
|
|
26
|
+
a = new
|
|
27
|
+
a.id = IO::BsonCodec.extract_id(doc["$ID"] || doc["\$ID"])
|
|
28
|
+
a.name = doc["Name"] || doc["name"]
|
|
29
|
+
a.documentation = doc["Documentation"] || doc["documentation"] || ""
|
|
30
|
+
# ParentID = FROM entity (counter-intuitive — see class comment)
|
|
31
|
+
a.from_entity_id = IO::BsonCodec.extract_id(
|
|
32
|
+
doc["ParentPointer"] || doc["ParentID"] || doc["parentId"]
|
|
33
|
+
)
|
|
34
|
+
child = doc['Child'] || doc["ChildPointer"] || doc["ChildID"] || doc["childId"]
|
|
35
|
+
a.to_entity_id = if child.is_a?(String) && child.include?('.')
|
|
36
|
+
child
|
|
37
|
+
else
|
|
38
|
+
IO::BsonCodec.extract_id(child)
|
|
39
|
+
end
|
|
40
|
+
a.association_type = (doc["Type"] || doc["type"] || "Reference").to_sym
|
|
41
|
+
a.owner = (doc["Owner"] || doc["owner"] || "Default").to_sym
|
|
42
|
+
a.storage_format = (doc["StorageFormat"] || "Column").to_sym
|
|
43
|
+
a.delete_behavior = doc["DeleteBehavior"] || doc["deleteBehavior"]
|
|
44
|
+
a.export_level = doc["ExportLevel"] || "Hidden"
|
|
45
|
+
a
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def to_bson
|
|
49
|
+
{
|
|
50
|
+
"$ID" => @id || SecureRandom.uuid,
|
|
51
|
+
"$Type" => "DomainModels$Association",
|
|
52
|
+
"Name" => @name,
|
|
53
|
+
"Documentation" => @documentation || "",
|
|
54
|
+
# ParentID = FROM entity (the one that carries the FK)
|
|
55
|
+
"ParentID" => @from_entity_id,
|
|
56
|
+
"ChildID" => @to_entity_id,
|
|
57
|
+
"Type" => (@association_type || :Reference).to_s,
|
|
58
|
+
"Owner" => (@owner || :Default).to_s,
|
|
59
|
+
"StorageFormat" => (@storage_format || :Column).to_s,
|
|
60
|
+
"DeleteBehavior" => @delete_behavior || default_delete_behavior,
|
|
61
|
+
"ExportLevel" => @export_level || "Hidden",
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def inspect
|
|
66
|
+
"#<Mxrb::Association name=#{@name.inspect} type=#{@association_type} " \
|
|
67
|
+
"from=#{@from_entity_id.to_s.slice(0, 8)} to=#{@to_entity_id.to_s.slice(0, 8)}>"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def parent_delete_behavior
|
|
71
|
+
behavior_value('parentDeleteBehavior', 'ParentDeleteBehavior')
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def child_delete_behavior
|
|
75
|
+
behavior_value('childDeleteBehavior', 'ChildDeleteBehavior')
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def behavior_value(lower, upper)
|
|
81
|
+
return :NoAction unless @delete_behavior.is_a?(Hash)
|
|
82
|
+
|
|
83
|
+
(@delete_behavior[lower] || @delete_behavior[upper] || 'NoAction').to_sym
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def default_delete_behavior
|
|
87
|
+
{
|
|
88
|
+
"$ID" => SecureRandom.uuid,
|
|
89
|
+
"$Type" => "DomainModels$DeleteBehavior",
|
|
90
|
+
"parentDeleteBehavior" => "NoAction",
|
|
91
|
+
"childDeleteBehavior" => "NoAction",
|
|
92
|
+
"parentErrorMessage" => nil,
|
|
93
|
+
"childErrorMessage" => nil,
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Mxrb
|
|
6
|
+
module Model
|
|
7
|
+
# Attributes are embedded in Entity BSON — NOT separate Unit rows.
|
|
8
|
+
# $Type: DomainModels$Attribute
|
|
9
|
+
class Attribute
|
|
10
|
+
# Maps DSL symbol → Mendix storage type name
|
|
11
|
+
TYPE_MAP = {
|
|
12
|
+
string: "DomainModels$StringAttributeType",
|
|
13
|
+
integer: "DomainModels$IntegerAttributeType",
|
|
14
|
+
long: "DomainModels$LongAttributeType",
|
|
15
|
+
float: "DomainModels$FloatAttributeType",
|
|
16
|
+
decimal: "DomainModels$DecimalAttributeType",
|
|
17
|
+
boolean: "DomainModels$BooleanAttributeType",
|
|
18
|
+
datetime: "DomainModels$DateTimeAttributeType",
|
|
19
|
+
autonumber: "DomainModels$AutoNumberAttributeType",
|
|
20
|
+
hashstring: "DomainModels$HashedStringAttributeType",
|
|
21
|
+
binary: "DomainModels$BinaryAttributeType",
|
|
22
|
+
enum: "DomainModels$EnumerationAttributeType",
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
REVERSE_TYPE_MAP = TYPE_MAP.invert.freeze
|
|
26
|
+
|
|
27
|
+
attr_accessor :id, :name, :documentation, :type, :default_value,
|
|
28
|
+
:data_storage_guid, :export_level, :raw_type_doc, :raw_value_doc,
|
|
29
|
+
:length, :localize_date, :enumeration, :required, :unique
|
|
30
|
+
|
|
31
|
+
def self.from_bson(doc)
|
|
32
|
+
a = new
|
|
33
|
+
a.id = IO::BsonCodec.extract_id(doc["$ID"] || doc["\$ID"])
|
|
34
|
+
a.name = doc["name"] || doc["Name"]
|
|
35
|
+
a.documentation = doc["documentation"] || doc["Documentation"] || ""
|
|
36
|
+
a.data_storage_guid = doc["dataStorageGuid"] || doc["DataStorageGuid"]
|
|
37
|
+
a.export_level = doc["exportLevel"] || doc["ExportLevel"] || "Hidden"
|
|
38
|
+
|
|
39
|
+
type_doc = doc["type"] || doc["Type"] || doc["newType"] || doc["NewType"]
|
|
40
|
+
a.raw_type_doc = type_doc
|
|
41
|
+
if type_doc.is_a?(Hash)
|
|
42
|
+
storage_type = type_doc["$Type"] || type_doc["\$Type"]
|
|
43
|
+
a.type = REVERSE_TYPE_MAP[storage_type] || :string
|
|
44
|
+
a.length = type_doc['length'] || type_doc['Length']
|
|
45
|
+
a.localize_date = if type_doc.key?('localizeDate')
|
|
46
|
+
type_doc['localizeDate']
|
|
47
|
+
else
|
|
48
|
+
type_doc['LocalizeDate']
|
|
49
|
+
end
|
|
50
|
+
a.enumeration = type_doc['enumeration'] || type_doc['Enumeration']
|
|
51
|
+
else
|
|
52
|
+
a.type = :string
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
a.raw_value_doc = doc["value"] || doc["Value"]
|
|
56
|
+
a.default_value = extract_default(a.raw_value_doc)
|
|
57
|
+
a
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def to_bson
|
|
61
|
+
{
|
|
62
|
+
"$ID" => @id || SecureRandom.uuid,
|
|
63
|
+
"$Type" => "DomainModels$Attribute",
|
|
64
|
+
"name" => @name,
|
|
65
|
+
"documentation" => @documentation || "",
|
|
66
|
+
"dataStorageGuid" => @data_storage_guid || SecureRandom.uuid,
|
|
67
|
+
"exportLevel" => @export_level || "Hidden",
|
|
68
|
+
"type" => serialize_type,
|
|
69
|
+
"value" => serialize_value,
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def inspect
|
|
74
|
+
"#<Mxrb::Attribute name=#{@name.inspect} type=#{@type}>"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def self.extract_default(value_doc)
|
|
80
|
+
return nil unless value_doc.is_a?(Hash)
|
|
81
|
+
value_doc["defaultValue"] || value_doc["DefaultValue"]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def serialize_type
|
|
85
|
+
storage_type = TYPE_MAP[@type] || TYPE_MAP[:string]
|
|
86
|
+
base = { "$ID" => SecureRandom.uuid, "$Type" => storage_type }
|
|
87
|
+
base["localizeDate"] = true if @type == :datetime
|
|
88
|
+
base
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def serialize_value
|
|
92
|
+
{
|
|
93
|
+
"$ID" => SecureRandom.uuid,
|
|
94
|
+
"$Type" => "DomainModels$StoredValue",
|
|
95
|
+
"defaultValue" => @default_value.to_s,
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mxrb
|
|
4
|
+
module Model
|
|
5
|
+
# A marketplace connector detected inside a project.
|
|
6
|
+
#
|
|
7
|
+
# MXRB never runs the connector; it only represents the public surface that
|
|
8
|
+
# the semantic index can observe. Protected or non-decodable internals are
|
|
9
|
+
# reported as unavailable (empty), never guessed.
|
|
10
|
+
Connector = Data.define(
|
|
11
|
+
:module_name, :protocol, :appstore_guid, :appstore_version,
|
|
12
|
+
:entities, :microflows, :protected, :metadata
|
|
13
|
+
) do
|
|
14
|
+
def known? = !protocol.nil?
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|