adf_builder 0.4.0 → 1.1.0

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.
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AdfBuilder
4
+ module Nodes
5
+ class Node
6
+ include AdfBuilder::Validations
7
+
8
+ attr_reader :children, :attributes, :value, :tag_name
9
+
10
+ def initialize
11
+ @children = []
12
+ @attributes = {}
13
+ @value = nil
14
+ @tag_name = nil
15
+ end
16
+
17
+ def add_child(node)
18
+ @children << node
19
+ end
20
+
21
+ def remove_children(tag_name)
22
+ @children.reject! { |c| c.tag_name == tag_name }
23
+ end
24
+
25
+ def to_xml
26
+ Serializer.to_xml(self)
27
+ end
28
+
29
+ def method_missing(method_name, *args, &block)
30
+ # Support for dynamic/custom tags
31
+ # usage: custom_tag "value", attr: "val"
32
+ # usage: custom_tag { ... }
33
+
34
+ tag_name = method_name
35
+ attributes = args.last.is_a?(Hash) ? args.last : {}
36
+ value = args.first unless args.first == attributes
37
+
38
+ # If it's a block, it's a structural node
39
+ if block_given?
40
+ node = GenericNode.new(tag_name, attributes)
41
+ node.instance_eval(&block)
42
+ add_child(node)
43
+ # If it has a value, it's a leaf node/element
44
+ elsif value
45
+ node = GenericNode.new(tag_name, attributes, value)
46
+ add_child(node)
47
+ else
48
+ # Just a tag with attributes? e.g. <flag active="true"/>
49
+ node = GenericNode.new(tag_name, attributes)
50
+ add_child(node)
51
+ end
52
+ end
53
+
54
+ def respond_to_missing?(method_name, include_private = false)
55
+ true
56
+ end
57
+ end
58
+
59
+ class GenericNode < Node
60
+ def initialize(tag_name, attributes = {}, value = nil)
61
+ super()
62
+ @tag_name = tag_name
63
+ @attributes = attributes
64
+ @value = value
65
+ end
66
+ end
67
+
68
+ class Root < Node
69
+ # The root context of the builder
70
+ def prospect(&block)
71
+ prospect = Prospect.new
72
+ prospect.instance_eval(&block) if block_given?
73
+ add_child(prospect)
74
+ end
75
+
76
+ def first_prospect
77
+ @children.find { |c| c.is_a?(Prospect) }
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AdfBuilder
4
+ module Nodes
5
+ class Prospect < Node
6
+ def request_date(date)
7
+ @attributes[:requestdate] = date
8
+ end
9
+
10
+ def vehicle(&block)
11
+ vehicle = Vehicle.new
12
+ vehicle.instance_eval(&block) if block_given?
13
+ add_child(vehicle)
14
+ end
15
+
16
+ def customer(&block)
17
+ customer = Customer.new
18
+ customer.instance_eval(&block) if block_given?
19
+ add_child(customer)
20
+ end
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
+
34
+ # Helpers for Editing
35
+ def vehicles
36
+ @children.select { |c| c.is_a?(Vehicle) }
37
+ end
38
+
39
+ def customers
40
+ @children.select { |c| c.is_a?(Customer) }
41
+ end
42
+ end
43
+ end
44
+ end
@@ -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,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AdfBuilder
4
+ module Nodes
5
+ class Id < Node
6
+ def initialize(value, sequence: nil, source: nil)
7
+ super()
8
+ @tag_name = :id
9
+ @value = value
10
+ @attributes[:sequence] = sequence if sequence
11
+ @attributes[:source] = source if source
12
+ end
13
+ end
14
+
15
+ class Phone < Node
16
+ def initialize(value, type: nil, time: nil, preferredcontact: nil)
17
+ super()
18
+ @tag_name = :phone
19
+ @value = value
20
+ @attributes[:type] = type if type
21
+ @attributes[:time] = time if time
22
+ @attributes[:preferredcontact] = preferredcontact if preferredcontact
23
+ end
24
+ end
25
+
26
+ class Email < Node
27
+ def initialize(value, preferredcontact: nil)
28
+ super()
29
+ @tag_name = :email
30
+ @value = value
31
+ @attributes[:preferredcontact] = preferredcontact if preferredcontact
32
+ end
33
+ end
34
+
35
+ class Name < Node
36
+ def initialize(value, part: nil, type: nil)
37
+ super()
38
+ @tag_name = :name
39
+ @value = value
40
+ @attributes[:part] = part if part
41
+ @attributes[:type] = type if type
42
+ end
43
+ end
44
+
45
+ class Address < Node
46
+ def initialize(type: nil)
47
+ super()
48
+ @tag_name = :address
49
+ @attributes[:type] = type if type
50
+ end
51
+
52
+ def street(value, line: nil)
53
+ node = GenericNode.new(:street, { line: line }.compact, value)
54
+ add_child(node)
55
+ end
56
+
57
+ # Simple elements
58
+ %i[apartment city regioncode postalcode country].each do |tag|
59
+ define_method(tag) do |value|
60
+ add_child(GenericNode.new(tag, {}, value))
61
+ end
62
+ end
63
+ end
64
+
65
+ class Contact < Node
66
+ def initialize(primary_contact: false)
67
+ super()
68
+ @tag_name = :contact
69
+ # primary_contact might be useful for logic but not an attribute
70
+ end
71
+
72
+ def name(value, part: nil, type: nil)
73
+ add_child(Name.new(value, part: part, type: type))
74
+ end
75
+
76
+ def email(value, preferredcontact: nil)
77
+ add_child(Email.new(value, preferredcontact: preferredcontact))
78
+ end
79
+
80
+ def phone(value, type: nil, time: nil, preferredcontact: nil)
81
+ add_child(Phone.new(value, type: type, time: time, preferredcontact: preferredcontact))
82
+ end
83
+
84
+ def address(type: nil, &block)
85
+ addr = Address.new(type: type)
86
+ addr.instance_eval(&block) if block_given?
87
+ add_child(addr)
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AdfBuilder
4
+ module Nodes
5
+ class Vehicle < Node
6
+ validates_inclusion_of :status, in: %i[new used]
7
+ validates_inclusion_of :interest, in: %i[buy lease sell trade-in test-drive]
8
+
9
+ def initialize
10
+ super
11
+ @tag_name = :vehicle
12
+ @attributes[:status] = :new
13
+ @attributes[:interest] = :buy
14
+ end
15
+
16
+ # Simple Text Elements (Singular)
17
+ %i[year make model vin stock trim doors bodystyle transmission condition pricecomments comments].each do |tag|
18
+ define_method(tag) do |value|
19
+ remove_children(tag)
20
+ add_child(GenericNode.new(tag, {}, value))
21
+ end
22
+ end
23
+
24
+ def interest(value)
25
+ @attributes[:interest] = value
26
+ end
27
+
28
+ def status(value)
29
+ @attributes[:status] = value
30
+ end
31
+
32
+ # Complex Elements
33
+ def id(value, sequence: nil, source: nil)
34
+ # id* is multiple, so just add
35
+ add_child(Id.new(value, sequence: sequence, source: source))
36
+ end
37
+
38
+ def odometer(value, status: nil, units: nil)
39
+ remove_children(:odometer)
40
+ add_child(Odometer.new(value, status: status, units: units))
41
+ end
42
+
43
+ def imagetag(value, width: nil, height: nil, alttext: nil)
44
+ remove_children(:imagetag)
45
+ add_child(ImageTag.new(value, width: width, height: height, alttext: alttext))
46
+ end
47
+
48
+ def price(value, **attrs)
49
+ remove_children(:price)
50
+ add_child(Price.new(value, **attrs))
51
+ end
52
+
53
+ def option(&block)
54
+ # option* is multiple
55
+ opt = Option.new
56
+ opt.instance_eval(&block) if block_given?
57
+ add_child(opt)
58
+ end
59
+
60
+ def finance(&block)
61
+ remove_children(:finance)
62
+ fin = Finance.new
63
+ fin.instance_eval(&block) if block_given?
64
+ add_child(fin)
65
+ end
66
+
67
+ def colorcombination(&block)
68
+ # colorcombination* is multiple
69
+ cc = ColorCombination.new
70
+ cc.instance_eval(&block) if block_given?
71
+ add_child(cc)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,137 @@
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 Price < Node
30
+ validates_inclusion_of :type, in: %i[quote offer msrp invoice call appraisal asking]
31
+ validates_inclusion_of :delta, in: %i[absolute relative percentage]
32
+ validates_inclusion_of :relativeto, in: %i[msrp invoice]
33
+
34
+ def initialize(value, type: :quote, currency: nil, delta: nil, relativeto: nil, source: nil)
35
+ super()
36
+ @tag_name = :price
37
+ @value = value
38
+ @attributes[:type] = type
39
+ @attributes[:currency] = currency if currency
40
+ @attributes[:delta] = delta if delta
41
+ @attributes[:relativeto] = relativeto if relativeto
42
+ @attributes[:source] = source if source
43
+ end
44
+ end
45
+
46
+ class Amount < Node
47
+ validates_inclusion_of :type, in: %i[downpayment monthly total]
48
+ validates_inclusion_of :limit, in: %i[maximum minimum exact]
49
+
50
+ def initialize(value, type: :total, limit: :maximum, currency: nil)
51
+ super()
52
+ @tag_name = :amount
53
+ @value = value
54
+ @attributes[:type] = type
55
+ @attributes[:limit] = limit
56
+ @attributes[:currency] = currency if currency
57
+ end
58
+ end
59
+
60
+ class Balance < Node
61
+ validates_inclusion_of :type, in: %i[finance residual]
62
+
63
+ def initialize(value, type: :finance, currency: nil)
64
+ super()
65
+ @tag_name = :balance
66
+ @value = value
67
+ @attributes[:type] = type
68
+ @attributes[:currency] = currency if currency
69
+ end
70
+ end
71
+
72
+ class Finance < Node
73
+ def initialize
74
+ super
75
+ @tag_name = :finance
76
+ end
77
+
78
+ def method(value)
79
+ add_child(GenericNode.new(:method, {}, value))
80
+ end
81
+
82
+ def amount(value, type: :total, limit: :maximum, currency: nil)
83
+ add_child(Amount.new(value, type: type, limit: limit, currency: currency))
84
+ end
85
+
86
+ def balance(value, type: :finance, currency: nil)
87
+ add_child(Balance.new(value, type: type, currency: currency))
88
+ end
89
+ end
90
+
91
+ class Option < Node
92
+ def initialize
93
+ super
94
+ @tag_name = :option
95
+ end
96
+
97
+ def optionname(value)
98
+ add_child(GenericNode.new(:optionname, {}, value))
99
+ end
100
+
101
+ def manufacturercode(value)
102
+ add_child(GenericNode.new(:manufacturercode, {}, value))
103
+ end
104
+
105
+ def stock(value)
106
+ add_child(GenericNode.new(:stock, {}, value))
107
+ end
108
+
109
+ def weighting(value)
110
+ add_child(GenericNode.new(:weighting, {}, value))
111
+ end
112
+
113
+ def price(value, **attrs)
114
+ add_child(Price.new(value, **attrs))
115
+ end
116
+ end
117
+
118
+ class ColorCombination < Node
119
+ def initialize
120
+ super
121
+ @tag_name = :colorcombination
122
+ end
123
+
124
+ def interiorcolor(value)
125
+ add_child(GenericNode.new(:interiorcolor, {}, value))
126
+ end
127
+
128
+ def exteriorcolor(value)
129
+ add_child(GenericNode.new(:exteriorcolor, {}, value))
130
+ end
131
+
132
+ def preference(value)
133
+ add_child(GenericNode.new(:preference, {}, value))
134
+ end
135
+ end
136
+ end
137
+ 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
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ox"
4
+
5
+ module AdfBuilder
6
+ class Serializer
7
+ def self.to_xml(node)
8
+ new(node).to_xml
9
+ end
10
+
11
+ def initialize(root_node)
12
+ @root_node = root_node
13
+ end
14
+
15
+ def to_xml
16
+ doc = Ox::Document.new
17
+
18
+ # XML Instruction
19
+ instruct = Ox::Instruct.new(:xml)
20
+ instruct[:version] = "1.0"
21
+ doc << instruct
22
+
23
+ # ADF Instruction
24
+ adf_instruct = Ox::Instruct.new("ADF")
25
+ adf_instruct[:version] = "1.0"
26
+ doc << adf_instruct
27
+
28
+ # ADF Root Element
29
+ adf = Ox::Element.new("adf")
30
+ doc << adf
31
+
32
+ @root_node.children.each do |child|
33
+ serialize_node(child, adf)
34
+ end
35
+
36
+ Ox.dump(doc)
37
+ end
38
+
39
+ private
40
+
41
+ def serialize_node(node, parent_element)
42
+ # Determine element name using tag_name to avoid conflict with DSL methods like 'name'
43
+ # Safest check: Does it have the instance variable set?
44
+ element_name = if node.instance_variable_defined?(:@tag_name) && node.instance_variable_get(:@tag_name)
45
+ node.instance_variable_get(:@tag_name).to_s
46
+ else
47
+ node.class.name.split("::").last.downcase
48
+ end
49
+
50
+ # puts "DEBUG: Serializing #{node.class} as <#{element_name}>"
51
+
52
+ element = Ox::Element.new(element_name)
53
+
54
+ # Add attributes (XML attributes) or Simple Child Elements
55
+ node.attributes.each do |key, value|
56
+ if attribute?(key)
57
+ element[key] = value.to_s
58
+ else
59
+ # It's a simple child element
60
+ child_el = Ox::Element.new(key.to_s)
61
+ child_el << value.to_s
62
+ element << child_el
63
+ end
64
+ end
65
+
66
+ # Add Node-level Text Content (e.g. <name>John</name>)
67
+ element << node.value.to_s if node.respond_to?(:value) && node.value
68
+
69
+ # Recursively add children nodes
70
+ node.children.each do |child|
71
+ serialize_node(child, element)
72
+ end
73
+
74
+ parent_element << element
75
+ end
76
+
77
+ def attribute?(key)
78
+ %i[
79
+ part type status sequence source id valid preferredcontact time
80
+ interest units width height alttext limit currency delta relativeto line
81
+ ].include?(key)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AdfBuilder
4
+ module Validations
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ def validates_inclusion_of(attribute, in:)
11
+ @validations ||= []
12
+ @validations << { type: :inclusion, attribute: attribute, in: binding.local_variable_get(:in) }
13
+ end
14
+
15
+ def validations
16
+ @validations || []
17
+ end
18
+ end
19
+
20
+ def validate!
21
+ self.class.validations.each do |validation|
22
+ value = @attributes[validation[:attribute]]
23
+ next if value.nil? # Allow nil unless presence validation is added
24
+
25
+ next unless validation[:type] == :inclusion
26
+
27
+ allowed = validation[:in]
28
+ unless allowed.include?(value)
29
+ raise AdfBuilder::Error,
30
+ "Invalid value for #{validation[:attribute]}: #{value}. Allowed: #{allowed.join(", ")}"
31
+ end
32
+ end
33
+
34
+ # Recursively validate children
35
+ @children.each(&:validate!)
36
+ end
37
+ end
38
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AdfBuilder
4
- VERSION = "0.4.0"
4
+ VERSION = "1.1.0"
5
5
  end