adf_builder 1.0.0 → 1.2.1

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: efabe67ca191eb4a8d3cb985d6fa47869b2d719989337213d691734c07263cd0
4
- data.tar.gz: 9b83a052a0198a37a8942bbdf39400681e0268a034ec05a0c5663d038ffbb378
3
+ metadata.gz: 24b99369efbb491a84bd7a472de00624914228c6de3334e806ab46c7adbb9fa9
4
+ data.tar.gz: c9b0e631fa1118938a976d80e3be092d958c7fc35b7d2fe547bb291c4d4bbff9
5
5
  SHA512:
6
- metadata.gz: 61da2f959f608ea29bbd29a0c3a63591ec3ef4d5a94dd0df50b8c7ea9f0eec4a254e696e05fe2f58d229d01081a837870a337852801afcb57a3090122036aefc
7
- data.tar.gz: e219d9a2b74c562295a72464042cec57160e3f9ec8b99cc2b90f7bd4deb96fd956dad7c9632f74741470467443a10d2c4472e87468b299af2e50c381347a0087
6
+ metadata.gz: 8e46cdda771a5c61882464d1e21a4e1117d156c13603877a7cb35e50bc0cd45ef480236a8aa2add261b983e5ed11142f655e9611e6f97cf04c7944305b6c3640
7
+ data.tar.gz: 4560e7057db7c62d8a6bdc90e3b5413c99ae273c5f70a1fd66d621e3a8c3d574ee80e619797c0faf32ce6a6c6fbfd33976fadc3cbda26d66c1a24e673b911447
data/.gitignore CHANGED
@@ -11,3 +11,4 @@
11
11
  .rspec_status
12
12
 
13
13
  deploy.txt
14
+ *.gem
data/CHANGELOG.md CHANGED
@@ -1,4 +1,26 @@
1
- ## [Unreleased]
1
+ ## [1.2.1] - 2026-01-19
2
+ - **Strict Validations**: Added `presence` validation for `Vehicle` (year, make, model required).
3
+
4
+ ## [1.2.0] - 2026-01-19
5
+ - **Strict Validations**: Added validation for `Vehicle` condition, `Option` weighting (range), `Finance` method, and required `ID` source.
6
+
7
+ ## [1.1.0] - 2026-01-19
8
+ - **Feature Complete**: Implemented all ADF 1.0 nodes and attributes including `Vendor`, `Provider`, and complex `Vehicle` tags (`Finance`, `Option`, `Odometer`, `ColorCombination`, `ImageTag`, `Price`).
9
+ - **Singular Field logic**: Methods for singular fields (e.g. `vehicle.year`) now correctly replace existing values instead of appending.
10
+ - **Removed Legacy Code**: Cleaned up deprecated legacy implementation directories.
11
+
12
+ ## [1.0.0] - 2026-01-19
13
+ - **MAJOR OVERHAUL**: Complete rewrtie of the library architecture.
14
+ - **New Block-based DSL**: Intuitive API for building ADF documents (`AdfBuilder.build { vehicle { ... } }`).
15
+ - **Validation**: Strict enforcement of ADF enumerations and structure (e.g. `vehicle status: :new`).
16
+ - **Editing**: New `AdfBuilder.tree` method for programmatic modifications after construction.
17
+ - **Robustness**: Complete rewrite of XML generation using robust heuristics and strict `Ox` serialization.
18
+ - **Compatibility**: Verified for Ruby 3.4.x.
19
+ - **Features**:
20
+ - Support for multiple vehicles/customers.
21
+ - Support for singular vs multiple item logic.
22
+ - Dynamic support for arbitrary/custom tags (`method_missing`).
23
+ - Automatic handling of XML attributes vs simple elements.
2
24
 
3
25
  ## [0.4.0] - 2026-01-19
4
26
  - Modernized dependencies
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- adf_builder (1.0.0)
4
+ adf_builder (1.2.0)
5
5
  ox (~> 2.14)
6
6
 
7
7
  GEM
@@ -3,56 +3,56 @@
3
3
  module AdfBuilder
