Class: Coradoc::AsciiDoc::Model::AttributeList

Inherits:
Base
  • Object
show all
Includes:
Matchers
Defined in:
lib/coradoc/asciidoc/model/attribute_list.rb,
lib/coradoc/asciidoc/model/attribute_list/matchers.rb

Overview

Attribute list for AsciiDoc elements.

Attribute lists represent the square-bracket attribute syntax in AsciiDoc: [positional1, pos2, name1=value1, name2=value2]

This class manages both positional and named attributes, with support for validation and rejection of invalid values.

Examples:

Create an attribute list

attrs = Coradoc::AsciiDoc::Model::Coradoc::AsciiDoc::Model::AttributeList.new
attrs.add_positional("value1")
attrs.add_named("option", "value")
attrs.to_adoc # => "[value1,option=value]\n"

Empty attribute list

attrs = Coradoc::AsciiDoc::Model::Coradoc::AsciiDoc::Model::AttributeList.new
attrs.to_adoc # => "[]"

Hide empty attribute list

attrs = Coradoc::AsciiDoc::Model::Coradoc::AsciiDoc::Model::AttributeList.new
attrs.to_adoc(show_empty: false) # => ""

Defined Under Namespace

Modules: Matchers

Instance Attribute Summary collapse

Attributes inherited from Base

#id

Instance Method Summary collapse

Methods included from Matchers

#many, #one

Methods inherited from Base

#block_level?, #inline?, #serialize_content, #simplify_block_content, #to_h, visit, #visit

Instance Attribute Details

#namedArray<Coradoc::AsciiDoc::Model::NamedAttribute> (readonly)

Returns Named attributes (key-value pairs).

Returns:



40
41
42
43
44
45
46
47
48
49
50
51
52
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/coradoc/asciidoc/model/attribute_list.rb', line 40

