diagram 0.3.2 → 0.3.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b6a7329e68d37cab7fe076fb763b56fddff42b23e81619bd70f3a89ef82ebff8
4
- data.tar.gz: 3eac3513c7ebb76307967f12f3c9d189e35022203ce817e3fbc90be310ac7b10
3
+ metadata.gz: c99defb74ddced6d50a0dd2bd5cf61b90ce2edab9192c5f119586442eb625d12
4
+ data.tar.gz: 1bd666eed832e6052f789c3a37fd634d2cb4c198468281bafc17bc1ca500d05a
5
5
  SHA512:
6
- metadata.gz: 0b59a02a496318c080f8ff72bbfb6bb224ae4dfeb3657eda3e262ac401cca6871f8a18a487fbe012699c7016ae8ef07cfc565f224e7697cc3a6aba9abb60907b
7
- data.tar.gz: 6cee2aa87b44ae4aab58a99fcf9cfadfbde2f9496523d7e02e5dcb00f6c4ec14abce8b36ff79c1b276e44eec407fe09db6dcaabd9a14c83a050b7684df854f41
6
+ metadata.gz: f9d0b5269c5093821a09140273590af586dc35e4a21b9aed5c45a5201db8ebcfdb769e3764038fe0a74e467eea3d18e1f1fc970c295e69fefbdc686230106b48
7
+ data.tar.gz: 7a53a67aecab0fa28adee1e6f826114ad0f0184e994e31c346d9b3224a4fd72e255ed4eaea714327c467664cef766ae2da1cbded21c58d620d7cb5862c65617d
data/lib/diagrams/base.rb CHANGED
@@ -66,13 +66,13 @@ module Diagrams
66
66
  other_collection = other_elements[type] || []
67
67
 
68
68
  # Determine identifier method (prefer id, then name, then title, then label, fallback to object itself)
69
- identifier_method = if self_collection.first&.respond_to?(:id)
69
+ identifier_method = if self_collection.first.respond_to?(:id)
70
70
  :id
71
- elsif self_collection.first&.respond_to?(:name)
71
+ elsif self_collection.first.respond_to?(:name)
72
72
  :name
73
- elsif self_collection.first&.respond_to?(:title) # For TimelineSection
73
+ elsif self_collection.first.respond_to?(:title) # For TimelineSection
74
74
  :title
75
- elsif self_collection.first&.respond_to?(:label) # For Slice, TimelinePeriod
75
+ elsif self_collection.first.respond_to?(:label) # For Slice, TimelinePeriod
76
76
  :label
77
77
  else
78
78
  :itself # Fallback to object identity/equality
@@ -196,6 +196,10 @@ module Diagrams
196
196
  # Simple helper to convert snake_case to CamelCase
197
197
  # (Avoids ActiveSupport dependency)
198
198
  def snake_to_camel_case(string)
199
+ # Handle specific acronyms first
200
+ return 'ERDiagram' if string == 'er_diagram'
201
+
202
+ # Default conversion
199
203
  string.split('_').collect(&:capitalize).join
200
204
  end
201
205
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diagrams
4
+ module Elements
5
+ # Represents an attribute within an ERD entity.
6
+ class ERDAttribute < Dry::Struct
7
+ include Elements::Types
8
+
9
+ attribute :type, Types::Strict::String.constrained(min_size: 1)
10
+ attribute :name, Types::Strict::String.constrained(min_size: 1)
11
+ attribute :keys, Types::Strict::Array.of(Types::Strict::Symbol.enum(:PK, :FK, :UK)).default([].freeze)
12
+ attribute :comment, Types::Strict::String.optional.default(nil)
13
+
14
+ # Returns a hash representation suitable for serialization.
15
+ #
16
+ # @return [Hash{Symbol => String | Array<Symbol>}]
17
+ def to_h
18
+ hash = {
19
+ type:,
20
+ name:,
21
+ keys: keys.map(&:to_s) # Convert symbols to strings for serialization
22
+ }
23
+ hash[:comment] = comment if comment
24
+ hash
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diagrams
4
+ module Elements
5
+ # Represents an entity (table) in an ER Diagram.
6
+ class ERDEntity < Dry::Struct
7
+ include Elements::Types
8
+
9
+ attribute :name, Types::Strict::String.constrained(min_size: 1)
10
+ attribute :entity_attributes, Types::Strict::Array.of(ERDAttribute).default([].freeze)
11
+
12
+ # Returns a hash representation suitable for serialization.
13
+ #
14
+ # @return [Hash{Symbol => String | Array<Hash>}]
15
+ def to_h
16
+ {
17
+ name: name,
18
+ attributes: entity_attributes.map(&:to_h) # Renamed variable
19
+ }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diagrams
4
+ module Elements
5
+ # Represents a relationship between two entities in an ER Diagram.
6
+ class ERDRelationship < Dry::Struct
7
+ include Elements::Types
8
+
9
+ # Cardinality symbols (Crow's Foot notation mapping)
10
+ CARDINALITY = Types::Strict::Symbol.enum(
11
+ :ZERO_OR_ONE, # |o
12
+ :ONE_ONLY, # ||
13
+ :ZERO_OR_MORE, # }o
14
+ :ONE_OR_MORE # }|
15
+ )
16
+
17
+ attribute :entity1, Types::Strict::String.constrained(min_size: 1)
18
+ attribute :entity2, Types::Strict::String.constrained(min_size: 1)
19
+ attribute :cardinality1, CARDINALITY # Cardinality of entity1 relative to entity2
20
+ attribute :cardinality2, CARDINALITY # Cardinality of entity2 relative to entity1
21
+ attribute :identifying, Types::Strict::Bool.default(false) # Is it an identifying relationship? (solid vs dashed line)
22
+ attribute :label, Types::Strict::String.optional.default(nil) # Optional action/verb phrase
23
+
24
+ # Returns a hash representation suitable for serialization.
25
+ #
26
+ # @return [Hash{Symbol => String | Symbol | Bool}]
27
+ def to_h
28
+ hash = {
29
+ entity1: entity1,
30
+ entity2: entity2,
31
+ cardinality1: cardinality1.to_s, # Convert symbol to string
32
+ cardinality2: cardinality2.to_s, # Convert symbol to string
33
+ identifying: identifying
34
+ }
35
+ hash[:label] = label if label
36
+ hash
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diagrams
4
+ module Elements
5
+ # Represents a section within a Gantt chart, grouping multiple tasks.
6
+ class GanttSection < Dry::Struct
7
+ include Elements::Types
8
+
9
+ attribute :title, Types::Strict::String.constrained(min_size: 1)
10
+ attribute :tasks, Types::Strict::Array.of(Task).default([].freeze)
11
+
12
+ # Returns a hash representation suitable for serialization.
13
+ #
14
+ # @return [Hash{Symbol => String | Array<Hash>}]
15
+ def to_h
16
+ {
17
+ title:,
18
+ tasks: tasks.map(&:to_h)
19
+ }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -7,17 +7,29 @@ module Diagrams
7
7
  # Use the shared Types module