4
4
  module Nodes
5
5
  class Customer < Node
6
+ def initialize
7
+ super
8
+ @tag_name = :customer
9
+ end
10
+
6
11
  def contact(&block)
12
+ remove_children(:contact)
7
13
  contact = Contact.new
8
14
  contact.instance_eval(&block) if block_given?
9
15
  add_child(contact)
10
16
  end
11
- end
12
17
 
13
- class Contact < Node
14
- def name(value, part: nil, type: nil)
15
- name_node = Name.new(value, part: part, type: type)
16
- add_child(name_node)
18
+ def id(value, sequence: nil, source: nil)
19
+ # id* is multiple
20
+ add_child(Id.new(value, sequence: sequence, source: source))
17
21
  end
18
22
 
19
- def email(value)
20
- add_child(SimpleElement.new(:email, value))
23
+ def timeframe(&block)
24
+ remove_children(:timeframe)
25
+ tf = Timeframe.new
26
+ tf.instance_eval(&block) if block_given?
27
+ add_child(tf)
21
28
  end
22
29
 
23
- def phone(value, type: nil)
24
- phone_node = Phone.new(value, type: type)
25
- add_child(phone_node)
30
+ def comments(value)
31
+ remove_children(:comments)
32
+ add_child(GenericNode.new(:comments, {}, value))
26
33
  end
27
34
  end
28
35
 
29
- class Name < Node
30
- def initialize(value, part: nil, type: nil)
31
- super()
32
- @value = value
33
- @attributes[:part] = part if part
34
- @attributes[:type] = type if type
36
+ class Timeframe < Node
37
+ def initialize
38
+ super
39
+ @tag_name = :timeframe
35
40
  end
36
- attr_reader :value
37
- end
38
41
 
39
- class Phone < Node
40
- def initialize(value, type: nil)
41
- super()
42
- @value = value
43
- @attributes[:type] = type if type
42
+ def description(value)
43
+ remove_children(:description)
44
+ add_child(GenericNode.new(:description, {}, value))
45
+ end
46
+
47
+ def earliestdate(value)
48
+ remove_children(:earliestdate)
49
+ add_child(GenericNode.new(:earliestdate, {}, value))
44
50
  end
45
- attr_reader :value
46
- end
47
51
 
48
- # Simple Helper for tags like <email>foo</email>
49
- class SimpleElement < Node
50
- def initialize(tag_name, value)
51
- super()
52
- @tag_name = tag_name
53
- @value = value
52
+ def latestdate(value)
53
+ remove_children(:latestdate)
54
+ add_child(GenericNode.new(:latestdate, {}, value))
54
55
  end
55
- attr_reader :value, :tag_name
56
56
  end
57
57
  end
58
58
  end
@@ -5,17 +5,23 @@ module AdfBuilder
5
5
  class Node
6
6
  include AdfBuilder::Validations
7
7
 
8
- attr_reader :children, :attributes
8
+ attr_reader :children, :attributes, :value, :tag_name
9
9
 
10
10
  def initialize
11
11
  @children = []
12
12
  @attributes = {}
13
+ @value = nil
14
+ @tag_name = nil
13
15
  end
14
16
 
15
17
  def add_child(node)
16
18
  @children << node
17
19
  end
18
20
 
21
+ def remove_children(tag_name)
22
+ @children.reject! { |c| c.tag_name == tag_name }
23
+ end
24
+
19
25
  def to_xml
20
26
  Serializer.to_xml(self)
21
27
  end
@@ -57,7 +63,6 @@ module AdfBuilder
57
63
  @attributes = attributes
58
64
  @value = value
59
65
  end
60
- attr_reader :tag_name, :value
61
66
  end
62
67
 
63
68
  class Root < Node
@@ -19,6 +19,18 @@ module AdfBuilder
19
19
  add_child(customer)
20
20
  end
21
21
 
