Class: Archsight::Resources::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/archsight/resources/base.rb

Overview

Base is the base for all assets, should not be used directly, only by inheritance

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(raw, path_ref) ⇒ Base

Returns a new instance of Base.



149
150
151
152
153
154
155
156
157
158
159
# File 'lib/archsight/resources/base.rb', line 149

def initialize(raw, path_ref)
  @raw = raw
  @path_ref = path_ref
  @references = []
  # Auto-discover annotation keys
  return unless annotations

  annotations.each_key do |key|
    self.class.discovered_annotations.add(key)
  end
end

Instance Attribute Details

#path_refObject

Returns the value of attribute path_ref.



9
10
11
# File 'lib/archsight/resources/base.rb', line 9

def path_ref
  @path_ref
end

#rawObject

Returns the value of attribute raw.



9
10
11
# File 'lib/archsight/resources/base.rb', line 9

def raw
  @raw
end

#referencesObject

Returns the value of attribute references.



9
10
11
# File 'lib/archsight/resources/base.rb', line 9

def references
  @references
end

Class Method Details

.annotation(key, description: nil, filter: nil, title: nil, format: nil, enum: nil, sidebar: true, type: nil, list: false, editor: true, validator: nil) ⇒ Object

Define an annotation using the Annotation class



27
28
29
30
31
32
33
# File 'lib/archsight/resources/base.rb', line 27

def self.annotation(key, description: nil, filter: nil, title: nil, format: nil, enum: nil, sidebar: true,
                    type: nil, list: false, editor: true, validator: nil)
  @annotations ||= [] #: Array[Archsight::Annotations::Annotation]
  options = { description: description, filter: filter, title: title, format: format, enum: enum,
              sidebar: sidebar, type: type, list: list, editor: editor, validator: validator }
  @annotations << Archsight::Annotations::Annotation.new(key, options)
end

.annotation_enum(key) ⇒ Object



99
100
101
# File 'lib/archsight/resources/base.rb', line 99

def self.annotation_enum(key)
  annotation_matching(key)&.enum
end

.annotation_format(key) ⇒ Object



95
96
97
# File 'lib/archsight/resources/base.rb', line 95

def self.annotation_format(key)
  annotation_matching(key)&.format
end

.annotation_matching(key) ⇒ Object

Find annotation definition matching a key (handles patterns)



72
73
74
# File 'lib/archsight/resources/base.rb', line 72

def self.annotation_matching(key)
  annotations.find { |a| a.matches?(key) }
end

.annotation_title(key) ⇒ Object



91
92
93
# File 'lib/archsight/resources/base.rb', line 91

def self.annotation_title(key)
  annotation_matching(key)&.title || key.split("/").last.capitalize
end

.annotationsObject

Get all annotation definitions



36
37
38
# File 'lib/archsight/resources/base.rb', line 36

def self.annotations
  @annotations || []
end

.computed_annotation(key, description: nil, filter: nil, title: nil, format: nil, enum: nil, sidebar: false, type: nil, list: false, editor: true) { ... } ⇒ Object

Define a computed annotation using a block Computed annotations are calculated from related resources after the database is loaded. Supports all the same options as regular annotations.

Parameters:

  • key (String)

    The annotation key (e.g., ‘computed/total_cost’)

  • description (String, nil) (defaults to: nil)

    Human-readable description

  • filter (Symbol, nil) (defaults to: nil)

    Filter type (:word, :list, or nil)

  • title (String, nil) (defaults to: nil)

    Display title

  • format (Symbol, nil) (defaults to: nil)

    Rendering format (:markdown, :tag_word, :tag_list)

  • enum (Array, nil) (defaults to: nil)

    Allowed values

  • sidebar (Boolean) (defaults to: false)

    Show in sidebar (default false for computed)

  • type (Class, nil) (defaults to: nil)

    Type for value coercion (Integer, Float, String)

  • list (Boolean) (defaults to: false)

    Whether values are lists (default false)

Yields:

  • Block that computes the annotation value, evaluated in Evaluator context



53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/archsight/resources/base.rb', line 53

def self.computed_annotation(key, description: nil, filter: nil, title: nil, format: nil, enum: nil,
                             sidebar: false, type: nil, list: false, editor: true, &)
  require_relative "../annotations/computed"
  @computed_annotations ||= [] #: Array[Archsight::Annotations::Computed]
  @computed_annotations << Archsight::Annotations::Computed.new(key, description: description, type: type, &)

  # Also register as a regular annotation so it passes validation and is recognized
  @annotations ||= [] #: Array[Archsight::Annotations::Annotation]
  options = { description: description, filter: filter, title: title, format: format, enum: enum,
              sidebar: sidebar, type: type, list: list, editor: editor }
  @annotations << Archsight::Annotations::Annotation.new(key, options)
end

.computed_annotationsObject

Get all computed annotation definitions



67
68
69
# File 'lib/archsight/resources/base.rb', line 67

def self.computed_annotations
  @computed_annotations || []
end