8
8
  include Elements::Types
9
9
 
10
- attribute :id, Types::Strict::String.constrained(min_size: 1)
11
- attribute :name, Types::Strict::String.constrained(min_size: 1)
12
- # Using String for dates initially for simplicity.
13
- # Consider Types::Strict::Date or custom coercible types later.
14
- attribute :start_date, Types::Strict::String.constrained(min_size: 1) # Basic check
15
- attribute :end_date, Types::Strict::String.constrained(min_size: 1) # Basic check
16
- # TODO: Add dependencies attribute (e.g., Types::Strict::Array.of(Types::Strict::String))
10
+ # Status symbols allowed by Mermaid Gantt
11
+ STATUS = Types::Strict::Symbol.enum(:done, :active, :crit)
12
+
13
+ # Attributes
14
+ attribute :id, Types::Strict::String.constrained(min_size: 1) # Unique ID for dependencies
15
+ attribute :label, Types::Strict::String.constrained(min_size: 1) # Display name
16
+ attribute :status, STATUS.optional.default(nil) # Task status (nil implies default/future)
17
+ attribute :start, Types::Strict::String.constrained(min_size: 1) # Start date, task ID, or 'after taskX[, taskY]'
18
+ attribute :duration, Types::Strict::String.constrained(min_size: 1) # Duration string (e.g., '7d', '2w')
17
19
 
18
20
  # Returns a hash representation suitable for serialization.
19
21
  #
20
- # @return [Hash{Symbol => String}]
22
+ # @return [Hash{Symbol => String | Symbol | nil}]
23
+ def to_h
24
+ hash = {
25
+ id: id,
26
+ label: label,
27
+ start: start,
28
+ duration: duration
29
+ }
30
+ hash[:status] = status if status # Include status only if set
31
+ hash
32
+ end
21
33
  end
22
34
  end
23
35
  end
@@ -18,4 +18,4 @@ module Diagrams
18
18
  end
19
19
  end
20
20
  end
21
- end
21
+ end
@@ -20,4 +20,4 @@ module Diagrams
20
20
  end
21
21
  end
22
22
  end
23
- end
23
+ end
@@ -20,4 +20,4 @@ module Diagrams
20
20
  end
21
21
  end
22
22
  end