22
+ def vendor(&block)
23
+ vendor = Vendor.new
24
+ vendor.instance_eval(&block) if block_given?
25
+ add_child(vendor)
26
+ end
27
+
28
+ def provider(&block)
29
+ provider = Provider.new
30
+ provider.instance_eval(&block) if block_given?
31
+ add_child(provider)
32
+ end
33
+
22
34
  # Helpers for Editing
23
35
  def vehicles
24
36
  @children.select { |c| c.is_a?(Vehicle) }
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AdfBuilder
4
+ module Nodes
5
+ class Provider < Node
6
+ def initialize
7
+ super
8
+ @tag_name = :provider
9
+ end
10
+
11
+ def id(value, sequence: nil, source: nil)
12
+ add_child(Id.new(value, sequence: sequence, source: source))
13
+ end
14
+
15
+ def name(value, part: nil, type: nil)
16
+ remove_children(:name)
17
+ add_child(Name.new(value, part: part, type: type))
18
+ end
19
+
20
+ def service(value)
21
+ remove_children(:service)
22
+ add_child(GenericNode.new(:service, {}, value))
23
+ end
24
+
25
+ def url(value)
26
+ remove_children(:url)
27
+ add_child(GenericNode.new(:url, {}, value))
28
+ end
29
+
30
+ def email(value, preferredcontact: nil)
31
+ remove_children(:email)
32
+ add_child(Email.new(value, preferredcontact: preferredcontact))
33
+ end
34
+
35
+ def phone(value, type: nil, time: nil, preferredcontact: nil)
36
+ remove_children(:phone)
37
+ add_child(Phone.new(value, type: type, time: time, preferredcontact: preferredcontact))
38
+ end
39
+
40
+ def contact(primary_contact: false, &block)
41
+ remove_children(:contact)
42
+ contact = Contact.new(primary_contact: primary_contact)
43
+ contact.instance_eval(&block) if block_given?
44
+ add_child(contact)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AdfBuilder
4
+ module Nodes
5
+ class Id < Node
6
+ def initialize(value, source:, sequence: nil)
7
+ super()
8
+ raise ArgumentError, "Source is required" if source.nil?
9
+
10
+ @tag_name = :id
11
+ @value = value
12
+ @attributes[:sequence] = sequence if sequence
13
+ @attributes[:source] = source
14
+ end
15
+ end
16
+
17
+ class Phone < Node
18
+ def initialize(value, type: nil, time: nil, preferredcontact: nil)
19
+ super()
20
+ @tag_name = :phone
21
+ @value = value
22
+ @attributes[:type] = type if type
23
+ @attributes[:time] = time if time
24
+ @attributes[:preferredcontact] = preferredcontact if preferredcontact
25
+ end
26
+ end
27
+
28
+ class Email < Node
29
+ def initialize(value, preferredcontact: nil)
30
+ super()
31
+ @tag_name = :email
32
+ @value = value
33
+ @attributes[:preferredcontact] = preferredcontact if preferredcontact
34
+ end
35
+ end
36
+
37
+ class Name < Node
38
+ def initialize(value, part: nil, type: nil)
39
+ super()
40
+ @tag_name = :name
41
+ @value = value
42
+ @attributes[:part] = part if part
43
+ @attributes[:type] = type if type
44
+ end
45
+ end
46
+
47
+ class Address < Node
48
+ def initialize(type: nil)
49
+ super()
50
+ @tag_name = :address
51
+ @attributes[:type] = type if type
52
+ end
53
+
54
+ def street(value, line: nil)
55
+ node = GenericNode.new(:street, { line: line }.compact, value)
56
+ add_child(node)
57
+ end
58
+
59
+ # Simple elements
60
+ %i[apartment city regioncode postalcode country].each do |tag|
61
+ define_method(tag) do |value|
62
+ add_child(GenericNode.new(tag, {}, value))
63
+ end
64
+ end
65
+ end
66
+
67
+ class Contact < Node
68
+ def initialize(primary_contact: false)
69
+ super()
70
+ @tag_name = :contact
71
+ # primary_contact might be useful for logic but not an attribute
72
+ end
73
+
74
+ def name(value, part: nil, type: nil)
75
+ add_child(Name.new(value, part: part, type: type))
76
+ end
77
+
78
+ def email(value, preferredcontact: nil)
79
+ add_child(Email.new(value, preferredcontact: preferredcontact))
80
+ end
81
+
82
+ def phone(value, type: nil, time: nil, preferredcontact: nil)
83
+ add_child(Phone.new(value, type: type, time: time, preferredcontact: preferredcontact))
84
+ end
85
+
86
+ def address(type: nil, &block)
87
+ addr = Address.new(type: type)
88
+ addr.instance_eval(&block) if block_given?
89
+ add_child(addr)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -4,22 +4,79 @@ module AdfBuilder
4
4
  module Nodes