class AttributeList < Base
  # Autoload Matchers module
  autoload :Matchers, 'coradoc/asciidoc/model/attribute_list/matchers'

  # Include Matchers module for validation methods
  include Matchers

  attribute :positional,
            Coradoc::AsciiDoc::Model::AttributeListAttribute,
            collection: true,
            initialize_empty: true
  attribute :named, Coradoc::AsciiDoc::Model::NamedAttribute, collection: true, initialize_empty: true
  attribute :rejected_positional,
            Coradoc::AsciiDoc::Model::RejectedPositionalAttribute,
            collection: true,
            initialize_empty: true
  attribute :rejected_named,
            Coradoc::AsciiDoc::Model::NamedAttribute,
            collection: true,
            initialize_empty: true

  # Add positional attributes to this list
  #
  # @param attr [Array<Object>] Values to add as positional attributes
  #
  # @example Adding positional attributes
  #   attrs.add_positional("value1", "value2")
  #
  def add_positional(*attr)
    attr.each do |a|
      @positional << AttributeListAttribute.new(value: a)
    end
  end

  # Add a named attribute to this list
  #
  # @param name [String, Symbol] The attribute name
  # @param value [Object] The attribute value (will be converted to array)
  #
  # @example Adding named attributes
  #   attrs.add_named("title", "My Title")
  #   attrs.add_named("cols", "3,2,1")
  #
  def add_named(name, value)
    @named << NamedAttribute.new(
      name:,
      value: value.is_a?(Array) ? value : [value]
    )
  end

  # Remove the first named attribute matching `name` and return its
  # scalar value (or nil if absent). Used by promoters that lift a
  # named attr out of the residual bag into a typed field.
  # @param name [String, Symbol]
  # @return [String, nil]
  def delete_named(name)
    name_str = name.to_s
    idx = named.index { |n| n.name.to_s == name_str }
    return nil unless idx

    removed = @named.delete_at(idx)
    removed.value.first&.to_s
  end

  # Validate named attributes against validators
  #
  # @param validators [Hash] Hash of name => matcher pairs
  # @yield [name, value] Block called for each invalid attribute
  #
  # @example Validate with custom validator
  #   attrs.validate_named(title: /./) do |name, value|
  #     puts "Invalid #{name}: #{value}"
  #   end
  #
  def validate_named(validators: {})
    named.each_with_index do |named_attribute, _i|
      name = named_attribute.name.to_sym
      value = named_attribute.value

      matcher = validators[name]

      next if matcher && matcher === value

      # Previous implementation would remove the value from the list
      # named.delete(name)
      rejected_named << named_attribute.dup
      yield(name, value) if block_given?
    end
  end

  # Validate positional attributes against validators
  #
  # @param validators [Array] Array of [position, matcher] pairs
  # @yield [position, value] Block called for each invalid attribute
  #
  # @example Validate positional attributes
  #   attrs.validate_positional([[0, /./], [1, Integer]])
  #
  def validate_positional(validators: [])
    positional.each_with_index do |positional_attribute, i|
      matcher = validators[i][1]
      value = positional_attribute.value

      next unless matcher && !(matcher === value)

      warn "#{value} does not match #{matcher}"
      # Previous implementation would remove the value from the list
      # positional[i] = nil
      rejected_positional << RejectedPositionalAttribute.new(
        position: i, value:
      )
      yield(i, value) if block_given?
    end
  end

  # To be overridden in subclasses.
  # @return [Array] Array of positional validators
  def positional_validators
    []
  end

  # To be overridden in subclasses.
  # @return [Hash] Hash of named validators
  def named_validators
    {}
  end

  # Validate this attribute list
  #
  # @return [Array<Lutaml::Model::Error>] Validation errors (empty if valid)
  def validate
    errors = super

    validate_positional(positional_validators) do |i, value|
      errors << Lutaml::Model::Error.new(
        "Positional attribute at position #{i} with value '#{value}' is not valid"
      )
    end

    validate_named(named_validators) do |name, value|
      errors << Lutaml::Model::Error.new(
        "Named attribute #{name} with value '#{value}' is not valid"
      )
    end

    errors
  end

  # Serialize this attribute list to AsciiDoc
  #
  # Generates the square-bracket syntax with valid attributes only.
  #
  # @param show_empty [Boolean] If true, show "[]" for empty lists (default: true)
  # @return [String] AsciiDoc representation of this attribute list
  #
  # @example Serialize with options
  #   attrs.to_adoc(show_empty: true)  # => "[value1,name=val]"
  #   attrs.to_adoc(show_empty: false) # => "[value1,name=val]"
  #   empty.to_adoc                     # => "[]"
  #   empty.to_adoc(show_empty: false) # => ""
  #
  def to_adoc(show_empty: true)
    valid_positional = positional.reject.with_index do |_p, i|
      rejected_positional.any? { |r| r.position == i }
    end

    valid_named = named.reject do |n|
      rejected_named.any? { |r| r.name == n.name }
    end

    adoc = [valid_positional,
            valid_named].flatten.map(&:to_adoc).join(',')

    if adoc.empty? && show_empty
      '[]'
    elsif adoc.empty?
      ''
    else
      "[#{adoc}]"
    end
  end

  def empty?
    positional.empty? && named.empty?
  end

  # Get the scalar value of the first named attribute matching `name`.
  # Returns the first element of the underlying multi-value array, or
  # nil if the name is absent. Use {#fetch_all} to retrieve the full
  # multi-value array.
  # @param name [String, Symbol] The attribute name
  # @return [String, nil]
  def [](name)
    name_str = name.to_s
    named.find { |n| n.name.to_s == name_str }&.value&.first&.to_s
  end

  # Get the full multi-value array for a named attribute.
  # @param name [String, Symbol]
  # @return [Array<String>] (empty when absent)
  def fetch_all(name)
    name_str = name.to_s
    found = named.find { |n| n.name.to_s == name_str }
    found ? found.value : []
  end

  # Get a named attribute value with default
  # @param name [String, Symbol] The attribute name
  # @param default [Object] The default value if not found
  # @return [Object] The attribute value or default
  def fetch(name, default = nil)
    value = self[name]
    value.nil? ? default : value
  end
end

#positionalArray<Coradoc::AsciiDoc::Model::AttributeListAttribute> (readonly)

Returns Positional attributes (by position).

Returns:



40
41
42
43
44
45
46
47
48
49
50
51
52
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/coradoc/asciidoc/model/attribute_list.rb', line 40