23
- end
23
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diagrams
4
+ # Represents an Entity Relationship Diagram (ERD).
5
+ class ERDiagram < Base
6
+ attr_reader :entities, :relationships
7
+
8
+ # Initializes a new ERDiagram.
9
+ #
10
+ # @param entities [Array<Element::ERDEntity>] Initial entities (optional).
11
+ # @param relationships [Array<Element::ERDRelationship>] Initial relationships (optional).
12
+ # @param version [String, Integer, nil] User-defined version identifier.
13
+ def initialize(entities: [], relationships: [], version: 1)
14
+ super(version:)
15
+ @entities = (entities.is_a?(Array) ? entities : []).each_with_object({}) { |e, h| h[e.name] = e }
16
+ @relationships = relationships.is_a?(Array) ? relationships : []
17
+ validate_relationships!
18
+ update_checksum!
19
+ end
20
+
21
+ # Adds an entity to the diagram.
22
+ #
23
+ # @param name [String] The unique name of the entity.
24
+ # @param attributes [Array<Hash>] Array of attribute definitions (hashes like { type:, name:, keys:, comment: }).
25
+ # @raise [ArgumentError] if an entity with the same name already exists.
26
+ # @return [Elements::ERDEntity] The added entity.
27
+ def add_entity(name:, attributes: [])
28
+ raise ArgumentError, "Entity name '#{name}' cannot be empty" if name.nil? || name.strip.empty?
29
+ raise ArgumentError, "Entity with name '#{name}' already exists" if @entities.key?(name)
30
+
31
+ entity_attributes = attributes.map do |attr_hash|
32
+ Elements::ERDAttribute.new(attr_hash.transform_keys(&:to_sym))
33
+ end
34
+ new_entity = Elements::ERDEntity.new(name:, entity_attributes: entity_attributes) # Use renamed attribute
35
+
36
+ @entities[name] = new_entity
37
+ update_checksum!
38
+ new_entity
39
+ end
40
+
41
+ # Adds a relationship between two entities.
42
+ #
43
+ # @param entity1 [String] Name of the first entity.
44
+ # @param entity2 [String] Name of the second entity.
45
+ # @param cardinality1 [Symbol] Cardinality of entity1 relative to entity2 (e.g., :ONE_ONLY).
46
+ # @param cardinality2 [Symbol] Cardinality of entity2 relative to entity1 (e.g., :ZERO_OR_MORE).
47
+ # @param identifying [Boolean] Whether the relationship is identifying (default: false).
48
+ # @param label [String, nil] Optional label describing the relationship action.
49
+ # @raise [ArgumentError] if either entity does not exist.
50
+ # @return [Elements::ERDRelationship] The added relationship.
51
+ def add_relationship(entity1:, entity2:, cardinality1:, cardinality2:, identifying: false, label: nil)
52
+ unless @entities.key?(entity1) && @entities.key?(entity2)
53
+ raise ArgumentError, "One or both entities ('#{entity1}', '#{entity2}') not found for relationship."
54
+ end
55
+
56
+ new_relationship = Elements::ERDRelationship.new(
57
+ entity1:,
58
+ entity2:,
59
+ cardinality1:,
60
+ cardinality2:,
61
+ identifying:,
62
+ label:
63
+ )
64
+ @relationships << new_relationship
65
+ update_checksum!
66
+ new_relationship
67
+ end
68
+
69
+ # Finds an entity by its name.
70
+ #
71
+ # @param entity_name [String] The name of the entity to find.
72
+ # @return [Elements::ERDEntity, nil] The found entity or nil.
73
+ def find_entity(entity_name)
74
+ @entities[entity_name]
75
+ end
76
+
77
+ # --- Base Class Implementation ---
78
+
79
+ def to_h_content
80
+ {
81
+ entities: @entities.values.map(&:to_h),
82
+ relationships: @relationships.map(&:to_h)
83
+ }
84
+ end
85
+
86
+ def identifiable_elements
87
+ {
88
+ entities: @entities.values,
89
+ relationships: @relationships # Relationships don't have a simple unique ID, rely on object equality for diff
90
+ }
91
+ end
92
+
93
+ def self.from_h(data_hash, version:, checksum:)
94
+ entities_data = data_hash[:entities] || data_hash['entities'] || []
95
+ relationships_data = data_hash[:relationships] || data_hash['relationships'] || []
96
+
97
+ entities = entities_data.map do |entity_h|
98
+ entity_data = entity_h.transform_keys(&:to_sym)
99
+ attributes_data = entity_data[:entity_attributes] || entity_data[:attributes] || [] # Accept both old and new key for now
100
+ attributes = attributes_data.map do |attr_h|
101
+ attr_data = attr_h.transform_keys(&:to_sym)
102
+ # Convert keys back to symbols if they are strings
103
+ # Convert keys back to symbols
104
+ attr_data[:keys] = attr_data[:keys].map(&:to_sym) if attr_data[:keys].is_a?(Array)
105
+ Elements::ERDAttribute.new(attr_data)
106
+ end
107
+ # Use the correct attribute name when creating the entity
108
+ Elements::ERDEntity.new(entity_data.merge(entity_attributes: attributes))
109
+ end
110
+
111
+ relationships = relationships_data.map do |rel_h|
112
+ rel_data = rel_h.transform_keys(&:to_sym)
113
+ # Convert cardinalities back to symbols if they are strings
114
+ rel_data[:cardinality1] = rel_data[:cardinality1].to_sym if rel_data[:cardinality1].is_a?(String)
115
+ rel_data[:cardinality2] = rel_data[:cardinality2].to_sym if rel_data[:cardinality2].is_a?(String)
116
+ Elements::ERDRelationship.new(rel_data)
117
+ end
118
+
119
+ diagram = new(entities:, relationships:, version:)
120
+
121
+ # Optional: Verify checksum
122
+ if checksum && diagram.checksum != checksum
123
+ warn "Checksum mismatch for loaded ERDiagram (version: #{version}). Expected #{checksum}, got #{diagram.checksum}."
124
+ end
125
+
126
+ diagram
127
+ end
128
+
129
+ private
130
+
131
+ # Validates that all relationships refer to existing entities during initialization.
132
+ def validate_relationships!
133
+ @relationships.each do |rel|
134
+ unless @entities.key?(rel.entity1) && @entities.key?(rel.entity2)
135
+ raise ArgumentError, "Relationship refers to non-existent entity IDs ('#{rel.entity1}' or '#{rel.entity2}')"
136
+ end
137
+ end
138
+ end
139
+
140
+ # Protected method access
141
+ protected :update_checksum!
142
+ end
143
+ end
@@ -1,84 +1,156 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Diagrams
4
- # Represents a Gantt Chart diagram consisting of tasks over time.
4
+ # Represents a Gantt Chart diagram consisting of tasks over time, grouped into sections.
5
5
  class GanttDiagram < Base