5
5
  class Vehicle < Node
6
6
  validates_inclusion_of :status, in: %i[new used]
7
+ validates_inclusion_of :interest, in: %i[buy lease sell trade-in test-drive]
8
+ validates_presence_of :year, :make, :model
7
9
 
8
- def year(value)
9
- @attributes[:year] = value
10
+ def initialize
11
+ super
12
+ @tag_name = :vehicle
13
+ @attributes[:status] = :new
14
+ @attributes[:interest] = :buy
10
15
  end
11
16
 
12
- def make(value)
13
- @attributes[:make] = value
17
+ # Simple Text Elements (Singular)
18
+ # Simple Text Elements (Singular)
19
+ %i[year make model vin stock trim doors bodystyle transmission pricecomments comments].each do |tag|
20
+ define_method(tag) do |value|
21
+ remove_children(tag)
22
+ add_child(GenericNode.new(tag, {}, value))
23
+ end
14
24
  end
15
25
 
16
- def model(value)
17
- @attributes[:model] = value
26
+ def condition(value)
27
+ remove_children(:condition)
28
+ add_child(Condition.new(value))
29
+ end
30
+
31
+ def interest(value)
32
+ @attributes[:interest] = value
18
33
  end
19
34
 
20
35
  def status(value)
21
36
  @attributes[:status] = value
22
37
  end
38
+
39
+ # Complex Elements
40
+ def id(value, sequence: nil, source: nil)
41
+ # id* is multiple, so just add
42
+ add_child(Id.new(value, sequence: sequence, source: source))
43
+ end
44
+
45
+ def odometer(value, status: nil, units: nil)
46
+ remove_children(:odometer)
47
+ add_child(Odometer.new(value, status: status, units: units))
48
+ end
49
+
50
+ def imagetag(value, width: nil, height: nil, alttext: nil)
51
+ remove_children(:imagetag)
52
+ add_child(ImageTag.new(value, width: width, height: height, alttext: alttext))
53
+ end
54
+
55
+ def price(value, **attrs)
56
+ remove_children(:price)
57
+ add_child(Price.new(value, **attrs))
58
+ end
59
+
60
+ def option(&block)
61
+ # option* is multiple
62
+ opt = Option.new
63
+ opt.instance_eval(&block) if block_given?
64
+ add_child(opt)
65
+ end
66
+
67
+ def finance(&block)
68
+ remove_children(:finance)
69
+ fin = Finance.new
70
+ fin.instance_eval(&block) if block_given?
71
+ add_child(fin)
72
+ end
73
+
74
+ def colorcombination(&block)
75
+ # colorcombination* is multiple
76
+ cc = ColorCombination.new
77
+ cc.instance_eval(&block) if block_given?
78
+ add_child(cc)
79
+ end
23
80
  end
24
81
  end
25
82
  end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AdfBuilder