.description(text = nil) ⇒ Object



119
120
121
122
123
124
125
# File 'lib/archsight/resources/base.rb', line 119

def self.description(text = nil)
  if text
    @description = text
  else
    @description
  end
end

.discovered_annotationsObject



145
146
147
# File 'lib/archsight/resources/base.rb', line 145

def self.discovered_annotations
  @discovered_annotations ||= Set.new
end

.filterable_annotationsObject

Get filterable annotations as array of Annotation objects



82
83
84
# File 'lib/archsight/resources/base.rb', line 82

def self.filterable_annotations
  annotations.select(&:filterable?).reject(&:pattern?)
end

.icon(icon_name = nil) ⇒ Object



103
104
105
106
107
108
109
# File 'lib/archsight/resources/base.rb', line 103

def self.icon(icon_name = nil)
  if icon_name
    @icon = icon_name
  else
    @icon || "page" # default icon
  end
end

.include_annotations(*names) ⇒ Object

Include annotation modules by symbol name

Examples:

include_annotations :git, :architecture, :backup

Parameters:

  • names (Array<Symbol>)

    Symbols representing annotation modules (:git, :architecture, :backup, :generated)



130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/archsight/resources/base.rb', line 130

def self.include_annotations(*names)
  names.each do |name|
    # Convert snake_case to CamelCase (e.g., :git -> 'Git', :my_module -> 'MyModule')
    module_name = name.to_s.split("_").map(&:capitalize).join
    mod = Archsight::Annotations.const_get(module_name)
    include mod
  rescue NameError
    available = Archsight::Annotations.constants
                                      .select { |c| Archsight::Annotations.const_get(c).is_a?(Module) && Archsight::Annotations.const_get(c).respond_to?(:included) }
                                      .map { |c| ":#{c.to_s.gsub(/([a-z])([A-Z])/, '\1_\2').downcase}" }
                                      .sort
    Kernel.raise "Unknown annotation module :#{name}. Available: #{available.join(", ")}"
  end
end

.inherited(subclass) ⇒ Object



11
12
13
14
15
# File 'lib/archsight/resources/base.rb', line 11

def self.inherited(subclass)
  super
  # Auto-register when class is defined
  Archsight::Resources.register(subclass)
end

.layer(layer_name = nil) ⇒ Object



111
112
113
114
115
116
117
# File 'lib/archsight/resources/base.rb', line 111

def self.layer(layer_name = nil)
  if layer_name
    @layer = layer_name
  else
    @layer || "other" # default layer
  end
end

.list_annotationsObject

Get annotations marked for list display



87
88
89
# File 'lib/archsight/resources/base.rb', line 87

def self.list_annotations
  annotations.select(&:list_display?).reject(&:pattern?)
end

.matches_annotation_pattern?(key) ⇒ Boolean

Check if key matches any pattern annotation

Returns:

  • (Boolean)


77
78
79
# File 'lib/archsight/resources/base.rb', line 77

def self.matches_annotation_pattern?(key)
  annotations.any? { |a| a.pattern? && a.matches?(key) }
end

.relation(verb, kind, klass_name) ⇒ Object



17
18
19
20
# File 'lib/archsight/resources/base.rb', line 17

def self.relation(verb, kind, klass_name)
  @relations ||= [] #: Array[[Symbol, Symbol, String]]
  @relations << [verb, kind, klass_name]
end

.relationsObject



22
23
24
# File 'lib/archsight/resources/base.rb', line 22

def self.relations
  @relations || []
end

Instance Method Details

#abandoned?Boolean

Returns:

  • (Boolean)


210
211
212
# File 'lib/archsight/resources/base.rb', line 210

def abandoned?
  annotations["activity/status"] == "abandoned"
end

#annotationsObject



173
174
175
# File 'lib/archsight/resources/base.rb', line 173

def annotations
  ["annotations"] || {}
end

#computed_annotation_value(key) ⇒ Object?

Get a computed annotation value from the cache

Parameters:

  • key (String)

    The annotation key

Returns:

  • (Object, nil)

    The computed value or nil



194
195
196
# File 'lib/archsight/resources/base.rb', line 194

def computed_annotation_value(key)
  @computed_values&.[](key)
end

#has_relations?Boolean

Returns:

  • (Boolean)


214
215
216
# File 'lib/archsight/resources/base.rb', line 214

def has_relations?
  spec.any? { |_verb, kinds| kinds.is_a?(Hash) && kinds.values.any? { |v| v.is_a?(Array) && v.any? } }
end

#kindObject



165
166
167
# File 'lib/archsight/resources/base.rb', line 165

def kind
  @raw["kind"]
end

#klassObject



161
162
163
# File 'lib/archsight/resources/base.rb', line 161

def klass
  self.class.name.split("::").last
end

#merge!(inst) ⇒ Object



289
290
291
292
# File 'lib/archsight/resources/base.rb', line 289

def merge!(inst)
  # NOTE: path reference is preserved from the original instance
  @raw = Archsight::Helpers.deep_merge(@raw, inst.raw)