6
- attr_reader :title, :tasks
6
+ DEFAULT_SECTION_TITLE = 'Default Section'
7
+
8
+ attr_reader :title, :sections
7
9
 
8
10
  # Initializes a new GanttDiagram.
9
11
  #
10
12
  # @param title [String] The title of the Gantt chart.
11
- # @param tasks [Array<Element::Task>] An array of task objects.
13
+ # @param sections [Array<Element::GanttSection>] An array of section objects (containing tasks).
12
14
  # @param version [String, Integer, nil] User-defined version identifier.
13
- def initialize(title: '', tasks: [], version: 1)
15
+ def initialize(title: '', sections: [], version: 1)
14
16
  super(version:)
15
17
  @title = title.is_a?(String) ? title : ''
16
- @tasks = tasks.is_a?(Array) ? tasks : []
18
+ @sections = sections.is_a?(Array) ? sections : []
19
+ ensure_default_section if @sections.empty?
17
20
  validate_elements!
18
21
  update_checksum!
19
22
  end
20
23
 
21
- # Adds a task to the diagram.
24
+ # Adds a new section to the diagram.
25
+ # Subsequent tasks will be added to this section.
22
26
  #
23
- # @param task [Element::Task] The task object to add.
24
- # @raise [ArgumentError] if a task with the same ID already exists.
25
- # @return [Element::Task] The added task.
26
- def add_task(task)
27
- raise ArgumentError, 'Task must be a Diagrams::Elements::Task' unless task.is_a?(Diagrams::Elements::Task)
28
- raise ArgumentError, "Task with ID '#{task.id}' already exists" if find_task(task.id)
29
-
30
- @tasks << task
27
+ # @param section_title [String] The title of the section.
28
+ # @raise [ArgumentError] if a section with the same title already exists.
29
+ # @return [Elements::GanttSection] The newly added section.
30
+ def add_section(section_title)
31
+ clean_title = section_title.strip
32
+ raise ArgumentError, "Section title '#{clean_title}' cannot be empty" if clean_title.empty?
33
+ raise ArgumentError, "Section with title '#{clean_title}' already exists" if find_section(clean_title)
34
+
35
+ # Remove default section if it's empty and we're adding a real one
36
+ if @sections.size == 1 && @sections.first.title == DEFAULT_SECTION_TITLE && @sections.first.tasks.empty? # Check tasks for GanttSection
37
+ @sections.clear
38
+ end
39
+
40
+ # Use GanttSection
41
+ new_section = Elements::GanttSection.new(title: clean_title, tasks: [])
42
+ @sections << new_section
31
43
  update_checksum!
32
- task
44
+ new_section
33
45
  end
34
46
 
35
- # Finds a task by its ID.
47
+ # Adds a task to the current (last) section of the diagram.
48
+ #
49
+ # @param id [String] Unique ID for the task (used for dependencies).
50
+ # @param label [String] Display name/label for the task.
51
+ # @param status [Symbol, nil] Status (:done, :active, :crit). nil implies default/future.
52
+ # @param start [String] Start date, task ID (e.g., 'task1'), or dependency string ('after taskX').
53
+ # @param duration [String] Duration string (e.g., '7d', '2w').
54
+ # @raise [ArgumentError] if required fields are missing or a task with the same ID exists.
55
+ # @raise [StandardError] if no sections exist.
56
+ # @return [Elements::Task] The added task.
57
+ def add_task(id:, label:, start:, duration:, status: nil)
58
+ raise ArgumentError, 'Task ID cannot be empty' if id.nil? || id.strip.empty?
59
+ raise ArgumentError, "Task with ID '#{id}' already exists" if find_task(id)
60
+
61
+ new_task = Elements::Task.new(
62
+ id:,
63
+ label:,
64
+ status:,
65
+ start:,
66
+ duration:
67
+ )
68
+
69
+ current_section = @sections.last
70
+ raise StandardError, 'Cannot add task: No section available.' unless current_section
71
+
72
+ # Add task to the current section's 'tasks' array
73
+ updated_tasks = current_section.tasks + [new_task]
74
+ updated_section = Elements::GanttSection.new(title: current_section.title, tasks: updated_tasks)
75
+
76
+ # Update the section in the main array
77
+ current_section_index = @sections.index { |s| s.title == current_section.title }
78
+ unless current_section_index
79
+ raise StandardError,
80
+ "Could not find index for current section '#{current_section.title}'"
81
+ end
82
+
83
+ @sections[current_section_index] = updated_section
84
+
85
+ update_checksum!
86
+ new_task
87
+ end
88
+
89
+ # Finds a task by its ID across all sections.
36
90
  #
37
91
  # @param task_id [String] The ID of the task to find.
38
92
  # @return [Element::Task, nil] The found task or nil.
39
93
  def find_task(task_id)
40
- @tasks.find { |t| t.id == task_id }
94
+ all_tasks.find { |t| t.id == task_id }
95
+ end
96
+
97
+ # Finds a section by its title.
98
+ # @param section_title [String] The title of the section.
99
+ # @return [Elements::GanttSection, nil] The found section or nil.
100
+ def find_section(section_title)
101
+ @sections.find { |s| s.title == section_title }
41
102
  end
42
103
 
43
104
  # Returns the specific content of the Gantt diagram as a hash.
44
- # Called by `Diagrams::Base#to_h`.
45
105
  #
46
106
  # @return [Hash{Symbol => String | Array<Hash>}]
47
107
  def to_h_content