4
+ module Nodes
5
+ class Odometer < Node
6
+ validates_inclusion_of :status, in: %i[unknown rolledover replaced original]
7
+ validates_inclusion_of :units, in: %i[km mi]
8
+
9
+ def initialize(value, status: nil, units: nil)
10
+ super()
11
+ @tag_name = :odometer
12
+ @value = value
13
+ @attributes[:status] = status if status
14
+ @attributes[:units] = units if units
15
+ end
16
+ end
17
+
18
+ class ImageTag < Node
19
+ def initialize(value, width: nil, height: nil, alttext: nil)
20
+ super()
21
+ @tag_name = :imagetag
22
+ @value = value
23
+ @attributes[:width] = width if width
24
+ @attributes[:height] = height if height
25
+ @attributes[:alttext] = alttext if alttext
26
+ end
27
+ end
28
+
29
+ class Condition < Node
30
+ VALID_VALUES = %w[excellent good fair poor unknown].freeze
31
+
32
+ def initialize(value)
33
+ super()
34
+ @tag_name = :condition
35
+ unless VALID_VALUES.include?(value.to_s.downcase)
36
+ raise AdfBuilder::Error, "Invalid condition: #{value}. Must be one of: #{VALID_VALUES.join(", ")}"
37
+ end
38
+
39
+ @value = value
40
+ end
41
+ end
42
+
43
+ class Weighting < Node
44
+ def initialize(value)
45
+ super()
46
+ @tag_name = :weighting
47
+ int_val = value.to_i
48
+ unless int_val.between?(-100, 100)
49
+ raise AdfBuilder::Error, "Weighting must be between -100 and 100. Got: #{value}"
50
+ end
51
+
52
+ @value = value
53
+ end
54
+ end
55
+
56
+ class FinanceMethod < Node
57
+ VALID_VALUES = %w[cash finance lease].freeze
58
+
59
+ def initialize(value)
60
+ super()
61
+ @tag_name = :method
62
+ unless VALID_VALUES.include?(value.to_s.downcase)
63
+ raise AdfBuilder::Error, "Invalid finance method: #{value}. Must be one of: #{VALID_VALUES.join(", ")}"
64
+ end
65
+
66
+ @value = value
67
+ end
68
+ end
69
+
70
+ class Price < Node
71
+ validates_inclusion_of :type, in: %i[quote offer msrp invoice call appraisal asking]
72
+ validates_inclusion_of :delta, in: %i[absolute relative percentage]
73
+ validates_inclusion_of :relativeto, in: %i[msrp invoice]
74
+
75
+ def initialize(value, type: :quote, currency: nil, delta: nil, relativeto: nil, source: nil)
76
+ super()
77
+ @tag_name = :price
78
+ @value = value
79
+ @attributes[:type] = type
80
+ @attributes[:currency] = currency if currency
81
+ @attributes[:delta] = delta if delta
82
+ @attributes[:relativeto] = relativeto if relativeto
83
+ @attributes[:source] = source if source
84
+ end
85
+ end
86
+
87
+ class Amount < Node
88
+ validates_inclusion_of :type, in: %i[downpayment monthly total]
89
+ validates_inclusion_of :limit, in: %i[maximum minimum exact]
90
+
91
+ def initialize(value, type: :total, limit: :maximum, currency: nil)
92
+ super()
93
+ @tag_name = :amount
94
+ @value = value
95
+ @attributes[:type] = type
96
+ @attributes[:limit] = limit
97
+ @attributes[:currency] = currency if currency
98
+ end
99
+ end
100
+
101
+ class Balance < Node
102
+ validates_inclusion_of :type, in: %i[finance residual]
103
+
104
+ def initialize(value, type: :finance, currency: nil)
105
+ super()
106
+ @tag_name = :balance
107
+ @value = value
108
+ @attributes[:type] = type
109
+ @attributes[:currency] = currency if currency
110
+ end
111
+ end
112
+
113
+ class Finance < Node
114
+ def initialize
115
+ super
116
+ @tag_name = :finance
117
+ end
118
+
119
+ def method(value)
120
+ remove_children(:method)
121
+ add_child(FinanceMethod.new(value))
122
+ end
123
+
124
+ def amount(value, type: :total, limit: :maximum, currency: nil)
125
+ add_child(Amount.new(value, type: type, limit: limit, currency: currency))
126
+ end
127
+
128
+ def balance(value, type: :finance, currency: nil)
129
+ add_child(Balance.new(value, type: type, currency: currency))
130
+ end
131
+ end
132
+
133
+ class Option < Node
134
+ def initialize
135
+ super
136
+ @tag_name = :option
137
+ end
138
+
139
+ def optionname(value)
140
+ add_child(GenericNode.new(:optionname, {}, value))
141
+ end
142
+
143
+ def manufacturercode(value)
144
+ add_child(GenericNode.new(:manufacturercode, {}, value))
145
+ end
146
+
147
+ def stock(value)
148
+ add_child(GenericNode.new(:stock, {}, value))
149
+ end
150
+
151
+ def weighting(value)
152
+ add_child(Weighting.new(value))
153
+ end
154
+
155
+ def price(value, **attrs)
156
+ add_child(Price.new(value, **attrs))
157
+ end
158
+ end
159
+
160
+ class ColorCombination < Node
161
+ def initialize
162
+ super
163
+ @tag_name = :colorcombination
164
+ end
165
+
166
+ def interiorcolor(value)
167
+ add_child(GenericNode.new(:interiorcolor, {}, value))
168
+ end
169
+
170
+ def exteriorcolor(value)
171
+ add_child(GenericNode.new(:exteriorcolor, {}, value))
172
+ end
173
+
174
+ def preference(value)
175
+ add_child(GenericNode.new(:preference, {}, value))
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AdfBuilder
4
+ module Nodes
5
+ class Vendor < Node
6
+ def initialize
7
+ super
8
+ @tag_name = :vendor
9
+ end
10
+
11
+ def id(value, sequence: nil, source: nil)
12
+ add_child(Id.new(value, sequence: sequence, source: source))
13
+ end
14
+
15
+ def vendorname(value)
16
+ remove_children(:vendorname)
17
+ add_child(GenericNode.new(:vendorname, {}, value))
18
+ end
19
+
20
+ def url(value)
21
+ remove_children(:url)
22
+ add_child(GenericNode.new(:url, {}, value))
23
+ end
24
+
25
+ def contact(primary_contact: false, &block)
26
+ remove_children(:contact)
27
+ contact = Contact.new(primary_contact: primary_contact)
28
+ contact.instance_eval(&block) if block_given?
29
+ add_child(contact)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -4,8 +4,10 @@ require "ox"
4
4
 
