33require "yaml"
44require_relative "resources"
55require_relative "editor/file_writer"
6+ require_relative "editor/content_hasher"
67
78module Archsight
89 # Editor handles building and validating resources for the web editor
9- class Editor
10+ module Editor
11+ module_function
12+
1013 # Build a resource hash from form params
1114 # @param kind [String] Resource kind (e.g., "TechnologyArtifact")
1215 # @param name [String] Resource name
1316 # @param annotations [Hash] Annotation key-value pairs
1417 # @param relations [Array<Hash>] Array of {verb:, kind:, names:[]} hashes
1518 # where kind is the target class name (e.g., "TechnologyArtifact")
1619 # @return [Hash] Resource hash ready for YAML conversion
17- def self . build_resource ( kind :, name :, annotations : { } , relations : [ ] )
20+ def build_resource ( kind :, name :, annotations : { } , relations : [ ] )
1821 resource = {
1922 "apiVersion" => "architecture/v1alpha1" ,
2023 "kind" => kind ,
@@ -39,7 +42,7 @@ def self.build_resource(kind:, name:, annotations: {}, relations: [])
3942 # @param name [String] Resource name
4043 # @param annotations [Hash] Annotation key-value pairs
4144 # @return [Hash] { valid: Boolean, errors: { field => [messages] } }
42- def self . validate ( kind , name :, annotations : { } )
45+ def validate ( kind , name :, annotations : { } )
4346 klass = Archsight ::Resources [ kind ]
4447 errors = { }
4548
@@ -66,7 +69,7 @@ def self.validate(kind, name:, annotations: {})
6669 # Uses custom YAML dump that formats multiline strings with literal block scalars
6770 # @param resource_hash [Hash] Resource hash
6871 # @return [String] YAML string
69- def self . to_yaml ( resource_hash )
72+ def to_yaml ( resource_hash )
7073 visitor = Psych ::Visitors ::YAMLTree . create
7174 visitor << resource_hash
7275
@@ -79,7 +82,7 @@ def self.to_yaml(resource_hash)
7982
8083 # Recursively apply literal block style for multiline strings in YAML AST
8184 # @param node [Psych::Nodes::Node] YAML AST node
82- def self . apply_block_scalar_style ( node )
85+ def apply_block_scalar_style ( node )
8386 case node
8487 when Psych ::Nodes ::Scalar
8588 if node . value . is_a? ( String )
@@ -97,7 +100,7 @@ def self.apply_block_scalar_style(node)
97100 # Excludes pattern annotations, computed annotations, and annotations with editor: false
98101 # @param kind [String] Resource kind
99102 # @return [Array<Archsight::Annotations::Annotation>]
100- def self . editable_annotations ( kind )
103+ def editable_annotations ( kind )
101104 klass = Archsight ::Resources [ kind ]
102105 return [ ] unless klass
103106
@@ -115,7 +118,7 @@ def self.editable_annotations(kind)
115118 # Get available relations for a resource kind
116119 # @param kind [String] Resource kind
117120 # @return [Array<Array>] Array of [verb, target_kind, target_class_name]
118- def self . available_relations ( kind )
121+ def available_relations ( kind )
119122 klass = Archsight ::Resources [ kind ]
120123 return [ ] unless klass
121124
@@ -125,15 +128,15 @@ def self.available_relations(kind)
125128 # Get unique verbs for a resource kind's relations
126129 # @param kind [String] Resource kind
127130 # @return [Array<String>]
128- def self . relation_verbs ( kind )
131+ def relation_verbs ( kind )
129132 available_relations ( kind ) . map { |v , _ , _ | v . to_s } . uniq . sort
130133 end
131134
132135 # Get valid target class names for a given verb (for UI display and instance lookup)
133136 # @param kind [String] Resource kind
134137 # @param verb [String] Relation verb
135138 # @return [Array<String>] Target class names (e.g., "TechnologyArtifact")
136- def self . target_kinds_for_verb ( kind , verb )
139+ def target_kinds_for_verb ( kind , verb )
137140 # Relations structure is [verb, relation_name, target_class_name]
138141 available_relations ( kind )
139142 . select { |v , _ , _ | v . to_s == verb . to_s }
@@ -147,7 +150,7 @@ def self.target_kinds_for_verb(kind, verb)
147150 # @param verb [String] Relation verb
148151 # @param target_class [String] Target class name
149152 # @return [String, nil] Relation name (e.g., "technologyComponents")
150- def self . relation_name_for ( kind , verb , target_class )
153+ def relation_name_for ( kind , verb , target_class )
151154 relation = available_relations ( kind ) . find do |v , _ , tc |
152155 v . to_s == verb . to_s && tc . to_s == target_class . to_s
153156 end
@@ -161,7 +164,7 @@ def self.relation_name_for(kind, verb, target_class)
161164 # @param verb [String] Relation verb
162165 # @param relation_name [String] Relation name (e.g., "businessActors")
163166 # @return [String, nil] Target class name (e.g., "BusinessActor")
164- def self . target_class_for_relation ( kind , verb , relation_name )
167+ def target_class_for_relation ( kind , verb , relation_name )
165168 relation = available_relations ( kind ) . find do |v , rn , _ |
166169 v . to_s == verb . to_s && rn . to_s == relation_name . to_s
167170 end
@@ -170,65 +173,65 @@ def self.target_class_for_relation(kind, verb, relation_name)
170173 relation [ 2 ] . to_s
171174 end
172175
173- class << self
174- private
176+ # Filter out empty values from a hash and convert to plain Hash
177+ # (avoids !ruby/hash:Sinatra::IndifferentHash in YAML output)
178+ def filter_empty ( hash )
179+ return { } if hash . nil?
175180
176- # Filter out empty values from a hash and convert to plain Hash
177- # (avoids !ruby/hash:Sinatra::IndifferentHash in YAML output)
178- def filter_empty ( hash )
179- return { } if hash . nil?
181+ result = hash . reject { | _ , v | v . nil? || v . to_s . strip . empty? }
182+ # Convert to plain Hash to avoid Ruby-specific YAML tags
183+ result . to_h
184+ end
180185
181- result = hash . reject { |_ , v | v . nil? || v . to_s . strip . empty? }
182- # Convert to plain Hash to avoid Ruby-specific YAML tags
183- result . to_h
184- end
186+ # Build spec hash from relations array
187+ # @param source_kind [String] The source resource kind
188+ # @param relations [Array<Hash>] Array of {verb:, kind:, names:[]} hashes
189+ # where kind is the target class name (e.g., "TechnologyArtifact")
190+ # @return [Hash] Spec hash with proper relation_name keys
191+ def build_spec ( source_kind , relations )
192+ return { } if relations . nil? || relations . empty?
185193
186- # Build spec hash from relations array
187- # @param source_kind [String] The source resource kind
188- # @param relations [Array<Hash>] Array of {verb:, kind:, names:[]} hashes
189- # where kind is the target class name (e.g., "TechnologyArtifact")
190- # @return [Hash] Spec hash with proper relation_name keys
191- def build_spec ( source_kind , relations )
192- return { } if relations . nil? || relations . empty?
193-
194- spec = { }
195- relations . each { |rel | add_relation_to_spec ( spec , source_kind , rel ) }
196- deduplicate_spec_values ( spec )
197- end
194+ spec = { }
195+ relations . each { |rel | add_relation_to_spec ( spec , source_kind , rel ) }
196+ deduplicate_spec_values ( spec )
197+ end
198198
199- def add_relation_to_spec ( spec , source_kind , rel )
200- verb , target_class , names = extract_relation_parts ( rel )
201- return if invalid_relation? ( verb , target_class , names )
199+ def add_relation_to_spec ( spec , source_kind , rel )
200+ verb , target_class , names = extract_relation_parts ( rel )
201+ return if invalid_relation? ( verb , target_class , names )
202202
203- relation_name = Archsight :: Editor . relation_name_for ( source_kind , verb , target_class )
204- return unless relation_name
203+ rel_name = relation_name_for ( source_kind , verb , target_class )
204+ return unless rel_name
205205
206- spec [ verb . to_s ] ||= { }
207- spec [ verb . to_s ] [ relation_name ] ||= [ ]
208- spec [ verb . to_s ] [ relation_name ] . concat ( names )
209- end
206+ spec [ verb . to_s ] ||= { }
207+ spec [ verb . to_s ] [ rel_name ] ||= [ ]
208+ spec [ verb . to_s ] [ rel_name ] . concat ( names )
209+ end
210210
211- def extract_relation_parts ( rel )
212- verb = rel [ :verb ] || rel [ "verb" ]
213- target_class = rel [ :kind ] || rel [ "kind" ]
214- names = normalize_names ( rel [ :names ] || rel [ "names" ] || [ ] )
215- [ verb , target_class , names ]
216- end
211+ def extract_relation_parts ( rel )
212+ verb = rel [ :verb ] || rel [ "verb" ]
213+ target_class = rel [ :kind ] || rel [ "kind" ]
214+ names = normalize_names ( rel [ :names ] || rel [ "names" ] || [ ] )
215+ [ verb , target_class , names ]
216+ end
217217
218- def normalize_names ( names )
219- names = [ names ] unless names . is_a? ( Array )
220- names . map ( &:to_s ) . reject ( &:empty? )
221- end
218+ def normalize_names ( names )
219+ names = [ names ] unless names . is_a? ( Array )
220+ names . map ( &:to_s ) . reject ( &:empty? )
221+ end
222222
223- def invalid_relation? ( verb , target_class , names )
224- verb . nil? || verb . to_s . strip . empty? ||
225- target_class . nil? || target_class . to_s . strip . empty? ||
226- names . empty?
227- end
223+ def invalid_relation? ( verb , target_class , names )
224+ verb . nil? || verb . to_s . strip . empty? ||
225+ target_class . nil? || target_class . to_s . strip . empty? ||
226+ names . empty?
227+ end
228228
229- def deduplicate_spec_values ( spec )
230- spec . transform_values { |kinds | kinds . transform_values ( &:uniq ) }
231- end
229+ def deduplicate_spec_values ( spec )
230+ spec . transform_values { |kinds | kinds . transform_values ( &:uniq ) }
232231 end
232+
233+ private_class_method :filter_empty , :build_spec , :add_relation_to_spec ,
234+ :extract_relation_parts , :normalize_names , :invalid_relation? ,
235+ :deduplicate_spec_values
233236 end
234237end
0 commit comments