48
108
  {
49
109
  title: @title,
50
- tasks: @tasks.map(&:to_h)
110
+ # Serialize sections, renaming 'periods' back to 'tasks' for clarity
111
+ sections: @sections.map(&:to_h) # Use GanttSection's to_h directly
51
112
  }
52
113
  end
53
114
 
54
115
  # Returns a hash mapping element types to their collections for diffing.
55
- # @see Diagrams::Base#identifiable_elements
116
+ #
56
117
  # @return [Hash{Symbol => Array<Diagrams::Elements::Task>}]
57
118
  def identifiable_elements
58
119
  {
59
- tasks: @tasks
120
+ # Diffing based on tasks directly might be more useful than sections here
121
+ tasks: all_tasks
122
+ # sections: @sections # Could also diff sections if needed
60
123
  }
61
124
  end
62
125
 
63
126
  # Class method to create a GanttDiagram from a hash.
64
- # Used by the deserialization factory in `Diagrams::Base`.
65
127
  #
66
- # @param data_hash [Hash] Hash containing `:title` and `:tasks` array.
128
+ # @param data_hash [Hash] Hash containing `:title` and `:sections` array.
67
129
  # @param version [String, Integer, nil] Diagram version.
68
130
  # @param checksum [String, nil] Expected checksum (optional, for verification).
69
131
  # @return [GanttDiagram] The instantiated diagram.
70
132
  def self.from_h(data_hash, version:, checksum:)
71
133
  title = data_hash[:title] || data_hash['title'] || ''
72
- tasks_data = data_hash[:tasks] || data_hash['tasks'] || []
73
-
74
- tasks = tasks_data.map { |task_h| Diagrams::Elements::Task.new(task_h.transform_keys(&:to_sym)) }
134
+ sections_data = data_hash[:sections] || data_hash['sections'] || []
135
+
136
+ sections = sections_data.map do |section_h|
137
+ section_data = section_h.transform_keys(&:to_sym)
138
+ tasks_data = section_data[:tasks] || [] # Expect 'tasks' key in hash
139
+ # Map task data to Task objects
140
+ tasks = tasks_data.map do |task_h|
141
+ task_data = task_h.transform_keys(&:to_sym)
142
+ # Convert status back to symbol if it's a string and present
143
+ task_data[:status] = task_data[:status].to_sym if task_data[:status].is_a?(String)
144
+ Elements::Task.new(task_data)
145
+ end
146
+ Elements::GanttSection.new(title: section_data[:title], tasks: tasks)
147
+ end
75
148
 
76
- diagram = new(title:, tasks:, version:)
149
+ diagram = new(title:, sections:, version:)
77
150
 
78
- # Optional: Verify checksum if provided
151
+ # Optional: Verify checksum
79
152
  if checksum && diagram.checksum != checksum
80
153
  warn "Checksum mismatch for loaded GanttDiagram (version: #{version}). Expected #{checksum}, got #{diagram.checksum}."
81
- # Or raise an error: raise "Checksum mismatch..."
82
154
  end
83
155
 
84
156
  diagram
@@ -86,14 +158,28 @@ module Diagrams
86
158
 
87
159
  private
88
160
 
161
+ # Helper to get all tasks from all sections.
162
+ def all_tasks
163
+ @sections.flat_map(&:tasks)
164
+ end
165
+
166
+ # Ensures a default section exists if the sections array is empty.
167
+ def ensure_default_section
168
+ return if @sections.any? { |s| s.title == DEFAULT_SECTION_TITLE }
169
+
170
+ @sections << Elements::GanttSection.new(title: DEFAULT_SECTION_TITLE, tasks: [])
171
+ end
172
+
89
173
  # Validates the consistency of tasks during initialization.
90
174
  def validate_elements!
91
- task_ids = @tasks.map(&:id)
92
- return if task_ids.uniq.size == @tasks.size
175
+ task_ids = all_tasks.map(&:id)
176
+ return if task_ids.uniq.size == all_tasks.size
93
177
 
94
178
  raise ArgumentError, 'Duplicate task IDs found'
95
-
96
179
  # Add more validation if needed (e.g., date formats, dependencies)
97
180
  end
181
+
182
+ # Protected method access
183
+ protected :update_checksum!
98
184
  end
99
185
  end
@@ -40,9 +40,7 @@ module Diagrams
40
40
  def add_section(section_title)
41
41
  clean_title = section_title.strip
42
42
  raise ArgumentError, "Section title '#{clean_title}' cannot be empty" if clean_title.empty?
43
- if find_section(clean_title)
44
- raise ArgumentError, "Section with title '#{clean_title}' already exists"
45
- end
43
+ raise ArgumentError, "Section with title '#{clean_title}' already exists" if find_section(clean_title)
46
44
 
47
45
  # Remove default section if it's empty and we're adding a real one
48
46
  if @sections.size == 1 && @sections.first.title == DEFAULT_SECTION_TITLE && @sections.first.periods.empty?
@@ -64,16 +62,16 @@ module Diagrams
64
62
  # @return [Elements::TimelinePeriod] The newly added period.
65
63
  def add_period(period_label:, events:)
66
64
  clean_label = period_label.strip
67
- raise ArgumentError, "Period label cannot be empty" if clean_label.empty?
65
+ raise ArgumentError, 'Period label cannot be empty' if clean_label.empty?
68
66
 
69
67
  event_list = Array(events).map(&:strip).reject(&:empty?)