class AttributeList < Base
  # Autoload Matchers module
  autoload :Matchers, 'coradoc/asciidoc/model/attribute_list/matchers'

  # Include Matchers module for validation methods
  include Matchers

  attribute :positional,
            Coradoc::AsciiDoc::Model::AttributeListAttribute,
            collection: true,
            initialize_empty: true
  attribute :named, Coradoc::AsciiDoc::Model::NamedAttribute, collection: true, initialize_empty: true
  attribute :rejected_positional,
            Coradoc::AsciiDoc::Model::RejectedPositionalAttribute,
            collection: true,
            initialize_empty: true
  attribute :rejected_named,
            Coradoc::AsciiDoc::Model::NamedAttribute,
            collection: true,
            initialize_empty: true

  # Add positional attributes to this list
  #
  # @param attr [Array<Object>] Values to add as positional attributes
  #
  # @example Adding positional attributes
  #   attrs.add_positional("value1", "value2")
  #
  def add_positional(*attr)
    attr.each do |a|
      @positional << AttributeListAttribute.new(value: a)
    end
  end

  # Add a named attribute to this list
  #
  # @param name [String, Symbol] The attribute name
  # @param value [Object] The attribute value (will be converted to array)
  #
  # @example Adding named attributes
  #   attrs.add_named("title", "My Title")
  #   attrs.add_named("cols", "3,2,1")
  #
  def add_named(name, value)
    @named << NamedAttribute.new(
      name:,
      value: value.is_a?(Array) ? value : [value]
    )
  end

  # Remove the first named attribute matching `name` and return its
  # scalar value (or nil if absent). Used by promoters that lift a
  # named attr out of the residual bag into a typed field.
  # @param name [String, Symbol]
  # @return [String, nil]
  def delete_named(name)
    name_str = name.to_s
    idx = named.index { |n| n.name.to_s == name_str }
    return nil unless idx

    removed = @named.delete_at(idx)
    removed.value.first&.to_s
  end

  # Validate named attributes against validators
  #
  # @param validators [Hash] Hash of name => matcher pairs
  # @yield [name, value] Block called for each invalid attribute
  #
  # @example Validate with custom validator
  #   attrs.validate_named(title: /./) do |name, value|
  #     puts "Invalid #{name}: #{value}"
  #   end
  #
  def validate_named(validators: {})
    named.each_with_index do |named_attribute, _i|
      name = named_attribute.name.to_sym
      value = named_attribute.value

      matcher = validators[name]

      next if matcher && matcher === value

      # Previous implementation would remove the value from the list
      # named.delete(name)
      rejected_named << named_attribute.dup
      yield(name, value) if block_given?
    end
  end

  # Validate positional attributes against validators
  #
  # @param validators [Array] Array of [position, matcher] pairs
  # @yield [position, value] Block called for each invalid attribute
  #
  # @example Validate positional attributes
  #   attrs.validate_positional([[0, /./], [1, Integer]])
  #
  def validate_positional(validators: [])
    positional.each_with_index do |positional_attribute, i|
      matcher = validators[i][1]
      value = positional_attribute.value

      next unless matcher && !(matcher === value)

      warn "#{value} does not match #{matcher}"
      # Previous implementation would remove the value from the list
      # positional[i] = nil
      rejected_positional << RejectedPositionalAttribute.new(
        position: i, value:
      )
      yield(i, value) if block_given?
    end
  end

  # To be overridden in subclasses.
  # @return [Array] Array of positional validators
  def positional_validators
    []
  end

  # To be overridden in subclasses.
  # @return [Hash] Hash of named validators
  def named_validators
    {}
  end

  # Validate this attribute list
  #
  # @return [Array<Lutaml::Model::Error>] Validation errors (empty if valid)
  def validate
    errors = super

    validate_positional(positional_validators) do |i, value|
      errors << Lutaml::Model::Error.new(
        "Positional attribute at position #{i} with value '#{value}' is not valid"
      )
    end

    validate_named(named_validators) do |name, value|
      errors << Lutaml::Model::Error.new(
        "Named attribute #{name} with value '#{value}' is not valid"
      )
    end

    errors
  end

  # Serialize this attribute list to AsciiDoc
  #
  # Generates the square-bracket syntax with valid attributes only.
  #
  # @param show_empty [Boolean] If true, show "[]" for empty lists (default: true)
  # @return [String] AsciiDoc representation of this attribute list
  #
  # @example Serialize with options
  #   attrs.to_adoc(show_empty: true)  # => "[value1,name=val]"
  #   attrs.to_adoc(show_empty: false) # => "[value1,name=val]"
  #   empty.to_adoc                     # => "[]"
  #   empty.to_adoc(show_empty: false) # => ""
  #
  def to_adoc(show_empty: true)
    valid_positional = positional.reject.with_index do |_p, i|
      rejected_positional.any? { |r| r.position == i }
    end

    valid_named = named.reject do |n|
      rejected_named.any? { |r| r.name == n.name }
    end

    adoc = [valid_positional,
            valid_named].flatten.map(&:to_adoc).join(',')

    if adoc.empty? && show_empty
      '[]'
    elsif adoc.empty?
      ''
    else
      "[#{adoc}]"
    end
  end

  def empty?
    positional.empty? && named.empty?
  end

  # Get the scalar value of the first named attribute matching `name`.
  # Returns the first element of the underlying multi-value array, or
  # nil if the name is absent. Use {#fetch_all} to retrieve the full
  # multi-value array.
  # @param name [String, Symbol] The attribute name
  # @return [String, nil]
  def [](name)
    name_str = name.to_s
    named.find { |n| n.name.to_s == name_str }&.value&.first&.to_s
  end

  # Get the full multi-value array for a named attribute.
  # @param name [String, Symbol]
  # @return [Array<String>] (empty when absent)
  def fetch_all(name)
    name_str = name.to_s
    found = named.find { |n| n.name.to_s == name_str }
    found ? found.value : []
  end

  # Get a named attribute value with default
  # @param name [String, Symbol] The attribute name
  # @param default [Object] The default value if not found
  # @return [Object] The attribute value or default
  def fetch(name, default = nil)
    value = self[name]
    value.nil? ? default : value
  end