5
5
  module AdfBuilder
6
6
  class Serializer
7
- def self.to_xml(node)
8
- new(node).to_xml
7
+ class << self
8
+ def to_xml(root_node)
9
+ new(root_node).to_xml
10
+ end
9
11
  end
10
12
 
11
13
  def initialize(root_node)
@@ -13,7 +15,10 @@ module AdfBuilder
13
15
  end
14
16
 
15
17
  def to_xml
16
- doc = Ox::Document.new
18
+ # Validate the entire tree before serializing to ensure data integrity
19
+ @root_node.validate!
20
+
21
+ doc = Ox::Document.new(version: "1.0")
17
22
 
18
23
  # XML Instruction
19
24
  instruct = Ox::Instruct.new(:xml)
@@ -41,7 +46,7 @@ module AdfBuilder
41
46
  def serialize_node(node, parent_element)
42
47
  # Determine element name using tag_name to avoid conflict with DSL methods like 'name'
43
48
  # Safest check: Does it have the instance variable set?
44
- element_name = if node.instance_variable_defined?(:@tag_name)
49
+ element_name = if node.instance_variable_defined?(:@tag_name) && node.instance_variable_get(:@tag_name)
45
50
  node.instance_variable_get(:@tag_name).to_s
46
51
  else
47
52
  node.class.name.split("::").last.downcase
@@ -75,7 +80,10 @@ module AdfBuilder
75
80
  end
76
81
 
77
82
  def attribute?(key)
78
- %i[part type status sequence source id valid preferredcontact time].include?(key)
83
+ %i[
84
+ part type status sequence source id valid preferredcontact time
85
+ interest units width height alttext limit currency delta relativeto line
86
+ ].include?(key)
79
87
  end
80
88
  end
81
89
  end