70
- raise ArgumentError, "Events cannot be empty" if event_list.empty?
68
+ raise ArgumentError, 'Events cannot be empty' if event_list.empty?
71
69
 
72
70
  timeline_events = event_list.map { |desc| Elements::TimelineEvent.new(description: desc) }
73
71
  new_period = Elements::TimelinePeriod.new(label: clean_label, events: timeline_events)
74
72
 
75
73
  current_section = @sections.last
76
- raise StandardError, "Cannot add period: No section available." unless current_section
74
+ raise StandardError, 'Cannot add period: No section available.' unless current_section
77
75
 
78
76
  # Add period to the current section's periods array
79
77
  # Dry::Struct arrays are immutable, so we need to create a new section object
@@ -145,9 +143,9 @@ module Diagrams
145
143
 
146
144
  # Ensures a default section exists if the sections array is empty.
147
145
  def ensure_default_section
148
- unless @sections.any? { |s| s.title == DEFAULT_SECTION_TITLE }
149
- @sections << Elements::TimelineSection.new(title: DEFAULT_SECTION_TITLE)
150
- end
146
+ return if @sections.any? { |s| s.title == DEFAULT_SECTION_TITLE }
147
+
148
+ @sections << Elements::TimelineSection.new(title: DEFAULT_SECTION_TITLE)
151
149
  end
152
150
 
153
151
  # Finds a section by its title.
@@ -158,4 +156,4 @@ module Diagrams
158
156
  # Protected method access for from_h
159
157
  protected :update_checksum!
160
158
  end
161
- end
159
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Diagrams
4
- VERSION = '0.3.2'
4
+ VERSION = '0.3.3'
5
5
  end
data/lib/diagrams.rb CHANGED
@@ -9,6 +9,13 @@ require_relative 'diagrams/version'
9
9
 
10
10
  loader = Zeitwerk::Loader.for_gem
11
11
  loader.ignore("#{__dir__}/diagram.rb")
12
+ # Add inflection for ERDiagram
13
+ loader.inflector.inflect(
14
+ 'er_diagram' => 'ERDiagram',
15
+ 'erd_entity' => 'ERDEntity',
16
+ 'erd_attribute' => 'ERDAttribute',
17
+ 'erd_relationship' => 'ERDRelationship'
18
+ )
12
19
  loader.setup
13
20
 
14
21
  # This module handles diagrams creation and manipulation.
@@ -0,0 +1,20 @@
1
+ module Diagrams
2
+ module Elements
3
+ # Represents an attribute within an ERD entity.
4
+ class ERDAttribute < ::Dry::Struct
5
+ include Elements::Types
6
+
7
+ # Attributes
8
+ attr_reader type: String
9
+ attr_reader name: String
10
+ attr_reader keys: Array[Symbol] # :PK | :FK | :UK
11
+ attr_reader comment: String?
12
+
13
+ # Methods
14
+ def initialize: (type: String, name: String, ?keys: Array[Symbol], ?comment: String?) -> void
15
+ | (Hash[Symbol, untyped]) -> void # Allow hash initialization
16
+
17
+ def to_h: () -> Hash[Symbol, String | Array[Symbol]]
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ require_relative 'erd_attribute'
2
+
3
+ module Diagrams
4
+ module Elements
5
+ # Represents an entity (table) in an ER Diagram.
6
+ class ERDEntity < ::Dry::Struct
7
+ include Elements::Types
8
+
9
+ # Attributes
10
+ attr_reader name: String
11
+ attr_reader entity_attributes: Array[ERDAttribute]
12
+
13
+ # Methods
14
+ def initialize: (name: String, ?entity_attributes: Array[ERDAttribute]) -> void
15
+ | (Hash[Symbol, untyped]) -> void # Allow hash initialization
16
+
17
+ def to_h: () -> Hash[Symbol, String | Array[Hash[Symbol, untyped]]] # Output key is still :attributes
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ module Diagrams
2
+ module Elements
3
+ # Represents a relationship between two entities in an ER Diagram.
4
+ class ERDRelationship < ::Dry::Struct
5
+ include Elements::Types
6
+
7
+ # Type alias for cardinality symbols
8
+ type CARDINALITY = :ZERO_OR_ONE | :ONE_ONLY | :ZERO_OR_MORE | :ONE_OR_MORE
9
+
10
+ # Attributes
11
+ attr_reader entity1: String
12
+ attr_reader entity2: String
13
+ attr_reader cardinality1: CARDINALITY
14
+ attr_reader cardinality2: CARDINALITY
15
+ attr_reader identifying: bool
16
+ attr_reader label: String?
17
+
18
+ # Methods
19
+ def initialize: (entity1: String, entity2: String, cardinality1: CARDINALITY, cardinality2: CARDINALITY, ?identifying: bool, ?label: String?) -> void
20
+ | (Hash[Symbol, untyped]) -> void # Allow hash initialization
21
+
22
+ def to_h: () -> Hash[Symbol, String | Symbol | bool]
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,18 @@
1
+ module Diagrams
2
+ module Elements
3
+ # Represents a section within a Gantt chart, grouping multiple tasks.
4
+ class GanttSection < ::Dry::Struct
5
+ include Elements::Types
6
+
7
+ # Attributes
8
+ attr_reader title: String
9
+ attr_reader tasks: Array[Task]
10
+
11
+ # Methods
12
+ def initialize: (title: String, ?tasks: Array[Task]) -> void
13
+ | (Hash[Symbol, untyped]) -> void # Allow hash initialization
14
+
15
+ def to_h: () -> Hash[Symbol, String | Array[Hash[Symbol, untyped]]]
16
+ end
17
+ end
18
+ end
@@ -4,13 +4,21 @@ module Diagrams
4
4
  include Diagrams::Elements::Types