end

#rejected_namedArray<Coradoc::AsciiDoc::Model::NamedAttribute> (readonly)

Returns Rejected named attributes.

Returns:



40
41
42
43
44
45
46
47
48
49
50
51
52
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/coradoc/asciidoc/model/attribute_list.rb', line 40

class AttributeList < Base
  # Autoload Matchers module
  autoload :Matchers, 'coradoc/asciidoc/model/attribute_list/matchers'

  # Include Matchers module for validation methods
  include Matchers

  attribute :positional,
            Coradoc::AsciiDoc::Model::AttributeListAttribute,
            collection: true,
            initialize_empty: true
  attribute :named, Coradoc::AsciiDoc::Model::NamedAttribute, collection: true, initialize_empty: true
  attribute :rejected_positional,
            Coradoc::AsciiDoc::Model::RejectedPositionalAttribute,
            collection: true,
            initialize_empty: true
  attribute :rejected_named,
            Coradoc::AsciiDoc::Model::NamedAttribute,
            collection: true,
            initialize_empty: true

  # Add positional attributes to this list
  #
  # @param attr [Array<Object>] Values to add as positional attributes
  #
  # @example Adding positional attributes
  #   attrs.add_positional("value1", "value2")
  #
  def add_positional(*attr)
    attr.each do |a|
      @positional << AttributeListAttribute.new(value: a)
    end
  end

  # Add a named attribute to this list
  #
  # @param name [String, Symbol] The attribute name
  # @param value [Object] The attribute value (will be converted to array)
  #
  # @example Adding named attributes
  #   attrs.add_named("title", "My Title")
  #   attrs.add_named("cols", "3,2,1")
  #
  def add_named(name, value)
    @named << NamedAttribute.new(
      name:,
      value: value.is_a?(Array) ? value : [value]
    )
  end

  # Remove the first named attribute matching `name` and return its
  # scalar value (or nil if absent). Used by promoters that lift a
  # named attr out of the residual bag into a typed field.
  # @param name [String, Symbol]
  # @return [String, nil]
  def delete_named(name)
    name_str = name.to_s
    idx = named.index { |n| n.name.to_s == name_str }
    return nil unless idx

    removed = @named.delete_at(idx)
    removed.value.first&.to_s
  end

  # Validate named attributes against validators
  #
  # @param validators [Hash] Hash of name => matcher pairs
  # @yield [name, value] Block called for each invalid attribute
  #
  # @example Validate with custom validator
  #   attrs.validate_named(title: /./) do |name, value|
  #     puts "Invalid #{name}: #{value}"
  #   end
  #
  def validate_named(validators: {})
    named.each_with_index do |named_attribute, _i|
      name = named_attribute.name.to_sym
      value = named_attribute.value

      matcher = validators[name]

      next if matcher && matcher === value

      # Previous implementation would remove the value from the list
      # named.delete(name)
      rejected_named << named_attribute.dup
      yield(name, value) if block_given?
    end
  end

  # Validate positional attributes against validators
  #
  # @param validators [Array] Array of [position, matcher] pairs
  # @yield [position, value] Block called for each invalid attribute
  #
  # @example Validate positional attributes
  #   attrs.validate_positional([[0, /./], [1, Integer]])
  #
  def validate_positional(validators: [])
    positional.each_with_index do |positional_attribute, i|
      matcher = validators[i][1]
      value = positional_attribute.value

      next unless matcher && !(matcher === value)

      warn "#{value} does not match #{matcher}"
      # Previous implementation would remove the value from the list
      # positional[i] = nil
      rejected_positional << RejectedPositionalAttribute.new(
        position: i, value:
      )
      yield(i, value) if block_given?
    end
  end

  # To be overridden in subclasses.
  # @return [Array] Array of positional validators
  def positional_validators
    []
  end

  # To be overridden in subclasses.
  # @return [Hash] Hash of named validators
  def named_validators
    {}
  end

  # Validate this attribute list
  #
  # @return [Array<Lutaml::Model::Error>] Validation errors (empty if valid)
  def validate
    errors = super

    validate_positional(positional_validators) do |i, value|
      errors << Lutaml::Model::Error.new(
        "Positional attribute at position #{i} with value '#{value}' is not valid"
      )
    end

    validate_named(named_validators) do |name, value|
      errors << Lutaml::Model::Error.new(
        "Named attribute #{name} with value '#{value}' is not valid"
      )
    end

    errors
  end

  # Serialize this attribute list to AsciiDoc
  #
  # Generates the square-bracket syntax with valid attributes only.
  #
  # @param show_empty [Boolean] If true, show "[]" for empty lists (default: true)
  # @return [String] AsciiDoc representation of this attribute list
  #
  # @example Serialize with options
  #   attrs.to_adoc(show_empty: true)  # => "[value1,name=val]"
  #   attrs.to_adoc(show_empty: false) # => "[value1,name=val]"
  #   empty.to_adoc                     # => "[]"
  #   empty.to_adoc(show_empty: false) # => ""
  #
  def to_adoc(show_empty: true)
    valid_positional = positional.reject.with_index do |_p, i|
      rejected_positional.any? { |r| r.position == i }
    end

    valid_named = named.reject do |n|
      rejected_named.any? { |r| r.name == n.name }
    end

    adoc = [valid_positional,
            valid_named].flatten.map(&:to_adoc).join(',')

    if adoc.empty? && show_empty
      '[]'
    elsif adoc.empty?
      ''
    else
      "[#{adoc}]"
    end
  end

  def empty?
    positional.empty? && named.empty?
  end

  # Get the scalar value of the first named attribute matching `name`.
  # Returns the first element of the underlying multi-value array, or
  # nil if the name is absent. Use {#fetch_all} to retrieve the full
  # multi-value array.
  # @param name [String, Symbol] The attribute name
  # @return [String, nil]
  def [](name)
    name_str = name.to_s
    named.find { |n| n.name.to_s == name_str }&.value&.first&.to_s
  end

  # Get the full multi-value array for a named attribute.
  # @param name [String, Symbol]
  # @return [Array<String>] (empty when absent)
  def fetch_all(name)
    name_str = name.to_s
    found = named.find { |n| n.name.to_s == name_str }
    found ? found.value : []
  end

  # Get a named attribute value with default
  # @param name [String, Symbol] The attribute name
  # @param default [Object] The default value if not found
  # @return [Object] The attribute value or default
  def fetch(name, default = nil)
    value = self[name]
    value.nil? ? default : value
  end
