archsight 0.3.2 → 0.4.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 +4 -4
- data/README.md +7 -1
- data/docs/archimate.md +21 -1
- data/docs/configuration.md +1 -0
- data/docs/diagram.md +15 -3
- data/docs/docker.md +4 -1
- data/docs/icons.md +32 -0
- data/docs/import.md +178 -55
- data/docs/index.md.erb +31 -8
- data/docs/modeling.md +772 -24
- data/docs/pages.md +44 -13
- data/docs/search.md +21 -1
- data/docs/togaf.md +5 -1
- data/lib/archsight/analysis/result.rb +8 -2
- data/lib/archsight/analysis/sandbox.rb +8 -0
- data/lib/archsight/analysis/typst_renderer.rb +246 -0
- data/lib/archsight/analysis.rb +1 -0
- data/lib/archsight/annotations/annotation.rb +30 -3
- data/lib/archsight/annotations/architecture_annotations.rb +47 -0
- data/lib/archsight/annotations/asset_annotations.rb +34 -0
- data/lib/archsight/annotations/link_annotations.rb +15 -0
- data/lib/archsight/annotations/resource_reference.rb +12 -0
- data/lib/archsight/annotations/risk_annotations.rb +21 -0
- data/lib/archsight/cli.rb +124 -23
- data/lib/archsight/database.rb +40 -0
- data/lib/archsight/diagram/cli.rb +50 -2
- data/lib/archsight/diagram/formatter.rb +112 -0
- data/lib/archsight/diagram/routing/edge_router/orthogonal_path.rb +38 -10
- data/lib/archsight/diagram/routing/edge_router.rb +29 -4
- data/lib/archsight/diagram/routing/edge_routing.rb +61 -5
- data/lib/archsight/diagram/style/theme.rb +7 -2
- data/lib/archsight/documentation.rb +3 -2
- data/lib/archsight/editor/page_source.rb +13 -1
- data/lib/archsight/export/confluence/analysis_output.rb +101 -0
- data/lib/archsight/export/confluence/client.rb +13 -0
- data/lib/archsight/export/confluence/diagram_links.rb +1 -1
- data/lib/archsight/export/confluence/exporter.rb +40 -13
- data/lib/archsight/export/confluence/labels.rb +25 -0
- data/lib/archsight/export/confluence/page_properties.rb +4 -6
- data/lib/archsight/export/confluence/storage.rb +38 -2
- data/lib/archsight/helpers/embeds.rb +18 -7
- data/lib/archsight/helpers/resource_resolver.rb +25 -0
- data/lib/archsight/import/executor.rb +139 -16
- data/lib/archsight/import/handler.rb +55 -6
- data/lib/archsight/import/handler_loader.rb +64 -0
- data/lib/archsight/import/handlers/cache_pruner.rb +30 -7
- data/lib/archsight/import/handlers/command.rb +120 -0
- data/lib/archsight/import/handlers/github.rb +4 -1
- data/lib/archsight/import/handlers/gitlab.rb +4 -1
- data/lib/archsight/import/handlers/go_dep_resolver.rb +10 -2
- data/lib/archsight/import/handlers/go_grapher.rb +102 -5
- data/lib/archsight/import/handlers/go_module_parser.rb +88 -0
- data/lib/archsight/import/handlers/grapher.rb +40 -1
- data/lib/archsight/import/handlers/repository.rb +3 -0
- data/lib/archsight/import/handlers/rest_api_index.rb +3 -0
- data/lib/archsight/import/registry.rb +22 -1
- data/lib/archsight/import.rb +64 -1
- data/lib/archsight/linter/structure.rb +239 -0
- data/lib/archsight/linter.rb +217 -6
- data/lib/archsight/page_loader.rb +12 -1
- data/lib/archsight/query/evaluator.rb +13 -6
- data/lib/archsight/query/parser.rb +73 -6
- data/lib/archsight/query.rb +1 -1
- data/lib/archsight/references.rb +3 -3
- data/lib/archsight/renderer.rb +80 -23
- data/lib/archsight/resources/analysis.rb +3 -2
- data/lib/archsight/resources/application_component.rb +83 -8
- data/lib/archsight/resources/application_event.rb +79 -0
- data/lib/archsight/resources/application_service.rb +18 -2
- data/lib/archsight/resources/base.rb +58 -5
- data/lib/archsight/resources/business_actor.rb +14 -1
- data/lib/archsight/resources/business_contract.rb +72 -0
- data/lib/archsight/resources/business_control.rb +10 -2
- data/lib/archsight/resources/business_event.rb +79 -0
- data/lib/archsight/resources/business_process.rb +8 -1
- data/lib/archsight/resources/business_product.rb +12 -0
- data/lib/archsight/resources/business_role.rb +70 -0
- data/lib/archsight/resources/compliance_evidence.rb +8 -2
- data/lib/archsight/resources/data_object.rb +6 -1
- data/lib/archsight/resources/implementation_deliverable.rb +63 -0
- data/lib/archsight/resources/implementation_event.rb +52 -0
- data/lib/archsight/resources/implementation_gap.rb +49 -0
- data/lib/archsight/resources/implementation_plateau.rb +67 -0
- data/lib/archsight/resources/implementation_work_package.rb +84 -0
- data/lib/archsight/resources/import.rb +37 -11
- data/lib/archsight/resources/location.rb +56 -0
- data/lib/archsight/resources/motivation_assessment.rb +121 -0
- data/lib/archsight/resources/motivation_constraint.rb +7 -1
- data/lib/archsight/resources/motivation_driver.rb +50 -0
- data/lib/archsight/resources/motivation_goal.rb +13 -1
- data/lib/archsight/resources/motivation_principle.rb +75 -0
- data/lib/archsight/resources/motivation_requirement.rb +18 -2
- data/lib/archsight/resources/motivation_stakeholder.rb +21 -0
- data/lib/archsight/resources/page.rb +9 -7
- data/lib/archsight/resources/strategy_capability.rb +9 -0
- data/lib/archsight/resources/technology_artifact.rb +11 -4
- data/lib/archsight/resources/technology_communication_network.rb +89 -0
- data/lib/archsight/resources/technology_device.rb +88 -0
- data/lib/archsight/resources/technology_equipment.rb +75 -0
- data/lib/archsight/resources/technology_event.rb +80 -0
- data/lib/archsight/resources/technology_facility.rb +70 -0
- data/lib/archsight/resources/technology_node.rb +57 -1
- data/lib/archsight/resources/technology_service.rb +33 -0
- data/lib/archsight/resources/technology_system_software.rb +28 -0
- data/lib/archsight/resources.rb +12 -0
- data/lib/archsight/version.rb +1 -1
- data/lib/archsight/web/api/analysis_helpers.rb +55 -0
- data/lib/archsight/web/api/json_helpers.rb +3 -37
- data/lib/archsight/web/api/openapi/spec.yaml +10 -8
- data/lib/archsight/web/api/page_helpers.rb +1 -8
- data/lib/archsight/web/api/routes.rb +7 -2
- data/lib/archsight/web/application.rb +38 -2
- data/lib/archsight/web/editor/routes.rb +2 -1
- data/lib/archsight/web/public/vue/ApiDocsPage-COp4onwn.js +1 -0
- data/lib/archsight/web/public/vue/DocPage-D52OwaKc.js +1 -0
- data/lib/archsight/web/public/vue/EditorPage-3aqL7yVf.js +35 -0
- data/lib/archsight/web/public/vue/ErrorPage--b9XeSYa.js +2 -0
- data/lib/archsight/web/public/vue/GraphView-qh_EtRrQ.js +1 -0
- data/lib/archsight/web/public/vue/HomePage-C2TNh2U-.js +2 -0
- data/lib/archsight/web/public/vue/InstanceRouter-ZaCcjlIX.css +1 -0
- data/lib/archsight/web/public/vue/InstanceRouter-uzb_OMG0.js +1 -0
- data/lib/archsight/web/public/vue/KindList-CyJuP_uq.js +1 -0
- data/lib/archsight/web/public/vue/KindList-yYcRT2BV.css +1 -0
- data/lib/archsight/web/public/vue/PageView-B93woeN3.css +1 -0
- data/lib/archsight/web/public/vue/PageView-FyygrS-b.js +1 -0
- data/lib/archsight/web/public/vue/QueryError-B4Ex_csZ.js +1 -0
- data/lib/archsight/web/public/vue/ResourceList-BY6Z1Kcb.js +2 -0
- data/lib/archsight/web/public/vue/ResourceList-D75O87zG.css +1 -0
- data/lib/archsight/web/public/vue/{SearchResults-CyPUZZSC.css → SearchResults-BV6UHriJ.css} +1 -1
- data/lib/archsight/web/public/vue/SearchResults-CkFpHF8m.js +1 -0
- data/lib/archsight/web/public/vue/WikiPage-BoHNewCX.js +3 -0
- data/lib/archsight/web/public/vue/WikiPage-TpmM1VAF.css +1 -0
- data/lib/archsight/web/public/vue/architecture-7GRP2DOG-BvzKZE4e.js +1 -0
- data/lib/archsight/web/public/vue/cynefin-OW5HDTMX-D-xs_Hxh.js +1 -0
- data/lib/archsight/web/public/vue/eventmodeling-NTZA5JFV-nbjxG01S.js +1 -0
- data/lib/archsight/web/public/vue/gitGraph-4MIJSDKK-CW6mw2ab.js +1 -0
- data/lib/archsight/web/public/vue/index-BGlYCbi7.js +3 -0
- data/lib/archsight/web/public/vue/index-eUHmJ7K2.css +1 -0
- data/lib/archsight/web/public/vue/info-A6RAGUB7-C12sjlHe.js +1 -0
- data/lib/archsight/web/public/vue/{mermaid-BjEi5URd.js → mermaid-DjsXQ_XN.js} +35 -35
- data/lib/archsight/web/public/vue/packet-AYTQ26CC-wMwKwHmA.js +1 -0
- data/lib/archsight/web/public/vue/pie-WAS4IAKB-w1-sAU_K.js +1 -0
- data/lib/archsight/web/public/vue/radar-RG4KPBEZ-DpwqIFoy.js +1 -0
- data/lib/archsight/web/public/vue/railroad-74A4TZTK-Ce5XC24d.js +1 -0
- data/lib/archsight/web/public/vue/railroad-abnf-HS5TGJTU-BSQO2pds.js +1 -0
- data/lib/archsight/web/public/vue/railroad-ebnf-LZEXJU2U-Dq1c2YtD.js +1 -0
- data/lib/archsight/web/public/vue/railroad-peg-WCYAUIDC-DKj6dli0.js +1 -0
- data/lib/archsight/web/public/vue/rolldown-runtime-hePW80VL.js +1 -0
- data/lib/archsight/web/public/vue/treeView-Q6P3EWNA-BmHNTWxU.js +1 -0
- data/lib/archsight/web/public/vue/treemap-WGGIJYW6-DsEI7sSD.js +1 -0
- data/lib/archsight/web/public/vue/{useGraphviz-DweKV7Kg.js → useGraphviz-4evusK9i.js} +13 -1
- data/lib/archsight/web/public/vue/{useHighlight-D5Gf8DLl.js → useHighlight-DPguaxfm.js} +8 -8
- data/lib/archsight/web/public/vue/usePanZoom-seQE1TPk.js +12 -0
- data/lib/archsight/web/public/vue/wardley-WFR3VGLG-CSCW83-M.js +1 -0
- data/lib/archsight/web/public/vue.html +5 -5
- metadata +72 -38
- data/lib/archsight/web/public/vue/ApiDocsPage-DoOxKjG0.js +0 -1
- data/lib/archsight/web/public/vue/DocPage-CfsC3CeQ.js +0 -1
- data/lib/archsight/web/public/vue/EditorPage-CsJA0q8n.js +0 -35
- data/lib/archsight/web/public/vue/ErrorPage-OJpJz9df.js +0 -2
- data/lib/archsight/web/public/vue/GraphView-CBq6oFRV.js +0 -1
- data/lib/archsight/web/public/vue/HomePage-BwgMLgVC.js +0 -2
- data/lib/archsight/web/public/vue/InstanceRouter-DQz-SsQq.js +0 -1
- data/lib/archsight/web/public/vue/InstanceRouter-jeavM6PW.css +0 -1
- data/lib/archsight/web/public/vue/KindList-4q0K9Cll.js +0 -1
- data/lib/archsight/web/public/vue/PageView-DzeGnvoS.js +0 -1
- data/lib/archsight/web/public/vue/QueryError-kg6Yf-pW.js +0 -1
- data/lib/archsight/web/public/vue/ResourceList-B0FLaFR3.css +0 -1
- data/lib/archsight/web/public/vue/ResourceList-Df_0e0qt.js +0 -2
- data/lib/archsight/web/public/vue/SearchResults-CKgY6_wH.js +0 -1
- data/lib/archsight/web/public/vue/WikiPage-DoFJ0dbW.css +0 -1
- data/lib/archsight/web/public/vue/WikiPage-DrvCHwJ7.js +0 -13
- data/lib/archsight/web/public/vue/architecture-7GRP2DOG-BfA1TPxQ.js +0 -1
- data/lib/archsight/web/public/vue/cynefin-OW5HDTMX-BmgdUKVD.js +0 -1
- data/lib/archsight/web/public/vue/eventmodeling-NTZA5JFV-DbXDvAWF.js +0 -1
- data/lib/archsight/web/public/vue/gitGraph-4MIJSDKK-BkVhrKS1.js +0 -1
- data/lib/archsight/web/public/vue/index-D0Q5GZRs.js +0 -3
- data/lib/archsight/web/public/vue/index-oF-iyZvk.css +0 -1
- data/lib/archsight/web/public/vue/info-A6RAGUB7-BIlCVuUY.js +0 -1
- data/lib/archsight/web/public/vue/packet-AYTQ26CC-eoQTjY3j.js +0 -1
- data/lib/archsight/web/public/vue/pie-WAS4IAKB-DEOotGhh.js +0 -1
- data/lib/archsight/web/public/vue/radar-RG4KPBEZ-C2yucuUN.js +0 -1
- data/lib/archsight/web/public/vue/railroad-74A4TZTK-CzONK5VY.js +0 -1
- data/lib/archsight/web/public/vue/railroad-abnf-HS5TGJTU-9zRPsDsx.js +0 -1
- data/lib/archsight/web/public/vue/railroad-ebnf-LZEXJU2U-BJ4dkz9l.js +0 -1
- data/lib/archsight/web/public/vue/railroad-peg-WCYAUIDC-DFy_rqAj.js +0 -1
- data/lib/archsight/web/public/vue/treeView-Q6P3EWNA-2cq5CyD2.js +0 -1
- data/lib/archsight/web/public/vue/treemap-WGGIJYW6-DK-e9Ez4.js +0 -1
- data/lib/archsight/web/public/vue/wardley-WFR3VGLG-BQwqUqfB.js +0 -1
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Archsight
|
|
4
|
+
class Linter
|
|
5
|
+
# Structural checks over the declared relations, the rules of "Choosing the Kind" and "Choosing the Kind:
|
|
6
|
+
# Infrastructure" in the modeling guide. All findings are warnings: they point at modeling decisions to review and
|
|
7
|
+
# have legitimate exceptions (internal services, capabilities that no product carries yet).
|
|
8
|
+
class Structure
|
|
9
|
+
# Network types that run over another network and need one that serves them
|
|
10
|
+
OVERLAY_TYPES = %w[overlay vpn].freeze
|
|
11
|
+
NETWORK_DEVICE_TYPES = %w[switch router].freeze
|
|
12
|
+
|
|
13
|
+
def initialize(database)
|
|
14
|
+
@database = database
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Findings in a stable order, each starting with the path reference of the resource
|
|
18
|
+
def warnings
|
|
19
|
+
[
|
|
20
|
+
component_in_several_services,
|
|
21
|
+
artifact_in_several_components,
|
|
22
|
+
service_with_same_named_component,
|
|
23
|
+
product_without_service,
|
|
24
|
+
service_reached_from_nothing,
|
|
25
|
+
capability_without_product,
|
|
26
|
+
generated_component_in_several_repositories,
|
|
27
|
+
link_not_between_two_facilities,
|
|
28
|
+
overlay_without_underlay,
|
|
29
|
+
facility_without_location,
|
|
30
|
+
equipment_without_facility,
|
|
31
|
+
device_without_facility,
|
|
32
|
+
device_connected_to_itself,
|
|
33
|
+
network_device_realizing_no_network,
|
|
34
|
+
device_served_apart_from_its_equipment
|
|
35
|
+
].flatten.sort
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def instances(kind)
|
|
41
|
+
@database.instances_by_kind(kind).values
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def targets(instance, verb, key)
|
|
45
|
+
instance.relations(verb, key)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def warning(instance, message)
|
|
49
|
+
"#{instance.path_ref}: #{instance.klass} '#{instance.name}' #{message}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def names(list)
|
|
53
|
+
list.map { |item| "'#{item.name}'" }.join(", ")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# { target => [owners that list it under verb/key] } for the instances of `kind`
|
|
57
|
+
def owners_by_target(kind, verb, key)
|
|
58
|
+
owners = Hash.new { |hash, target| hash[target] = [] }
|
|
59
|
+
instances(kind).each { |owner| targets(owner, verb, key).each { |target| owners[target] << owner } }
|
|
60
|
+
owners
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def component_in_several_services
|
|
64
|
+
shared("ApplicationService", :realizedThrough, :applicationComponents, "services")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def artifact_in_several_components
|
|
68
|
+
shared("ApplicationComponent", :realizedThrough, :technologyArtifacts, "components")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def shared(kind, verb, key, plural)
|
|
72
|
+
owners_by_target(kind, verb, key).filter_map do |target, owners|
|
|
73
|
+
next if owners.length < 2
|
|
74
|
+
|
|
75
|
+
warning(target, "is realized by #{owners.length} #{plural}: #{names(owners.sort_by(&:name))}")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def service_with_same_named_component
|
|
80
|
+
instances("ApplicationService").filter_map do |service|
|
|
81
|
+
components = targets(service, :realizedThrough, :applicationComponents)
|
|
82
|
+
next unless components.length == 1 && components.first.name == service.name
|
|
83
|
+
|
|
84
|
+
warning(service, "has one component with the same name")
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def product_without_service
|
|
89
|
+
instances("BusinessProduct").filter_map do |product|
|
|
90
|
+
next unless targets(product, :servedBy, :applicationServices).empty?
|
|
91
|
+
|
|
92
|
+
warning(product, "is served by no ApplicationService")
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# A service is reached when a product or process is served by it, directly or through services that depend on it
|
|
97
|
+
def service_reached_from_nothing
|
|
98
|
+
reached = reached_services
|
|
99
|
+
instances("ApplicationService").filter_map do |service|
|
|
100
|
+
next if reached.include?(service)
|
|
101
|
+
|
|
102
|
+
warning(service, "is not served to any product or process")
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def reached_services
|
|
107
|
+
reached = []
|
|
108
|
+
queue = (instances("BusinessProduct") + instances("BusinessProcess")).flat_map { |item| targets(item, :servedBy, :applicationServices) }
|
|
109
|
+
until queue.empty?
|
|
110
|
+
service = queue.shift
|
|
111
|
+
next if reached.include?(service)
|
|
112
|
+
|
|
113
|
+
reached << service
|
|
114
|
+
queue.concat(targets(service, :dependsOn, :applicationServices))
|
|
115
|
+
end
|
|
116
|
+
reached
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def capability_without_product
|
|
120
|
+
realized = owners_by_target("BusinessProduct", :realizes, :strategyCapabilities)
|
|
121
|
+
instances("StrategyCapability").filter_map do |capability|
|
|
122
|
+
next if realized.key?(capability)
|
|
123
|
+
|
|
124
|
+
warning(capability, "is realized by no product")
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# A generated component realized by several repositories is two components merged into one: the repositories
|
|
129
|
+
# declare the same module path. A re-import gives each its own name (GoModuleParser#owned_component_name).
|
|
130
|
+
def generated_component_in_several_repositories
|
|
131
|
+
instances("ApplicationComponent").filter_map do |component|
|
|
132
|
+
next unless component.annotations.key?("generated/script")
|
|
133
|
+
|
|
134
|
+
repositories = targets(component, :realizedThrough, :technologyArtifacts).select { |a| a.name.start_with?("Repo:") }
|
|
135
|
+
next if repositories.length < 2
|
|
136
|
+
|
|
137
|
+
warning(component, "is generated from #{repositories.length} repositories with the same module path: " \
|
|
138
|
+
"#{names(repositories.sort_by(&:name))}")
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# ---- infrastructure
|
|
143
|
+
|
|
144
|
+
def link_not_between_two_facilities
|
|
145
|
+
instances("TechnologyEquipment").filter_map do |equipment|
|
|
146
|
+
next unless equipment.annotations["equipment/type"] == "link"
|
|
147
|
+
|
|
148
|
+
count = targets(equipment, :locatedAt, :technologyFacilities).length
|
|
149
|
+
next if count == 2
|
|
150
|
+
|
|
151
|
+
warning(equipment, "is a link located at #{count} #{count == 1 ? "facility" : "facilities"}, not 2")
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def overlay_without_underlay
|
|
156
|
+
instances("TechnologyCommunicationNetwork").filter_map do |network|
|
|
157
|
+
type = network.annotations["network/type"]
|
|
158
|
+
next unless OVERLAY_TYPES.include?(type)
|
|
159
|
+
next unless targets(network, :servedBy, :technologyCommunicationNetworks).empty?
|
|
160
|
+
|
|
161
|
+
warning(network, "is #{type == "overlay" ? "an" : "a"} #{type} network served by no network")
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Only the outermost facility needs a location; nested halls inherit it from their campus
|
|
166
|
+
def facility_without_location
|
|
167
|
+
nested = owners_by_target("TechnologyFacility", :composedOf, :technologyFacilities)
|
|
168
|
+
instances("TechnologyFacility").filter_map do |facility|
|
|
169
|
+
next if nested.key?(facility) || targets(facility, :locatedAt, :locations).any?
|
|
170
|
+
|
|
171
|
+
warning(facility, "is located at no Location")
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# TechnologyEquipment that is part of other equipment is placed through its whole, and a whole (a backbone) through its
|
|
176
|
+
# parts (the links)
|
|
177
|
+
def equipment_without_facility
|
|
178
|
+
parts = owners_by_target("TechnologyEquipment", :composedOf, :technologyEquipment)
|
|
179
|
+
instances("TechnologyEquipment").filter_map do |equipment|
|
|
180
|
+
next if parts.key?(equipment) || targets(equipment, :composedOf, :technologyEquipment).any?
|
|
181
|
+
next if targets(equipment, :locatedAt, :technologyFacilities).any?
|
|
182
|
+
|
|
183
|
+
warning(equipment, "is located at no TechnologyFacility")
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# A device that is part of equipment (a rack) or of another device (a virtual chassis) is placed through its
|
|
188
|
+
# whole, and a chassis through its members
|
|
189
|
+
def device_without_facility
|
|
190
|
+
parts = owners_by_target("TechnologyDevice", :composedOf, :technologyDevices)
|
|
191
|
+
racked = owners_by_target("TechnologyEquipment", :composedOf, :technologyDevices)
|
|
192
|
+
instances("TechnologyDevice").filter_map do |device|
|
|
193
|
+
next if parts.key?(device) || racked.key?(device) || targets(device, :composedOf, :technologyDevices).any?
|
|
194
|
+
next if targets(device, :locatedAt, :technologyFacilities).any?
|
|
195
|
+
|
|
196
|
+
warning(device, "is located at no TechnologyFacility")
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def device_connected_to_itself
|
|
201
|
+
instances("TechnologyDevice").filter_map do |device|
|
|
202
|
+
next unless targets(device, :connectedTo, :technologyDevices).include?(device)
|
|
203
|
+
|
|
204
|
+
warning(device, "is connectedTo itself")
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# The members of a virtual chassis or stack realize the network through the chassis
|
|
209
|
+
def network_device_realizing_no_network
|
|
210
|
+
realized = owners_by_target("TechnologyCommunicationNetwork", :realizedBy, :technologyDevices)
|
|
211
|
+
members = owners_by_target("TechnologyDevice", :composedOf, :technologyDevices)
|
|
212
|
+
instances("TechnologyDevice").filter_map do |device|
|
|
213
|
+
type = device.annotations["device/type"]
|
|
214
|
+
next unless NETWORK_DEVICE_TYPES.include?(type)
|
|
215
|
+
next if realized.key?(device) || members.key?(device)
|
|
216
|
+
|
|
217
|
+
warning(device, "is a #{type} that realizes no TechnologyCommunicationNetwork")
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# A device in a partition (equipment servedBy a team) serves that team; one that names another team has been
|
|
222
|
+
# handed over without moving it. A device that names no team is served through its equipment.
|
|
223
|
+
def device_served_apart_from_its_equipment
|
|
224
|
+
holders = owners_by_target("TechnologyEquipment", :composedOf, :technologyDevices)
|
|
225
|
+
instances("TechnologyDevice").filter_map do |device|
|
|
226
|
+
teams = targets(device, :servedBy, :businessActors)
|
|
227
|
+
equipment_teams = holders.fetch(device, []).flat_map { |equipment| targets(equipment, :servedBy, :businessActors) }
|
|
228
|
+
next if teams.empty? || equipment_teams.empty?
|
|
229
|
+
|
|
230
|
+
others = teams - equipment_teams
|
|
231
|
+
next if others.empty?
|
|
232
|
+
|
|
233
|
+
warning(device, "is servedBy #{names(others.sort_by(&:name))}, not by the team of its equipment " \
|
|
234
|
+
"(#{names(equipment_teams.uniq.sort_by(&:name))})")
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
data/lib/archsight/linter.rb
CHANGED
|
@@ -2,18 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
require "kramdown"
|
|
4
4
|
require_relative "helpers"
|
|
5
|
+
require_relative "linter/structure"
|
|
5
6
|
|
|
6
7
|
module Archsight
|
|
7
8
|
class Linter
|
|
8
9
|
# Valid @component references in View annotations
|
|
9
10
|
VALID_COMPONENTS = %w[activity git jira languages owner repositories status].freeze
|
|
10
11
|
|
|
12
|
+
# Relations the cycle check leaves out. Relations are followed from the dependent to what it relies on, so
|
|
13
|
+
# the declared ones form a DAG; `dependsOn` is the exception because components may depend on each other at
|
|
14
|
+
# runtime, and `connectedTo` because cabling between devices is cyclic by nature (a ring, a mesh, both ends of
|
|
15
|
+
# a cable). The derived relations (`mentions`, `depicts`) are sideways views and are not declared, so they
|
|
16
|
+
# never enter the check.
|
|
17
|
+
CYCLE_EXEMPT_VERBS = %i[dependsOn connectedTo].freeze
|
|
18
|
+
|
|
19
|
+
# Structure relations between elements of one kind (specialization, composition, aggregation). Each verb is
|
|
20
|
+
# checked on its own: a chain of one of them must not lead back to where it started. They stay out of the
|
|
21
|
+
# general check above so that a cycle is reported once.
|
|
22
|
+
HIERARCHY_VERBS = %i[specializes composedOf aggregates].freeze
|
|
23
|
+
|
|
11
24
|
def initialize(database)
|
|
12
25
|
@database = database
|
|
13
26
|
@errors = []
|
|
14
27
|
end
|
|
15
28
|
|
|
16
29
|
def validate
|
|
30
|
+
validate_imports
|
|
17
31
|
@database.instances.each_value do |instances_hash|
|
|
18
32
|
instances_hash.each_value do |instance|
|
|
19
33
|
validate_instance_annotations(instance)
|
|
@@ -22,18 +36,73 @@ module Archsight
|
|
|
22
36
|
validate_menu(instance) if instance.klass == "PageMenu"
|
|
23
37
|
end
|
|
24
38
|
end
|
|
39
|
+
validate_relation_cycles
|
|
40
|
+
validate_hierarchy_cycles
|
|
41
|
+
validate_unique_annotations
|
|
25
42
|
|
|
26
43
|
@errors
|
|
27
44
|
end
|
|
28
45
|
|
|
29
|
-
# Things that still work but should be changed: renamed kinds and relation keys that the files still use
|
|
30
|
-
# They are not errors, so they do not make `archsight lint` fail.
|
|
46
|
+
# Things that still work but should be changed: renamed kinds and relation keys that the files still use, and
|
|
47
|
+
# annotations their kind marks `deprecated:`. They are not errors, so they do not make `archsight lint` fail.
|
|
31
48
|
def warnings
|
|
32
|
-
@database.respond_to?(:deprecations) ? @database.deprecations.map(&:to_s) : []
|
|
49
|
+
renamed = @database.respond_to?(:deprecations) ? @database.deprecations.map(&:to_s) : []
|
|
50
|
+
renamed + deprecated_annotations
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Structural modeling checks (rules of "Choosing the Kind", see Linter::Structure). Warnings, never errors.
|
|
54
|
+
def structural_warnings
|
|
55
|
+
Structure.new(@database).warnings
|
|
33
56
|
end
|
|
34
57
|
|
|
35
58
|
private
|
|
36
59
|
|
|
60
|
+
# Each use of an annotation the kind marks `deprecated:`, with what to use instead
|
|
61
|
+
def deprecated_annotations
|
|
62
|
+
@database.instances.each_value.flat_map do |instances_hash|
|
|
63
|
+
instances_hash.each_value.flat_map do |instance|
|
|
64
|
+
instance.annotations.keys.filter_map do |key|
|
|
65
|
+
annotation = instance.class.annotation_matching(key)
|
|
66
|
+
"#{instance.path_ref}: #{instance.klass} '#{instance.name}' uses deprecated annotation '#{key}': #{annotation.deprecated}" if annotation&.deprecated?
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Imports: the handlers of the resources repository load (before the `import/handler` names are checked against
|
|
73
|
+
# the registry), and every `import/dependsOn` entry names an import or is a pattern
|
|
74
|
+
def validate_imports
|
|
75
|
+
return unless @database.respond_to?(:instances_by_kind)
|
|
76
|
+
|
|
77
|
+
imports = @database.instances_by_kind("Import")&.values || []
|
|
78
|
+
return if imports.empty?
|
|
79
|
+
|
|
80
|
+
require "archsight/import/handler_loader"
|
|
81
|
+
Archsight::Import::HandlerLoader.load(@database).each { |problem| @errors << problem.to_s }
|
|
82
|
+
names = imports.to_set(&:name)
|
|
83
|
+
imports.each do |import|
|
|
84
|
+
Archsight::Import.declared_dependencies(import).each do |entry|
|
|
85
|
+
next if Archsight::Import.pattern?(entry) || names.include?(entry)
|
|
86
|
+
|
|
87
|
+
@errors << "#{import.path_ref}: Import '#{import.name}' depends on unknown import '#{entry}'"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
validate_command_outputs(imports)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# A command import replaces its whole output file on every run, so no other import may write to it
|
|
94
|
+
def validate_command_outputs(imports)
|
|
95
|
+
file_of = ->(import) { File.expand_path(Archsight::Import.output_path(import), "/") }
|
|
96
|
+
imports.select { |import| import.annotations["import/handler"] == "command" }.each do |command|
|
|
97
|
+
others = imports.reject { |import| import.equal?(command) }.select { |import| file_of.call(import) == file_of.call(command) }
|
|
98
|
+
next if others.empty?
|
|
99
|
+
|
|
100
|
+
@errors << "#{command.path_ref}: Import '#{command.name}' runs a command, which replaces its output file " \
|
|
101
|
+
"#{Archsight::Import.output_path(command)}, but #{others.map { |o| "'#{o.name}'" }.join(", ")} " \
|
|
102
|
+
"also write#{"s" if others.one?} to it; give it a file of its own"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
37
106
|
# Pages need exactly one menu and their [[links]] must resolve
|
|
38
107
|
def validate_page(page)
|
|
39
108
|
menus = page.references.map { |r| r[:instance] }.select { |i| i.klass == "PageMenu" }.uniq
|
|
@@ -79,6 +148,140 @@ module Archsight
|
|
|
79
148
|
nil
|
|
80
149
|
end
|
|
81
150
|
|
|
151
|
+
# The values of a unique annotation (aliases) must not be shared by two resources of a kind
|
|
152
|
+
def validate_unique_annotations
|
|
153
|
+
@database.instances.each do |klass, instances_hash|
|
|
154
|
+
next unless klass.respond_to?(:annotations)
|
|
155
|
+
|
|
156
|
+
klass.annotations.select(&:unique?).reject(&:pattern?).each do |annotation|
|
|
157
|
+
owners = {}
|
|
158
|
+
instances_hash.each_value { |instance| check_unique_values(annotation, instance, owners) }
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def check_unique_values(annotation, instance, owners)
|
|
164
|
+
Array(annotation.value_for(instance)).each do |value|
|
|
165
|
+
owner = owners[value]
|
|
166
|
+
if owner.nil?
|
|
167
|
+
owners[value] = instance
|
|
168
|
+
elsif !owner.equal?(instance)
|
|
169
|
+
@errors << "#{instance.path_ref}: #{instance.klass} '#{instance.name}': #{annotation.key} '#{value}' " \
|
|
170
|
+
"is already used by '#{owner.name}' (#{owner.path_ref})"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Declared relations must not lead back to where they started (see the "Direction of Relations" modeling guide)
|
|
176
|
+
def validate_relation_cycles
|
|
177
|
+
graph = relation_graph { |verb| !CYCLE_EXEMPT_VERBS.include?(verb) && !HIERARCHY_VERBS.include?(verb) }
|
|
178
|
+
report_cycles(graph) { |start, path| "#{start.path_ref}: #{start.klass} '#{start.name}' is part of a relation cycle (#{path})" }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# `specializes`, `composedOf` and `aggregates` (one chain per verb) must not lead back to where they started
|
|
182
|
+
def validate_hierarchy_cycles
|
|
183
|
+
HIERARCHY_VERBS.each do |hierarchy_verb|
|
|
184
|
+
graph = relation_graph { |verb| verb == hierarchy_verb }
|
|
185
|
+
report_cycles(graph) { |start, path| "#{start.path_ref}: #{start.klass} '#{start.name}' is part of a cycle of #{hierarchy_verb} relations (#{path})" }
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Adds one error per group of instances that reach each other; the block builds the message from the first
|
|
190
|
+
# instance of the group and the path of one concrete cycle
|
|
191
|
+
def report_cycles(graph)
|
|
192
|
+
strongly_connected_groups(graph).each do |group|
|
|
193
|
+
cycle = cycle_through(group.first, group, graph)
|
|
194
|
+
start = group.first
|
|
195
|
+
path = ([start.name] + cycle.map { |step| step[:to].name }).zip(cycle.map { |step| step[:verb] }).flat_map { |name, verb| [name, verb && "-#{verb}->"] }.compact
|
|
196
|
+
@errors << yield(start, path.join(" "))
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# { instance => [{ verb:, to: }] } over the declared relations whose verb the block accepts
|
|
201
|
+
def relation_graph
|
|
202
|
+
graph = {}
|
|
203
|
+
@database.instances.each_value do |instances_hash|
|
|
204
|
+
instances_hash.each_value do |instance|
|
|
205
|
+
graph[instance] = instance.class.declared_relations.flat_map do |verb, key, _kind|
|
|
206
|
+
next [] unless yield(verb)
|
|
207
|
+
|
|
208
|
+
instance.relations(verb, key).map { |target| { verb: verb, to: target } }
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
graph
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Groups of instances that reach each other (iterative Tarjan, the graph can be deep); a single instance counts
|
|
216
|
+
# only when it points at itself
|
|
217
|
+
def strongly_connected_groups(graph)
|
|
218
|
+
index = {}
|
|
219
|
+
lowlink = {}
|
|
220
|
+
on_stack = {}
|
|
221
|
+
stack = []
|
|
222
|
+
groups = []
|
|
223
|
+
counter = 0
|
|
224
|
+
|
|
225
|
+
graph.each_key do |root|
|
|
226
|
+
next if index.key?(root)
|
|
227
|
+
|
|
228
|
+
work = [[root, 0]]
|
|
229
|
+
until work.empty?
|
|
230
|
+
node, edge = work.last
|
|
231
|
+
if edge.zero?
|
|
232
|
+
index[node] = lowlink[node] = counter
|
|
233
|
+
counter += 1
|
|
234
|
+
stack << node
|
|
235
|
+
on_stack[node] = true
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
edges = graph[node] || []
|
|
239
|
+
if edge < edges.length
|
|
240
|
+
work.last[1] += 1
|
|
241
|
+
target = edges[edge][:to]
|
|
242
|
+
if !index.key?(target)
|
|
243
|
+
work << [target, 0]
|
|
244
|
+
elsif on_stack[target]
|
|
245
|
+
lowlink[node] = [lowlink[node], index[target]].min
|
|
246
|
+
end
|
|
247
|
+
else
|
|
248
|
+
work.pop
|
|
249
|
+
lowlink[work.last.first] = [lowlink[work.last.first], lowlink[node]].min unless work.empty?
|
|
250
|
+
next unless lowlink[node] == index[node]
|
|
251
|
+
|
|
252
|
+
group = []
|
|
253
|
+
loop do
|
|
254
|
+
member = stack.pop
|
|
255
|
+
on_stack[member] = false
|
|
256
|
+
group << member
|
|
257
|
+
break if member.equal?(node)
|
|
258
|
+
end
|
|
259
|
+
groups << group if group.length > 1 || edges.any? { |e| e[:to].equal?(node) }
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
groups
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# One concrete cycle through `start` inside its group: [{ verb:, to: }, ...] ending at `start`
|
|
267
|
+
def cycle_through(start, group, graph)
|
|
268
|
+
members = group.to_h { |member| [member, true] }
|
|
269
|
+
queue = [[start, []]]
|
|
270
|
+
seen = { start => true }
|
|
271
|
+
until queue.empty?
|
|
272
|
+
node, path = queue.shift
|
|
273
|
+
graph[node].each do |edge|
|
|
274
|
+
next unless members[edge[:to]]
|
|
275
|
+
return path + [edge] if edge[:to].equal?(start)
|
|
276
|
+
next if seen[edge[:to]]
|
|
277
|
+
|
|
278
|
+
seen[edge[:to]] = true
|
|
279
|
+
queue << [edge[:to], path + [edge]]
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
[]
|
|
283
|
+
end
|
|
284
|
+
|
|
82
285
|
def validate_instance_annotations(instance)
|
|
83
286
|
instance.annotations.each do |key, value|
|
|
84
287
|
# Find matching annotation definition (handles both exact and pattern matches)
|
|
@@ -97,12 +300,20 @@ module Archsight
|
|
|
97
300
|
next unless annotation.has_validation?
|
|
98
301
|
|
|
99
302
|
# Validate value against annotation schema (type and enum constraints)
|
|
100
|
-
annotation.validate(value)
|
|
101
|
-
|
|
102
|
-
|
|
303
|
+
errors = annotation.validate(value)
|
|
304
|
+
errors.each { |error| @errors << "#{instance.path_ref}: Annotation '#{key}' #{error}" }
|
|
305
|
+
validate_resource_reference(instance, key, value) if annotation.resource? && errors.empty?
|
|
103
306
|
end
|
|
104
307
|
end
|
|
105
308
|
|
|
309
|
+
# The resource a `Name` / `Kind/Name` annotation points to must exist; a bare `Name` means the same kind
|
|
310
|
+
def validate_resource_reference(instance, key, value)
|
|
311
|
+
return if Archsight::Helpers::ResourceResolver.new(@database).find_annotated(value, instance.klass).is_a?(Array)
|
|
312
|
+
|
|
313
|
+
hint = value.to_s.include?("/") ? "" : " (a name without Kind/ is a #{instance.klass}, use Kind/Name for another kind)"
|
|
314
|
+
@errors << "#{instance.path_ref}: Annotation '#{key}' resource '#{value}' not found#{hint}"
|
|
315
|
+
end
|
|
316
|
+
|
|
106
317
|
def validate_markdown(instance, key, value)
|
|
107
318
|
# Skip markdown validation for generated files
|
|
108
319
|
return if instance.annotations.key?("generated/script")
|
|
@@ -62,12 +62,23 @@ module Archsight
|
|
|
62
62
|
meta.each do |key, value|
|
|
63
63
|
next if value.nil?
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
if key == "links" && value.is_a?(Hash)
|
|
66
|
+
links(value).each { |name, url| result["link/#{name}"] = url }
|
|
67
|
+
else
|
|
68
|
+
result["page/#{key}"] = format_value(key, value)
|
|
69
|
+
end
|
|
66
70
|
end
|
|
67
71
|
result["page/content"] = body
|
|
68
72
|
result
|
|
69
73
|
end
|
|
70
74
|
|
|
75
|
+
# `links: { confluence: <url> }` of the frontmatter: one link/<name> annotation per entry
|
|
76
|
+
def links(mapping)
|
|
77
|
+
mapping.each_with_object({}) do |(name, url), result|
|
|
78
|
+
result[name.to_s] = url.to_s unless url.nil?
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
71
82
|
def format_value(key, value)
|
|
72
83
|
case value
|
|
73
84
|
when Hash then value.map { |k, v| "#{k}: #{v.to_s.gsub(/\s*\n\s*/, " ")}" }.join("\n")
|
|
@@ -307,17 +307,24 @@ class Archsight::Query::Evaluator
|
|
|
307
307
|
query_values.include?(instance_kind)
|
|
308
308
|
end
|
|
309
309
|
|
|
310
|
+
# The name of a resource and its aliases (architecture/aliases): the name shortcut finds a resource by either
|
|
311
|
+
def names_of(instance)
|
|
312
|
+
aliases = instance.respond_to?(:annotations) ? instance.annotations&.[]("architecture/aliases") : nil
|
|
313
|
+
[instance.name, *aliases.to_s.split(",").map(&:strip).reject(&:empty?)].compact
|
|
314
|
+
end
|
|
315
|
+
|
|
310
316
|
def evaluate_name_condition(node, instance)
|
|
311
|
-
|
|
312
|
-
return false unless name
|
|
317
|
+
return false unless instance.name
|
|
313
318
|
|
|
319
|
+
names = names_of(instance)
|
|
314
320
|
case node.operator
|
|
315
321
|
when "=="
|
|
316
|
-
|
|
322
|
+
names.include?(node.value.value.to_s)
|
|
317
323
|
when "!="
|
|
318
|
-
|
|
324
|
+
names.none?(node.value.value.to_s)
|
|
319
325
|
when "=~"
|
|
320
|
-
|
|
326
|
+
regex = build_regex_from_value(node.value)
|
|
327
|
+
names.any? { |name| name.match?(regex) }
|
|
321
328
|
else
|
|
322
329
|
false
|
|
323
330
|
end
|
|
@@ -328,7 +335,7 @@ class Archsight::Query::Evaluator
|
|
|
328
335
|
return false unless name
|
|
329
336
|
|
|
330
337
|
query_values = node.values.map { |v| v.value.to_s }
|
|
331
|
-
|
|
338
|
+
names_of(instance).intersect?(query_values)
|
|
332
339
|
end
|
|
333
340
|
|
|
334
341
|
# Outgoing relations: what does this resource point to?
|
|
@@ -5,16 +5,21 @@ require_relative "ast"
|
|
|
5
5
|
|
|
6
6
|
# Recursive descent parser for the architecture query language.
|
|
7
7
|
class Archsight::Query::Parser
|
|
8
|
-
|
|
8
|
+
# Characters a resource name is made of, as the lexer reads an identifier
|
|
9
|
+
NAME_CHAR = %r{[a-zA-Z0-9_\-/.]}
|
|
10
|
+
|
|
11
|
+
# @param source [String, nil] the query text; with it, names that contain colons (`Archsight:CLI`, `ITGS:A001`)
|
|
12
|
+
# are read as one name instead of a kind prefix and a condition
|
|
13
|
+
def initialize(tokens, source: nil)
|
|
9
14
|
@tokens = tokens
|
|
15
|
+
@source = source
|
|
10
16
|
@position = 0
|
|
11
17
|
end
|
|
12
18
|
|
|
13
19
|
def parse
|
|
14
20
|
# Check for kind filter prefix: "Kind: expression"
|
|
15
21
|
kind_filter = nil
|
|
16
|
-
|
|
17
|
-
if current_token.type == :IDENTIFIER && peek_token&.type == :COLON && (current_token.value =~ /^[A-Z]/)
|
|
22
|
+
if kind_prefix?
|
|
18
23
|
kind_filter = current_token.value
|
|
19
24
|
advance # consume identifier
|
|
20
25
|
advance # consume colon
|
|
@@ -256,8 +261,7 @@ class Archsight::Query::Parser
|
|
|
256
261
|
|
|
257
262
|
# Parse inner query: optional kind filter + optional expression
|
|
258
263
|
kind_filter = nil
|
|
259
|
-
|
|
260
|
-
if current_token.type == :IDENTIFIER && peek_token&.type == :COLON && (current_token.value =~ /^[A-Z]/)
|
|
264
|
+
if kind_prefix?
|
|
261
265
|
kind_filter = current_token.value
|
|
262
266
|
advance # consume identifier
|
|
263
267
|
advance # consume colon
|
|
@@ -395,8 +399,71 @@ class Archsight::Query::Parser
|
|
|
395
399
|
Archsight::Query::AST::AnnotationExistsCondition.new(path)
|
|
396
400
|
else
|
|
397
401
|
# Bare identifier - treat as name =~ "identifier"
|
|
398
|
-
Archsight::Query::AST::NameCondition.new("=~", Archsight::Query::AST::StringValue.new(path))
|
|
402
|
+
Archsight::Query::AST::NameCondition.new("=~", Archsight::Query::AST::StringValue.new(glue_name(path)))
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# Names contain colons (`Archsight:CLI`, `ITGS:A001`). A bare name written without spaces around its colons is
|
|
407
|
+
# one word: `ApplicationService:ITGS:A001` is the kind and the name `ITGS:A001`.
|
|
408
|
+
def glue_name(first)
|
|
409
|
+
name = first
|
|
410
|
+
while (word = word_after_colon)
|
|
411
|
+
name += ":#{word}"
|
|
412
|
+
advance # consume the colon
|
|
413
|
+
advance while current_token.type != :EOF && current_token.position < @colon_end
|
|
414
|
+
end
|
|
415
|
+
name
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# The name word right after the current colon token, when the colon touches a name character on both sides
|
|
419
|
+
def word_after_colon
|
|
420
|
+
return nil unless @source && current_token.type == :COLON
|
|
421
|
+
|
|
422
|
+
colon = current_token.position
|
|
423
|
+
return nil unless colon.positive? && @source[colon - 1]&.match?(NAME_CHAR)
|
|
424
|
+
|
|
425
|
+
word = @source[(colon + 1)..].to_s[/\A#{NAME_CHAR}+/]
|
|
426
|
+
@colon_end = colon + 1 + word.length if word
|
|
427
|
+
word
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# `Kind: expression` or `Kind:`. The prefix is a kind when it starts with a capital letter and the colon is followed
|
|
431
|
+
# by a space or nothing; `Name:Word` without a space, where the prefix is no kind, is a name with a colon
|
|
432
|
+
# (`ITGS:A001`) that the bare word search handles.
|
|
433
|
+
def kind_prefix?
|
|
434
|
+
return false unless current_token.type == :IDENTIFIER && peek_token&.type == :COLON && current_token.value =~ /^[A-Z]/
|
|
435
|
+
return true if known_kind?(current_token.value)
|
|
436
|
+
|
|
437
|
+
!colon_glued?
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def known_kind?(name)
|
|
441
|
+
defined?(Archsight::Resources) && !Archsight::Resources[name].nil?
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# Whether the current token starts a name with colons (`ITGS:A001`) that is a bare word, not the start of a
|
|
445
|
+
# condition: the colons touch name characters, and no operator or `?` follows the name
|
|
446
|
+
def colon_glued?
|
|
447
|
+
return false unless @source
|
|
448
|
+
|
|
449
|
+
stop = name_run_end(peek_token.position)
|
|
450
|
+
return false unless stop
|
|
451
|
+
|
|
452
|
+
following = @tokens.find { |token| token.position >= stop }
|
|
453
|
+
!(following && %i[EQ NEQ MATCH GT LT GTE LTE IN QUESTION].include?(following.type))
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# Where `a:b:c` ends, starting at its first colon; nil when that colon does not touch a name character on both sides
|
|
457
|
+
def name_run_end(colon)
|
|
458
|
+
stop = nil
|
|
459
|
+
while @source[colon] == ":" && colon.positive? && @source[colon - 1].match?(NAME_CHAR)
|
|
460
|
+
word = @source[(colon + 1)..].to_s[/\A#{NAME_CHAR}+/]
|
|
461
|
+
break unless word
|
|
462
|
+
|
|
463
|
+
stop = colon + 1 + word.length
|
|
464
|
+
colon = stop
|
|
399
465
|
end
|
|
466
|
+
stop
|
|
400
467
|
end
|
|
401
468
|
|
|
402
469
|
def parse_quoted_annotation_path
|