5
5
 
6
6
  # Attributes
7
- def id: () -> ::String
8
- def name: () -> ::String
9
- def start_date: () -> ::String # Or ::Date if type is changed
10
- def end_date: () -> ::String # Or ::Date if type is changed
7
+ # Type alias for status symbols
8
+ type STATUS = :done | :active | :crit
9
+
10
+ # Attributes
11
+ attr_reader id: String # Unique ID for dependencies
12
+ attr_reader label: String # Display name
13
+ attr_reader status: STATUS? # Task status (nil implies default/future)
14
+ attr_reader start: String # Start date, task ID, or 'after taskX[, taskY]'
15
+ attr_reader duration: String # Duration string (e.g., '7d', '2w')
11
16
 
12
17
  # Methods
13
- def to_h: () -> { id: ::String, name: ::String, start_date: ::String, end_date: ::String }
18
+ def initialize: (id: String, label: String, start: String, duration: String, ?status: STATUS?) -> void
19
+ | (Hash[Symbol, untyped]) -> void # Allow hash initialization
20
+
21
+ def to_h: () -> Hash[Symbol, String | Symbol | nil]
14
22
  end
15
23
  end
16
24
  end
@@ -0,0 +1,30 @@
1
+ module Diagrams
2
+ # Represents an Entity Relationship Diagram (ERD).
3
+ class ERDiagram < Base
4
+ # Instance Variables (via attr_reader)
5
+ attr_reader entities: Hash[String, Elements::ERDEntity]
6
+ attr_reader relationships: Array[Elements::ERDRelationship]
7
+
8
+ # Initialization
9
+ def initialize: (?entities: Array[Elements::ERDEntity], ?relationships: Array[Elements::ERDRelationship], ?version: String | Integer) -> void
10
+
11
+ # Public Methods
12
+ def add_entity: (name: String, ?attributes: Array[Hash[Symbol, untyped]]) -> Elements::ERDEntity
13
+ def add_relationship: (entity1: String, entity2: String, cardinality1: Symbol, cardinality2: Symbol, ?identifying: bool, ?label: String?) -> Elements::ERDRelationship
14
+ def find_entity: (String entity_name) -> Elements::ERDEntity?
15
+
16
+ # --- Base Class Implementation ---
17
+ def to_h_content: () -> Hash[Symbol, Array[Hash[Symbol, untyped]]]
18
+ def identifiable_elements: () -> Hash[Symbol, Array[Elements::ERDEntity | Elements::ERDRelationship]]
19
+
20
+ # Class method for deserialization
21
+ def self.from_h: (Hash[Symbol, untyped] data_hash, version: String | Integer | nil, checksum: String?) -> ERDiagram
22
+
23
+ # --- Private Methods ---
24
+ private
25
+ def validate_relationships!: () -> void
26
+
27
+ # Inherited protected method
28
+ # def update_checksum!: () -> String
29
+ end
30
+ end
@@ -1,29 +1,47 @@
1
1
  module Diagrams
2
+ # Represents a Gantt Chart diagram consisting of tasks over time, grouped into sections.
2
3
  class GanttDiagram < Base
3
- attr_reader title: ::String
4
- attr_reader tasks: ::Array[Elements::Task]
4
+ DEFAULT_SECTION_TITLE: String
5
+
6
+ attr_reader title: String?
7
+ attr_reader sections: Array[Elements::GanttSection] # Use GanttSection
5
8
 
6
9
  # Initializes a new GanttDiagram.
7
- def initialize: (?title: ::String?, ?tasks: ::Array[Elements::Task]?, ?version: Integer | String?) -> void
10
+ def initialize: (?title: String?, ?sections: Array[Elements::GanttSection]?, ?version: String | Integer) -> void
11
+
12
+ # Adds a new section to the diagram.
13
+ def add_section: (String section_title) -> Elements::GanttSection
14
+
15
+ # Adds a task to the current (last) section of the diagram.
16
+ def add_task: (id: String, label: String, start: String, duration: String, ?status: Elements::Task::STATUS?) -> Elements::Task
8
17
 
9
- # Adds a task to the diagram.
10
- def add_task: (Elements::Task task) -> Elements::Task
18
+ # Finds a task by its ID across all sections.
19
+ def find_task: (String task_id) -> Elements::Task?
11
20
 
12
- # Finds a task by its ID.
13
- def find_task: (::String task_id) -> Elements::Task?
21
+ # Finds a section by its title.
22
+ def find_section: (String section_title) -> Elements::GanttSection?
14
23
 
15
24
  # Returns the specific content of the Gantt diagram as a hash.
16
- def to_h_content: () -> { title: ::String, tasks: ::Array[Hash[Symbol, untyped]] }
25
+ def to_h_content: () -> Hash[Symbol, untyped] # More specific: { title: String?, sections: Array[Hash] }
17
26
 
18
27
  # Returns a hash mapping element types to their collections for diffing.
19
- def identifiable_elements: () -> { tasks: ::Array[Elements::Task] }
28
+ def identifiable_elements: () -> Hash[Symbol, Array[Elements::Task]] # Diffing tasks directly
20
29
 
21
30
  # Class method to create a GanttDiagram from a hash.