end

#rejected_positionalArray<Coradoc::AsciiDoc::Model::RejectedPositionalAttribute> (readonly)

Returns Rejected positional attributes.

Returns:



40
41
42
43
44
45
46
47
48
49
50
51
52
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/coradoc/asciidoc/model/attribute_list.rb', line 40

class AttributeList < Base
  # Autoload Matchers module
  autoload :Matchers, 'coradoc/asciidoc/model/attribute_list/matchers'

  # Include Matchers module for validation methods
  include Matchers

  attribute :positional,
            Coradoc::AsciiDoc::Model::AttributeListAttribute,
            collection: true,
            initialize_empty: true
  attribute :named, Coradoc::AsciiDoc::Model::NamedAttribute, collection: true, initialize_empty: true
  attribute :rejected_positional,
            Coradoc::AsciiDoc::Model::RejectedPositionalAttribute,
            collection: true,
            initialize_empty: true
  attribute :rejected_named,
            Coradoc::AsciiDoc::Model::NamedAttribute,
            collection: true,
            initialize_empty: true

  # Add positional attributes to this list
  #
  # @param attr [Array<Object>] Values to add as positional attributes
  #
  # @example Adding positional attributes
  #   attrs.add_positional("value1", "value2")
  #
  def add_positional(*attr)
    attr.each do |a|
      @positional << AttributeListAttribute.new(value: a)
    end
  end

  # Add a named attribute to this list
  #
  # @param name [String, Symbol] The attribute name
  # @param value [Object] The attribute value (will be converted to array)
  #
  # @example Adding named attributes
  #   attrs.add_named("title", "My Title")
  #   attrs.add_named("cols", "3,2,1")
  #
  def add_named(name, value)
    @named << NamedAttribute.new(
      name:,
      value: value.is_a?(Array) ? value : [value]
    )
  end

  # Remove the first named attribute matching `name` and return its
  # scalar value (or nil if absent). Used by promoters that lift a
  # named attr out of the residual bag into a typed field.
  # @param name [String, Symbol]
  # @return [String, nil]
  def delete_named(name)
    name_str = name.to_s
    idx = named.index { |n| n.name.to_s == name_str }
    return nil unless idx

    removed = @named.delete_at(idx)
    removed.value.first&.to_s
  end

  # Validate named attributes against validators
  #
  # @param validators [Hash] Hash of name => matcher pairs
  # @yield [name, value] Block called for each invalid attribute
  #
  # @example Validate with custom validator
  #   attrs.validate_named(title: /./) do |name, value|
  #     puts "Invalid #{name}: #{value}"
  #   end
  #
  def validate_named(validators: {})
    named.each_with_index do |named_attribute, _i|
      name = named_attribute.name.to_sym
      value = named_attribute.value

      matcher = validators[name]

      next if matcher && matcher === value

      # Previous implementation would remove the value from the list
      # named.delete(name)
      rejected_named << named_attribute.dup
      yield(name, value) if block_given?
    end
  end

  # Validate positional attributes against validators
  #
  # @param validators [Array] Array of [position, matcher] pairs
  # @yield [position, value] Block called for each invalid attribute
  #
  # @example Validate positional attributes
  #   attrs.validate_positional([[0, /./], [1, Integer]])
  #
  def validate_positional(validators: [])
    positional.each_with_index do |positional_attribute, i|
      matcher = validators[i][1]
      value = positional_attribute.value

      next unless matcher && !(matcher === value)

      warn "#{value} does not match #{matcher}"
      # Previous implementation would remove the value from the list
      # positional[i] = nil
      rejected_positional << RejectedPositionalAttribute.new(
        position: i, value:
      )
      yield(i, value) if block_given?
    end
  end

  # To be overridden in subclasses.
  # @return [Array] Array of positional validators
  def positional_validators
    []
  end

  # To be overridden in subclasses.
  # @return [Hash] Hash of named validators
  def named_validators
    {}
  end

  # Validate this attribute list
  #
  # @return [Array<Lutaml::Model::Error>] Validation errors (empty if valid)
  def validate
    errors = super

    validate_positional(positional_validators) do |i, value|
      errors << Lutaml::Model::Error.new(
        "Positional attribute at position #{i} with value '#{value}' is not valid"
      )
    end

    validate_named(named_validators) do |name, value|
      errors << Lutaml::Model::Error.new(
        "Named attribute #{name} with value '#{value}' is not valid"
      )
    end

    errors
  end

  # Serialize this attribute list to AsciiDoc
  #
  # Generates the square-bracket syntax with valid attributes only.
  #
  # @param show_empty [Boolean] If true, show "[]" for empty lists (default: true)
  # @return [String] AsciiDoc representation of this attribute list
  #
  # @example Serialize with options
  #   attrs.to_adoc(show_empty: true)  # => "[value1,name=val]"
  #   attrs.to_adoc(show_empty: false) # => "[value1,name=val]"
  #   empty.to_adoc                     # => "[]"
  #   empty.to_adoc(show_empty: false) # => ""
  #
  def to_adoc(show_empty: true)
    valid_positional = positional.reject.with_index do |_p, i|
      rejected_positional.any? { |r| r.position == i }
    end

    valid_named = named.reject do |n|
      rejected_named.any? { |r| r.name == n.name }
    end

    adoc = [valid_positional,
            valid_named].flatten.map(&:to_adoc).join(',')

    if adoc.empty? && show_empty
      '[]'
    elsif adoc.empty?
      ''
    else
      "[#{adoc}]"
    end
  end

  def empty?
    positional.empty? && named.empty?
  end

  # Get the scalar value of the first named attribute matching `name`.
  # Returns the first element of the underlying multi-value array, or
  # nil if the name is absent. Use {#fetch_all} to retrieve the full
  # multi-value array.
  # @param name [String, Symbol] The attribute name
  # @return [String, nil]
  def [](name)
    name_str = name.to_s
    named.find { |n| n.name.to_s == name_str }&.value&.first&.to_s
  end

  # Get the full multi-value array for a named attribute.
  # @param name [String, Symbol]
  # @return [Array<String>] (empty when absent)
  def fetch_all(name)
    name_str = name.to_s
    found = named.find { |n| n.name.to_s == name_str }
    found ? found.value : []
  end

  # Get a named attribute value with default
  # @param name [String, Symbol] The attribute name
  # @param default [Object] The default value if not found
  # @return [Object] The attribute value or default
  def fetch(name, default = nil)
    value = self[name]
    value.nil? ? default : value
  end
