Class: Diagrams::Base

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

Overview

Abstract base class for all diagram types. Provides common functionality like versioning, checksum calculation, serialization, and equality comparison.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(version: 1) ⇒ Base

Initializes the base diagram attributes. Subclasses should call super.

Raises:

  • (NotImplementedError)


18
19
20
21
22
23
24
# File 'lib/diagrams/base.rb', line 18

def initialize(version: 1)
  # Prevent direct instantiation of the base class
  raise NotImplementedError, 'Cannot instantiate abstract class Diagrams::Base' if instance_of?(Diagrams::Base)

  @version = version
  @checksum = nil # Will be calculated by subclasses via #update_checksum! after content is set
end

Instance Attribute Details

#checksumObject (readonly)

Returns the value of attribute checksum.



12
13
14
# File 'lib/diagrams/base.rb', line 12

def checksum
  @checksum
end

#versionObject (readonly)

Returns the value of attribute version.



12
13
14
# File 'lib/diagrams/base.rb', line 12

def version
  @version
end

Class Method Details

.from_hash(hash) ⇒ Diagrams::Base

Deserializes a diagram from a hash representation. Acts as a factory, dispatching to the appropriate subclass based on the ‘type’ field.

Raises:

  • (ArgumentError)

    if the hash is missing the ‘type’ key.

  • (NameError)

    if the type string doesn’t correspond to a known Diagram class.

  • (TypeError)

    if the resolved class is not a subclass of Diagrams::Base.



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/diagrams/base.rb', line 153

def from_hash(hash)
  # Ensure keys are symbols for consistent access
  symbolized_hash = hash.transform_keys(&:to_sym)

  type_string = symbolized_hash[:type]
  raise ArgumentError, "Input hash must include a 'type' key." unless type_string

  data_hash = symbolized_hash[:data] || {}
  version = symbolized_hash[:version]
  checksum = symbolized_hash[:checksum] # Pass checksum for potential verification

  begin
    # Convert snake_case type string back to CamelCase class name part
    camel_case_type = snake_to_camel_case(type_string)
    # Construct full class name (e.g., "Diagrams::FlowchartDiagram")
    klass_name = "Diagrams::#{camel_case_type}"
    klass = Object.const_get(klass_name)
  rescue NameError
    raise NameError, "Unknown diagram type '#{type_string}' corresponding to class '#{klass_name}'"
  end

  # Ensure the resolved class is actually a diagram type
  raise TypeError, "'#{klass_name}' is not a valid subclass of Diagrams::Base" unless klass < Diagrams::Base

  # Delegate to the specific subclass's from_h method
  # Each subclass must implement `from_h(data_hash, version:, checksum:)`
  klass.from_h(data_hash, version:, checksum:)
end

.from_json(json_string) ⇒ Diagrams::Base

Deserializes a diagram from a JSON string. Parses the JSON and delegates to ‘.from_hash`.



187
188
189
190
191
192
# File 'lib/diagrams/base.rb', line 187

def from_json(json_string)
  hash = JSON.parse(json_string)
  from_hash(hash)
rescue JSON::ParserError => e
  raise JSON::ParserError, "Failed to parse JSON: #{e.message}"
end

Instance Method Details

#diff(other) ⇒ Hash{Symbol => Hash{Symbol => Array<Diagrams::Elements::*>}}

Performs a basic diff against another diagram object. Only compares diagrams of the same type. Identifies added and removed elements based on common identifiers (id/name) or object equality. Does NOT currently detect modified elements.



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/diagrams/base.rb', line 53

def diff(other)
  diff_result = {}
  return diff_result unless other.is_a?(self.class) # Only compare same types
  return diff_result if self == other # Use existing equality check for quick exit

  self_elements = identifiable_elements
  other_elements = other.identifiable_elements

  # Ensure both diagrams define the same element types for comparison
  element_types = self_elements.keys & other_elements.keys

  element_types.each do |type|
    self_collection = self_elements[type] || []
    other_collection = other_elements[type] || []

    # Determine identifier method (prefer id, then name, then title, then label, fallback to object itself)
    identifier_method = if self_collection.first.respond_to?(:id)
                          :id
                        elsif self_collection.first.respond_to?(:name)
                          :name
                        elsif self_collection.first.respond_to?(:title) # For TimelineSection
                          :title
                        elsif self_collection.first.respond_to?(:label) # For Slice, TimelinePeriod
                          :label
                        else
                          :itself # Fallback to object identity/equality
                        end

    self_ids = self_collection.map(&identifier_method)
    other_ids = other_collection.map(&identifier_method)

    added_ids = other_ids - self_ids
    removed_ids = self_ids - other_ids

    added_elements = other_collection.select { |el| added_ids.include?(el.send(identifier_method)) }
    removed_elements = self_collection.select { |el| removed_ids.include?(el.send(identifier_method)) }

    # Basic check for modified elements (same ID, different content via checksum/hash if available, or simple !=)
    # This is a very basic modification check
    potential_modified_ids = self_ids & other_ids
    modified_elements = []
    potential_modified_ids.each do |id|
      self_el = self_collection.find { |el| el.send(identifier_method) == id }
      other_el = other_collection.find { |el| el.send(identifier_method) == id }
      # Use Dry::Struct equality if available, otherwise basic !=
      next unless self_el != other_el

      modified_elements << { old: self_el, new: other_el }
      # Remove from added/removed if detected as modified
      added_elements.delete(other_el)
      removed_elements.delete(self_el)
    end

    type_diff = {}
    type_diff[:added] = added_elements if added_elements.any?
    type_diff[:removed] = removed_elements if removed_elements.any?
    type_diff[:modified] = modified_elements if modified_elements.any? # Add modified info

    diff_result[type] = type_diff if type_diff.any?
  end

  diff_result
end

#identifiable_elementsHash{Symbol => Array<Diagrams::Elements::*>}

Abstract method: Subclasses must implement this to return a hash mapping element type symbols (e.g., :nodes, :edges) to arrays of the corresponding element objects within the diagram. Used for comparison and diffing.

Raises:

  • (NotImplementedError)


40
41
42
# File 'lib/diagrams/base.rb', line 40

def identifiable_elements
  raise NotImplementedError, "#{self.class.name} must implement #identifiable_elements"
end

#to_hHash

Returns a hash representation of the diagram, suitable for serialization. Includes common metadata and calls ‘#to_h_content` for specific data.



121
122
123
124
125
126
127
128
129
130
# File 'lib/diagrams/base.rb', line 121

def to_h
  {
    # Extract class name without module prefix (e.g., "FlowchartDiagram")
    # Convert class name to snake_case (e.g., FlowchartDiagram -> flowchart_diagram)
    type: camel_to_snake_case(self.class.name.split('::').last),
    version: @version,
    checksum: @checksum, # Ensure checksum is up-to-date before calling
    data: to_h_content
  }
end

#to_h_contentHash

Abstract method: Subclasses must implement this to return a hash representing their specific content, suitable for serialization.

Raises:

  • (NotImplementedError)


30
31
32
# File 'lib/diagrams/base.rb', line 30

def to_h_content
  raise NotImplementedError, "#{self.class.name} must implement #to_h_content"
end

#to_jsonString

Returns a JSON string representation of the diagram. Delegates to ‘#to_h` and uses `JSON.generate`. Accepts any arguments valid for `JSON.generate`.



138
139
140
# File 'lib/diagrams/base.rb', line 138

def to_json(*)
  JSON.generate(to_h, *)
end