22
- def self.from_h: (Hash[Symbol | String, untyped] data_hash, version: Integer | String?, checksum: String?) -> GanttDiagram
31
+ def self.from_h: (Hash[Symbol, untyped] data_hash, version: String | Integer | nil, checksum: String?) -> GanttDiagram
23
32
 
24
33
  private
25
34
 
35
+ # Helper to get all tasks from all sections.
36
+ def all_tasks: () -> Array[Elements::Task]
37
+
38
+ # Ensures a default section exists if the sections array is empty.
39
+ def ensure_default_section: () -> void
40
+
26
41
  # Validates the consistency of tasks during initialization.
27
42
  def validate_elements!: () -> void
43
+
44
+ # Inherited protected method
45
+ # def update_checksum!: () -> String
28
46
  end
29
47
  end
@@ -1,33 +1,14 @@
1
+ # RBS signatures for patches applied to the 'diagram' gem's TimelineDiagram class
1
2
  module Diagrams
2
- # Represents a timeline diagram illustrating a chronology of events.
3
3
  class TimelineDiagram < Base
4
- DEFAULT_SECTION_TITLE: String
5
-
6
- # Instance Variables (via attr_reader)
7
- attr_reader title: String?
8
- attr_reader sections: Array[Elements::TimelineSection]
9
-
10
- # Initialization
11
- def initialize: (?title: String?, ?sections: Array[Elements::TimelineSection], ?version: String | Integer) -> void
12
-
13
- # Public Methods
14
- def set_title: (String new_title) -> String
15
- def add_section: (String section_title) -> Elements::TimelineSection
16
- def add_period: (period_label: String, events: String | Array[String]) -> Elements::TimelinePeriod
17
-
18
- # --- Base Class Implementation ---
19
- def to_h_content: () -> Hash[Symbol, untyped] # More specific: Hash[:title?, String | :sections, Array[Hash]]
20
- def identifiable_elements: () -> Hash[Symbol, Array[Elements::TimelineSection | Elements::TimelinePeriod]]
21
-
22
- # Class method for deserialization
23
- def self.from_h: (Hash[Symbol, untyped] data_hash, version: String | Integer | nil, checksum: String?) -> TimelineDiagram
24
-
25
- # --- Private Methods ---
26
- private
27
- def ensure_default_section: () -> void
28
- def find_section: (String section_title) -> Elements::TimelineSection?
29
-
30
- # Inherited protected method
31
- # def update_checksum!: () -> String
4
+ # Added by mermaid-ruby gem
5
+ # Generates the Mermaid syntax for the timeline diagram.
6
+ def to_mermaid: () -> String
7
+
8
+ # Original methods from diagram gem (if known/needed for context)
9
+ # attr_reader title: String?
10
+ # attr_reader sections: Array[Elements::TimelineSection]
11
+ # def initialize: (?title: String?, ?sections: Array[Elements::TimelineSection], ?version: untyped) -> void
12
+ # ... other methods ...
32
13
  end
33
14
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: diagram
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -219,7 +219,11 @@ files:
219
219
  - lib/diagrams/elements.rb
220
220
  - lib/diagrams/elements/class_entity.rb
221
221
  - lib/diagrams/elements/edge.rb
222
+ - lib/diagrams/elements/erd_attribute.rb
223
+ - lib/diagrams/elements/erd_entity.rb
224
+ - lib/diagrams/elements/erd_relationship.rb
222
225
  - lib/diagrams/elements/event.rb
226
+ - lib/diagrams/elements/gantt_section.rb
223
227
  - lib/diagrams/elements/git_branch.rb
224
228
  - lib/diagrams/elements/git_commit.rb
225
229
  - lib/diagrams/elements/node.rb
@@ -231,6 +235,7 @@ files:
231
235
  - lib/diagrams/elements/timeline_period.rb
232
236
  - lib/diagrams/elements/timeline_section.rb
233
237
  - lib/diagrams/elements/transition.rb
238
+ - lib/diagrams/er_diagram.rb
234
239
  - lib/diagrams/flowchart_diagram.rb
235
240
  - lib/diagrams/gantt_diagram.rb
236
241
  - lib/diagrams/gitgraph_diagram.rb
@@ -242,7 +247,11 @@ files:
242
247
  - sig/diagrams/class_diagram.rbs
243
248
  - sig/diagrams/elements/class_entity.rbs
244
249
  - sig/diagrams/elements/edge.rbs
250
+ - sig/diagrams/elements/erd_attribute.rbs
251
+ - sig/diagrams/elements/erd_entity.rbs
252
+ - sig/diagrams/elements/erd_relationship.rbs
245
253
  - sig/diagrams/elements/event.rbs
254
+ - sig/diagrams/elements/gantt_section.rbs
246
255
  - sig/diagrams/elements/git_branch.rbs
247
256
  - sig/diagrams/elements/git_commit.rbs
248
257
  - sig/diagrams/elements/node.rbs
@@ -255,6 +264,7 @@ files:
255
264
  - sig/diagrams/elements/timeline_section.rbs
256
265
  - sig/diagrams/elements/transition.rbs
257
266
  - sig/diagrams/elements/types.rbs
267
+ - sig/diagrams/er_diagram.rbs
258
268
  - sig/diagrams/flowchart_diagram.rbs
259
269
  - sig/diagrams/gantt_diagram.rbs
260
270
  - sig/diagrams/gitgraph_diagram.rbs