end

Instance Method Details

#[](name) ⇒ String?

Get the scalar value of the first named attribute matching name. Returns the first element of the underlying multi-value array, or nil if the name is absent. Use #fetch_all to retrieve the full multi-value array.

Parameters:

  • name (String, Symbol)

    The attribute name

Returns:

  • (String, nil)


232
233
234
235
# File 'lib/coradoc/asciidoc/model/attribute_list.rb', line 232

def [](name)
  name_str = name.to_s
  named.find { |n| n.name.to_s == name_str }&.value&.first&.to_s
end

#add_named(name, value) ⇒ Object

Add a named attribute to this list

Examples:

Adding named attributes

attrs.add_named("title", "My Title")
attrs.add_named("cols", "3,2,1")

Parameters:

  • name (String, Symbol)

    The attribute name

  • value (Object)

    The attribute value (will be converted to array)



83
84
85
86
87
88
# File 'lib/coradoc/asciidoc/model/attribute_list.rb', line 83

def add_named(name, value)
  @named << NamedAttribute.new(
    name:,
    value: value.is_a?(Array) ? value : [value]
  )
end

#add_positional(*attr) ⇒ Object

Add positional attributes to this list

Examples:

Adding positional attributes

attrs.add_positional("value1", "value2")

Parameters:

  • attr (Array<Object>)

    Values to add as positional attributes