end

#metadataObject



198
199
200
# File 'lib/archsight/resources/base.rb', line 198

def 
  @raw["metadata"] || {}
end

#nameObject



169
170
171
# File 'lib/archsight/resources/base.rb', line 169

def name
  ["name"]
end

#raise(msg) ⇒ Object

raise provides a helper for better error messages including current path and line no



295
296
297
# File 'lib/archsight/resources/base.rb', line 295

def raise(msg)
  Kernel.raise(Archsight::ResourceError.new(msg, @path_ref))
end

#referenced_by(inst, verb = nil) ⇒ Object



235
236
237
238
239
# File 'lib/archsight/resources/base.rb', line 235

def referenced_by(inst, verb = nil)
  # Store reference with verb information for grouped display
  existing = @references.find { |r| r[:instance] == inst && r[:verb] == verb }
  @references << { instance: inst, verb: verb } unless existing
end

#references_groupedObject

Get references grouped by kind and verb for display (incoming) Returns: { “Kind” => { “verb” => [instances…] } }



243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/archsight/resources/base.rb', line 243

def references_grouped
  grouped = {} #: Hash[String, Hash[untyped, Array[Base]]]
  @references.each do |ref|
    inst = ref[:instance]
    verb = ref[:verb]
    kind = inst.klass
    grouped[kind] ||= {}
    grouped[kind][verb] ||= [] #: Array[Base]
    grouped[kind][verb] << inst
  end
  # Sort by kind name, then by verb name
  grouped.sort.to_h.transform_values { |verbs| verbs.sort.to_h }
end

#relations(verb, kind) ⇒ Object



226
227
228
# File 'lib/archsight/resources/base.rb', line 226

def relations(verb, kind)
  (spec[verb.to_s] || {})[kind.to_s] || []
end

#relations_groupedObject

Get outgoing relations grouped by verb and kind for display Returns: { “verb” => { “Kind” => [instances…] } }



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/archsight/resources/base.rb', line 259

def relations_grouped
  grouped = {} #: Hash[String, Hash[String, Array[Base]]]
  spec.each do |verb, kinds|
    next unless kinds.is_a?(Hash)

    kinds.each_value do |instances|
      next unless instances.is_a?(Array) && instances.any?

      instances.each do |inst|
        kind = inst.klass
        grouped[verb] ||= {}
        grouped[verb][kind] ||= [] #: Array[Base]
        grouped[verb][kind] << inst
      end
    end
  end
  # Sort by verb name, then by kind name
  grouped.sort.to_h.transform_values { |kinds| kinds.sort.to_h }
end

#set_computed_annotation(key, value) ⇒ Object

Set a computed annotation value This writes to both the computed values cache and the annotations hash so computed values are accessible via the normal annotations interface.

Parameters:

  • key (String)

    The annotation key

  • value (Object)

    The computed value



182
183
184
185
186
187
188
189
# File 'lib/archsight/resources/base.rb', line 182

def set_computed_annotation(key, value)
  @computed_values ||= {} #: Hash[String, untyped]
  @computed_values[key] = value
  # Write to annotations hash for query compatibility
  @raw["metadata"] ||= {}
  @raw["metadata"]["annotations"] = @raw["metadata"]["annotations"] || {} #: Hash[String, String]
  @raw["metadata"]["annotations"][key] = value
end

#set_relations(verb, kind, rels) ⇒ Object



230
231
232
233
# File 'lib/archsight/resources/base.rb', line 230

def set_relations(verb, kind, rels)
  spec[verb.to_s][kind.to_s] = rels
  rels.each { |rel| rel.referenced_by(self, verb) }
end

#specObject



202
203
204
# File 'lib/archsight/resources/base.rb', line 202

def spec
  @raw["spec"] || {}
end

#to_sObject



206
207
208
# File 'lib/archsight/resources/base.rb', line 206

def to_s
  "#<#{self.class} name=#{name}>"
end

#verb_allowed?(verb) ⇒ Boolean

Returns:

  • (Boolean)


218
219
220
# File 'lib/archsight/resources/base.rb', line 218

def verb_allowed?(verb)
  self.class.relations.any? { |v, _, _| v.to_s == verb.to_s }
end

#verb_kind_allowed?(verb, kind) ⇒ Boolean

Returns:

  • (Boolean)


222
223
224
# File 'lib/archsight/resources/base.rb', line 222

def verb_kind_allowed?(verb, kind)
  self.class.relations.any? { |v, k, _| v.to_s == verb.to_s && k.to_s == kind.to_s }
end

#verify!Object



279
280
281
282
283
284
285
286
287
# File 'lib/archsight/resources/base.rb', line 279

def verify!
  spec.each do |verb, kinds|
    raise "unknown verb #{verb}" unless verb_allowed?(verb)

    kinds.each_key do |kind, _|
      raise "unknown verb #{verb} / kind #{kind} combination" unless verb_kind_allowed?(verb, kind)
    end
  end
end