68
69
70
71
72
# File 'lib/coradoc/asciidoc/model/attribute_list.rb', line 68

def add_positional(*attr)
  attr.each do |a|
    @positional << AttributeListAttribute.new(value: a)
  end
end

#delete_named(name) ⇒ String?

Remove the first named attribute matching name and return its scalar value (or nil if absent). Used by promoters that lift a named attr out of the residual bag into a typed field.

Parameters:

  • name (String, Symbol)

Returns:

  • (String, nil)


95
96
97
98
99
100
101
102
# File 'lib/coradoc/asciidoc/model/attribute_list.rb', line 95

def delete_named(name)
  name_str = name.to_s
  idx = named.index { |n| n.name.to_s == name_str }
  return nil unless idx

  removed = @named.delete_at(idx)
  removed.value.first&.to_s
end

#empty?Boolean

Returns:

  • (Boolean)


222
223
224
# File 'lib/coradoc/asciidoc/model/attribute_list.rb', line 222

def empty?
  positional.empty? && named.empty?
end

#fetch(name, default = nil) ⇒ Object

Get a named attribute value with default

Parameters:

  • name (String, Symbol)

    The attribute name

  • default (Object) (defaults to: nil)

    The default value if not found

Returns:

  • (Object)

    The attribute value or default



250
251
252
253
# File 'lib/coradoc/asciidoc/model/attribute_list.rb', line 250

def fetch(name, default = nil)
  value = self[name]
  value.nil? ? default : value
end

#fetch_all(name) ⇒ Array<String>

Get the full multi-value array for a named attribute.

Parameters:

  • name (String, Symbol)

Returns:

  • (Array<String>)

    (empty when absent)



240
241
242
243
244
# File 'lib/coradoc/asciidoc/model/attribute_list.rb', line 240

def fetch_all(name)
  name_str = name.to_s
  found = named.find { |n| n.name.to_s == name_str }
  found ? found.value : []
end

#named_validatorsHash

To be overridden in subclasses.

Returns:

  • (Hash)

    Hash of named validators



163
164
165
# File 'lib/coradoc/asciidoc/model/attribute_list.rb', line 163

def named_validators
  {}
end

#positional_validatorsArray

To be overridden in subclasses.

Returns:

  • (Array)

    Array of positional validators



157
158
159
# File 'lib/coradoc/asciidoc/model/attribute_list.rb', line 157

def positional_validators
  []
end

#to_adoc(show_empty: true) ⇒ String

Serialize this attribute list to AsciiDoc

Generates the square-bracket syntax with valid attributes only.

Examples:

Serialize with options

attrs.to_adoc(show_empty: true)  # => "[value1,name=val]"
attrs.to_adoc(show_empty: false) # => "[value1,name=val]"
empty.to_adoc                     # => "[]"
empty.to_adoc(show_empty: false) # => ""

Parameters:

  • show_empty (Boolean) (defaults to: true)

    If true, show "[]" for empty lists (default: true)

Returns:

  • (String)

    AsciiDoc representation of this attribute list



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/coradoc/asciidoc/model/attribute_list.rb', line 201

def to_adoc(show_empty: true)
  valid_positional = positional.reject.with_index do |_p, i|
    rejected_positional.any? { |r| r.position == i }
  end

  valid_named = named.reject do |n|
    rejected_named.any? { |r| r.name == n.name }
  end

  adoc = [valid_positional,
          valid_named].flatten.map(&:to_adoc).join(',')

  if adoc.empty? && show_empty
    '[]'
  elsif adoc.empty?
    ''
  else
    "[#{adoc}]"
  end
end

#validateArray<Lutaml::Model::Error>

Validate this attribute list

Returns:

  • (Array<Lutaml::Model::Error>)

    Validation errors (empty if valid)



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/coradoc/asciidoc/model/attribute_list.rb', line 170

def validate
  errors = super

  validate_positional(positional_validators) do |i, value|
    errors << Lutaml::Model::Error.new(
      "Positional attribute at position #{i} with value '#{value}' is not valid"
    )
  end

  validate_named(named_validators) do |name, value|
    errors << Lutaml::Model::Error.new(
      "Named attribute #{name} with value '#{value}' is not valid"
    )
  end

  errors
end

#validate_named(validators: {}) {|name, value| ... } ⇒ Object

Validate named attributes against validators

Examples:

Validate with custom validator

attrs.validate_named(title: /./) do |name, value|
  puts "Invalid #{name}: #{value}"
end

Parameters:

  • validators (Hash) (defaults to: {})

    Hash of name => matcher pairs

Yields:

  • (name, value)

    Block called for each invalid attribute



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/coradoc/asciidoc/model/attribute_list.rb', line 114

def validate_named(validators: {})
  named.each_with_index do |named_attribute, _i|
    name = named_attribute.name.to_sym
    value = named_attribute.value

    matcher = validators[name]

    next if matcher && matcher === value

    # Previous implementation would remove the value from the list
    # named.delete(name)
    rejected_named << named_attribute.dup
    yield(name, value) if block_given?
  end
end

#validate_positional(validators: []) {|position, value| ... } ⇒ Object

Validate positional attributes against validators

Examples:

Validate positional attributes

attrs.validate_positional([[0, /./], [1, Integer]])

Parameters:

  • validators (Array) (defaults to: [])

    Array of [position, matcher] pairs

Yields:

  • (position, value)

    Block called for each invalid attribute



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/coradoc/asciidoc/model/attribute_list.rb', line 138

def validate_positional(validators: [])
  positional.each_with_index do |positional_attribute, i|
    matcher = validators[i][1]
    value = positional_attribute.value

    next unless matcher && !(matcher === value)

    warn "#{value} does not match #{matcher}"
    # Previous implementation would remove the value from the list
    # positional[i] = nil
    rejected_positional << RejectedPositionalAttribute.new(
      position: i, value:
    )
    yield(i, value) if block_given?
  end
end