metasploit_data_models 0.18.1-java → 0.19.0-java

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/app/models/mdm/host.rb +7 -0
  3. data/app/models/mdm/service.rb +30 -1
  4. data/app/models/mdm/tag.rb +10 -0
  5. data/app/models/metasploit_data_models/ip_address/v4/cidr.rb +14 -0
  6. data/app/models/metasploit_data_models/ip_address/v4/nmap.rb +14 -0
  7. data/app/models/metasploit_data_models/ip_address/v4/range.rb +12 -0
  8. data/app/models/metasploit_data_models/ip_address/v4/segment/nmap/list.rb +126 -0
  9. data/app/models/metasploit_data_models/ip_address/v4/segment/nmap/range.rb +12 -0
  10. data/app/models/metasploit_data_models/ip_address/v4/segment/single.rb +123 -0
  11. data/app/models/metasploit_data_models/ip_address/v4/segmented.rb +200 -0
  12. data/app/models/metasploit_data_models/ip_address/v4/single.rb +53 -0
  13. data/app/models/metasploit_data_models/search/operation/ip_address.rb +60 -0
  14. data/app/models/metasploit_data_models/search/operator/ip_address.rb +33 -0
  15. data/app/models/metasploit_data_models/search/visitor/attribute.rb +1 -0
  16. data/app/models/metasploit_data_models/search/visitor/includes.rb +1 -0
  17. data/app/models/metasploit_data_models/search/visitor/joins.rb +1 -0
  18. data/app/models/metasploit_data_models/search/visitor/where.rb +51 -0
  19. data/config/locales/en.yml +35 -4
  20. data/lib/metasploit_data_models/ip_address.rb +5 -0
  21. data/lib/metasploit_data_models/ip_address/cidr.rb +174 -0
  22. data/lib/metasploit_data_models/ip_address/range.rb +181 -0
  23. data/lib/metasploit_data_models/match/child.rb +48 -0
  24. data/lib/metasploit_data_models/match/parent.rb +103 -0
  25. data/lib/metasploit_data_models/version.rb +4 -4
  26. data/metasploit_data_models.gemspec +2 -1
  27. data/spec/app/models/mdm/cred_spec.rb +164 -31
  28. data/spec/app/models/mdm/service_spec.rb +33 -44
  29. data/spec/app/models/metasploit_data_models/ip_address/v4/cidr_spec.rb +121 -0
  30. data/spec/app/models/metasploit_data_models/ip_address/v4/nmap_spec.rb +151 -0
  31. data/spec/app/models/metasploit_data_models/ip_address/v4/range_spec.rb +300 -0
  32. data/spec/app/models/metasploit_data_models/ip_address/v4/segment/nmap/list_spec.rb +278 -0
  33. data/spec/app/models/metasploit_data_models/ip_address/v4/segment/nmap/range_spec.rb +304 -0
  34. data/spec/app/models/metasploit_data_models/ip_address/v4/segment/segmented_spec.rb +29 -0
  35. data/spec/app/models/metasploit_data_models/ip_address/v4/segment/single_spec.rb +315 -0
  36. data/spec/app/models/metasploit_data_models/ip_address/v4/single_spec.rb +183 -0
  37. data/spec/app/models/metasploit_data_models/search/operation/ip_address_spec.rb +182 -0
  38. data/spec/app/models/metasploit_data_models/search/operator/ip_address_spec.rb +19 -0
  39. data/spec/app/models/metasploit_data_models/search/visitor/relation_spec.rb +229 -36
  40. data/spec/dummy/config/application.rb +1 -1
  41. data/spec/dummy/db/structure.sql +3011 -0
  42. data/spec/factories/mdm/services.rb +3 -1
  43. data/spec/lib/metasploit_data_models/ip_address/cidr_spec.rb +350 -0
  44. data/spec/lib/metasploit_data_models/ip_address/range_spec.rb +77 -0
  45. data/spec/lib/metasploit_data_models/match/child_spec.rb +61 -0
  46. data/spec/lib/metasploit_data_models/match/parent_spec.rb +155 -0
  47. data/spec/support/matchers/match_regex_exactly.rb +28 -0
  48. data/spec/support/shared/contexts/rex/text.rb +15 -0
  49. data/spec/support/shared/examples/metasploit_data_models/search/operation/ipaddress/match.rb +109 -0
  50. metadata +58 -9
  51. data/spec/dummy/db/schema.rb +0 -609
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b7be5dc8c400b0c4e171151e3791f9d5d2151398
4
- data.tar.gz: 5ebfc43baa37dca8de4022cf1ed924143e96d44b
3
+ metadata.gz: 845f009f4823f85118e65b70b314ee5567bf176d
4
+ data.tar.gz: 946f8b9169447c94e8b31dbdb2dc826d5aeaf87d
5
5
  SHA512:
6
- metadata.gz: 877b157502eb0805f7bda613cf4457ab1108fec4dc09a12f627a0be6d898ca97f7f7b2105288c0ab94980bf9e6750a0bf0458dcc060992ee4ab0b7150e0c3847
7
- data.tar.gz: 41ae62017bbe000b4fdd461082dcc90e19f54e63da9e7e619e50473d9eb633d1edd3e49c4a9bce960d9bceb0997b8e99ba218245abdce77bd3e47592168e1578
6
+ metadata.gz: 525b6ff48dab490de69e576c4a636f45218fc4b1ec0e592a8aa96283456007f27885857620ccc5e5b2444887ccf065e9d5a72492ed527e2dce47bd3f9538be2d
7
+ data.tar.gz: 3bdf612d729f262791f9d4e855311da53ef13a1a60177dea3f71283a34fb484c5272eaeeb3b31f3b1d4eb65f78013d23037207a962d9119f3e1e170f7232a78b
@@ -500,6 +500,13 @@ class Mdm::Host < ActiveRecord::Base
500
500
  :os_sp
501
501
  ]
502
502
 
503
+ #
504
+ # Search Withs
505
+ #
506
+
507
+ search_with MetasploitDataModels::Search::Operator::IPAddress,
508
+ attribute: :address
509
+
503
510
  #
504
511
  # Instance Methods
505
512
  #
@@ -6,6 +6,9 @@ class Mdm::Service < ActiveRecord::Base
6
6
  # CONSTANTS
7
7
  #
8
8
 
9
+ # Valid values for {#proto}.
10
+ PROTOS = %w{tcp udp}
11
+
9
12
  # Valid values for {#state}.
10
13
  STATES = ['open', 'closed', 'filtered', 'unknown']
11
14
 
@@ -186,8 +189,14 @@ class Mdm::Service < ActiveRecord::Base
186
189
  # Search Attributes
187
190
  #
188
191
 
192
+ search_attribute :info,
193
+ type: :string
189
194
  search_attribute :name,
190
195
  type: :string
196
+ search_attribute :proto,
197
+ type: {
198
+ set: :string
199
+ }
191
200
 
192
201
  #
193
202
  # Search Withs
@@ -202,7 +211,27 @@ class Mdm::Service < ActiveRecord::Base
202
211
  numericality: {
203
212
  only_integer: true
204
213
  }
205
- validates :proto, presence: true
214
+ validates :proto,
215
+ inclusion: {
216
+ in: PROTOS
217
+ }
218
+
219
+ #
220
+ # Class Methods
221
+ #
222
+
223
+ # Set of searchable values for {#proto}.
224
+ #
225
+ # @return [Set<String>] {PROTOS} as a `Set`.
226
+ # @see Metasploit::Model::Search::Operation::Set#membership
227
+ # @see Metasploit::Model::Search::Operator::Attribute#attribute_set
228
+ def self.proto_set
229
+ @proto_set ||= Set.new(PROTOS)
230
+ end
231
+
232
+ #
233
+ # Instance Methods
234
+ #
206
235
 
207
236
  # {Mdm::Host::OperatingSystemNormalization#normalize_os Normalizes the host operating system} whenever {#info} has
208
237
  # changed.
@@ -1,4 +1,6 @@
1
1
  class Mdm::Tag < ActiveRecord::Base
2
+ include Metasploit::Model::Search
3
+
2
4
  #
3
5
  # Relations
4
6
  #
@@ -26,6 +28,14 @@ class Mdm::Tag < ActiveRecord::Base
26
28
  # @return [ActiveRecord::Relation<Mdm::Host>]
27
29
  has_many :hosts, :through => :hosts_tags, :class_name => 'Mdm::Host'
28
30
 
31
+ #
32
+ # Search
33
+ #
34
+
35
+ search_attribute :desc,
36
+ type: :string
37
+ search_attribute :name,
38
+ type: :string
29
39
 
30
40
  #
31
41
  # Validations
@@ -0,0 +1,14 @@
1
+ # An IPv4 CIDR (Classless InterDomain Routing) block composed of a
2
+ # {MetasploitDataModels::IPAddress::V4::Single IPv4} {MetasploitDataModels::IPAddress::CIDR#address address} and
3
+ # {MetasploitDataModels::IPAddress::CIDR#prefix_length prefix_length} written in the form `'a.b.c.d/prefix_length'`.
4
+ #
5
+ # @see https://en.wikipedia.org/wiki/Cidr#IPv6_CIDR_blocks
6
+ class MetasploitDataModels::IPAddress::V4::CIDR < Metasploit::Model::Base
7
+ include MetasploitDataModels::IPAddress::CIDR
8
+
9
+ #
10
+ # CIDR
11
+ #
12
+
13
+ cidr address_class: MetasploitDataModels::IPAddress::V4::Single
14
+ end
@@ -0,0 +1,14 @@
1
+ # Nmap's octet range format composed of segments of comma separated list of segment numbers and segment number ranges.
2
+ #
3
+ # @example Nmap octect range format
4
+ # # equivalent to ['1.5.6.7', '3.5.6.7', '4.5.6.7']
5
+ # '1,3-4.5.6.7'
6
+ #
7
+ # @see http://nmap.org/book/man-target-specification.html
8
+ class MetasploitDataModels::IPAddress::V4::Nmap < MetasploitDataModels::IPAddress::V4::Segmented
9
+ #
10
+ # Segments
11
+ #
12
+
13
+ segment class_name: 'MetasploitDataModels::IPAddress::V4::Segment::Nmap::List'
14
+ end
@@ -0,0 +1,12 @@
1
+ # A range of complete IPv4 addresses, separated by a `-`.
2
+ class MetasploitDataModels::IPAddress::V4::Range < Metasploit::Model::Base
3
+ extend MetasploitDataModels::Match::Child
4
+
5
+ include MetasploitDataModels::IPAddress::Range
6
+
7
+ #
8
+ # Range Extremes
9
+ #
10
+
11
+ extremes class_name: 'MetasploitDataModels::IPAddress::V4::Single'
12
+ end
@@ -0,0 +1,126 @@
1
+ # A comma separated list of {MetasploitDataModels::IPAddress::V4::Segment::Single segment numbers} and
2
+ # {MetasploitDataModels::IPAddress::V4::Segment::Nmap::Range range of segment numbers} making up one segment of
3
+ # {MetasploitDataModels::IPAddress::V4::Nmap}.
4
+ class MetasploitDataModels::IPAddress::V4::Segment::Nmap::List < Metasploit::Model::Base
5
+ extend ActiveSupport::Autoload
6
+
7
+ include MetasploitDataModels::Match::Parent
8
+
9
+ #
10
+ # CONSTANTS
11
+ #
12
+
13
+ # Either an individual {MetasploitDataModels::IPAddress::V4::Segment::Single segment number} or a
14
+ # {MetasploitDataModels::IPAddress::V4::Segment::Nmap::Range segment range}.
15
+ RANGE_OR_NUMBER_REGEXP = %r{
16
+ (?<range>#{parent::Range.regexp})
17
+ |
18
+ # range first because it contains a segment and if the range isn't first only the first part of the range will
19
+ # match.
20
+ (?<number>#{MetasploitDataModels::IPAddress::V4::Segment::Single::REGEXP})
21
+ }x
22
+ # Separator between number or ranges
23
+ SEPARATOR = ','
24
+ # Segment of an NMAP address, composed of comma separated {RANGE_OR_NUMBER_REGEXP segment numbers or ranges}.
25
+ REGEXP = /#{RANGE_OR_NUMBER_REGEXP}(#{SEPARATOR}#{RANGE_OR_NUMBER_REGEXP})*/
26
+
27
+ # Matches exactly an Nmap comma separated list of segment numbers and ranges.
28
+ MATCH_REGEXP = /\A#{REGEXP}\z/
29
+
30
+ #
31
+ # Attributes
32
+ #
33
+
34
+ # @!attribute value
35
+ # The NMAP IPv4 octect range.
36
+ #
37
+ # @return [Array<MetasploitDataModels::IPAddress::V4::Segment::Number, MetasploitDataModels::IPAddress::V4::Segment::Range>]
38
+ # number and range in the order they appeared in formatted value.
39
+ attr_reader :value
40
+
41
+ #
42
+ # Match Children
43
+ #
44
+
45
+ match_children_named %w{
46
+ MetasploitDataModels::IPAddress::V4::Segment::Single
47
+ MetasploitDataModels::IPAddress::V4::Segment::Nmap::Range
48
+ }
49
+
50
+ #
51
+ #
52
+ # Validations
53
+ #
54
+ #
55
+
56
+ #
57
+ # Method Validations
58
+ #
59
+
60
+ validate :value_elements_valid
61
+ validate :value_is_array
62
+
63
+ #
64
+ # Attribute Validations
65
+ #
66
+
67
+ validates :value,
68
+ presence: true
69
+
70
+ #
71
+ # Instance Methods
72
+ #
73
+
74
+ # @return [String]
75
+ def to_s
76
+ if value.is_a? Array
77
+ value.map(&:to_s).join(SEPARATOR)
78
+ else
79
+ value.to_s
80
+ end
81
+ end
82
+
83
+ # Set {#value} to an `Array` of segment numbers and ranges.
84
+ #
85
+ # @param formatted_value [#to_s]
86
+ # @return [Array<MetasploitDataModels::IPAddress::V4::Segment::Single, MetasploitDataModels::IPAddress::V4::Segment::Nmap::Range>] a parsed `Array` of segment numbers and ranges.
87
+ # @return [#to_s] if `formatted_value` does not match {MATCH_REGEXP}.
88
+ def value=(formatted_value)
89
+ string = formatted_value.to_s
90
+ match = MATCH_REGEXP.match(string)
91
+
92
+ if match
93
+ ranges_or_numbers = string.split(SEPARATOR)
94
+
95
+ @value = ranges_or_numbers.map { |range_or_number|
96
+ match_child(range_or_number) || range_or_number
97
+ }
98
+ else
99
+ @value = formatted_value
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ # Validates that {#value}'s elements are all valid.
106
+ #
107
+ # @return [void]
108
+ def value_elements_valid
109
+ if value.is_a? Array
110
+ value.each_with_index do |element, index|
111
+ unless element.valid?
112
+ errors.add(:value, :element, element: element, index: index)
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ # Validates that {#value} is an `Array`.
119
+ #
120
+ # @return [void]
121
+ def value_is_array
122
+ unless value.is_a? Array
123
+ errors.add(:value, :array)
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,12 @@
1
+ # A range of segment number composed of a {#begin} and {#end} segment number, separated by a `-`.
2
+ class MetasploitDataModels::IPAddress::V4::Segment::Nmap::Range < Metasploit::Model::Base
3
+ extend MetasploitDataModels::Match::Child
4
+
5
+ include MetasploitDataModels::IPAddress::Range
6
+
7
+ #
8
+ # Range Extremes
9
+ #
10
+
11
+ extremes class_name: 'MetasploitDataModels::IPAddress::V4::Segment::Single'
12
+ end
@@ -0,0 +1,123 @@
1
+ # A segment number in an IPv4 address or the
2
+ # {MetasploitDataModels::IPAddress::V4::Segment::Nmap::Range#begin} or
3
+ # {MetasploitDataModels::IPAddress::V4::Segment::Nmap::Range#send}.
4
+ class MetasploitDataModels::IPAddress::V4::Segment::Single < Metasploit::Model::Base
5
+ extend MetasploitDataModels::Match::Child
6
+
7
+ include Comparable
8
+
9
+ #
10
+ # CONSTANTS
11
+ #
12
+
13
+ # Number of bits in a IPv4 segment
14
+ BITS = 8
15
+
16
+ # Limit that {#value} can never reach
17
+ LIMIT = 1 << BITS
18
+
19
+ # Maximum segment {#value}
20
+ MAXIMUM = LIMIT - 1
21
+
22
+ # Minimum segment {#value}
23
+ MINIMUM = 0
24
+
25
+ # Regular expression for a segment (octet) of an IPv4 address in decimal dotted notation.
26
+ #
27
+ # @see http://stackoverflow.com/a/17871737/470451
28
+ REGEXP = /(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])/
29
+
30
+ #
31
+ # Attributes
32
+ #
33
+
34
+ # @!attribute value
35
+ # The segment number.
36
+ #
37
+ # @return [Integer, String]
38
+ attr_reader :value
39
+
40
+ #
41
+ # Validations
42
+ #
43
+
44
+ validates :value,
45
+ numericality: {
46
+ greater_than_or_equal_to: MINIMUM,
47
+ less_than_or_equal_to: MAXIMUM,
48
+ only_integer: true
49
+ }
50
+
51
+ #
52
+ # Class Methods
53
+ #
54
+
55
+ # (see BITS)
56
+ #
57
+ # @return [Integer] {BITS}
58
+ def self.bits
59
+ BITS
60
+ end
61
+
62
+ #
63
+ # Instance Methods
64
+ #
65
+
66
+ # Compare this segment to `other`.
67
+ #
68
+ # @param other [#value] another segent to compare against.
69
+ # @return [1] if this segment is greater than `other`.
70
+ # @return [0] if this segment is equal to `other`.
71
+ # @return [-1] if this segment is less than `other`.
72
+ def <=>(other)
73
+ value <=> other.value
74
+ end
75
+
76
+ # Full add (as in [full adder](https://en.wikipedia.org/wiki/Full_adder)) two (this segment and `other`) segments and
77
+ # a carry from the previous {#add_with_carry}.
78
+ #
79
+ # @param other [MetasploitDataModels:IPAddress::V4::Segment::Single] segment to add to this segment.
80
+ # @param carry [Integer] integer to add to this segment and other segment from a previous call to {#add_with_carry}
81
+ # for lower segments.
82
+ # @return [Array<(MetasploitDataModels::IPAddress::V4::Segment::Single, Integer)>] Array containing a proper segment
83
+ # (where {#value} is less than {LIMIT}) and a carry integer to pass to next call to {#add_with_carry}.
84
+ # @return (see #half_add)
85
+ def add_with_carry(other, carry=0)
86
+ improper_value = self.value + other.value + carry
87
+ proper_value = improper_value % LIMIT
88
+ carry = improper_value / LIMIT
89
+ segment = self.class.new(value: proper_value)
90
+
91
+ [segment, carry]
92
+ end
93
+
94
+ # The succeeding segment. Used in `Range`s when walking the `Range`.
95
+ #
96
+ # @return [MetasploitDataModels::IPAddress::V4::Segment::Single] if {#value} responds to `#succ`.
97
+ # @return [nil] otherwise
98
+ def succ
99
+ if value.respond_to? :succ
100
+ self.class.new(value: value.succ)
101
+ end
102
+ end
103
+
104
+ delegate :to_s,
105
+ to: :value
106
+
107
+ # Sets {#value} by type casting String to Integer.
108
+ #
109
+ # @param formatted_value [#to_s]
110
+ # @return [Integer] if `formatted_value` contains only an Integer#to_s
111
+ # @return [#to_s] `formatted_value` if it does not contain an Integer#to_s
112
+ def value=(formatted_value)
113
+ @value_before_type_cast = formatted_value
114
+
115
+ begin
116
+ # use Integer() instead of String#to_i as String#to_i will ignore trailing letters (i.e. '1two' -> 1) and turn all
117
+ # string without an integer in it to 0.
118
+ @value = Integer(formatted_value.to_s)
119
+ rescue ArgumentError
120
+ @value = formatted_value
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,200 @@
1
+ # @note {segment} must be called in subclasses to set the {segment_class_name}.
2
+ #
3
+ # An IPv4 address that is composed of {SEGMENT_COUNT 4} {#segments} separated by {SEPARATOR `'.'`}.
4
+ #
5
+ # @example Using single segments to make a single IPv4 address class
6
+ # class MetasploitDataModels::IPAddress::V4::Single < MetasploitDataModels::IPAddress::V4::Segmented
7
+ # #
8
+ # # Segments
9
+ # #
10
+ #
11
+ # segment class_name: 'MetasploitDataModels::IPAddress::V4::Segment::Single'
12
+ # end
13
+ #
14
+ class MetasploitDataModels::IPAddress::V4::Segmented < Metasploit::Model::Base
15
+ extend MetasploitDataModels::Match::Child
16
+
17
+ include Comparable
18
+
19
+ #
20
+ # CONSTANTS
21
+ #
22
+
23
+ # The number of {#segments}
24
+ SEGMENT_COUNT = 4
25
+ # Separator between segments
26
+ SEPARATOR = '.'
27
+
28
+ #
29
+ # Attributes
30
+ #
31
+
32
+ # @!attribute value
33
+ # Segments of IP address from high to low.
34
+ #
35
+ # @return [Array<MetasploitDataModels::IPAddress:V4::Segment::Nmap>]
36
+ attr_reader :value
37
+
38
+ #
39
+ #
40
+ # Validations
41
+ #
42
+ #
43
+
44
+ #
45
+ # Validation Methods
46
+ #
47
+
48
+ validate :segments_valid
49
+
50
+ #
51
+ # Attribute Validations
52
+ #
53
+
54
+ validates :segments,
55
+ length: {
56
+ is: SEGMENT_COUNT
57
+ }
58
+
59
+ #
60
+ # Class methods
61
+ #
62
+
63
+ # @note Call {segment} with the {segment_class_name} before calling this method, as it uses {segment_class} to look
64
+ # up the `REGEXP` of the {segment_class}.
65
+ #
66
+ # Regular expression that matches the part of a string that represents a IPv4 segmented IP address format.
67
+ #
68
+ # @return [Regexp]
69
+ def self.regexp
70
+ unless @regexp
71
+ separated_segment_count = SEGMENT_COUNT - 1
72
+
73
+ @regexp = %r{
74
+ (#{segment_class::REGEXP}#{Regexp.escape(SEPARATOR)}){#{separated_segment_count},#{separated_segment_count}}
75
+ #{segment_class::REGEXP}
76
+ }x
77
+ end
78
+
79
+ @regexp
80
+ end
81
+
82
+ # Sets up the {segment_class_name} for the subclass.
83
+ #
84
+ # @example Using {segment} to set {segment_class_name}
85
+ # segment class_name: 'MetasploitDataModels::IPAddress::V4::Segment::Single'
86
+ #
87
+ # @param options [Hash{Symbol => String}]
88
+ # @option options [String] :class_name a `Class#name` to use for {segment_class_name}.
89
+ # @return [void]
90
+ def self.segment(options={})
91
+ options.assert_valid_keys(:class_name)
92
+
93
+ @segment_class_name = options.fetch(:class_name)
94
+ end
95
+
96
+ # @note Call {segment} to set the {segment_class_name} before calling {segment_class}, which will attempt to
97
+ # String#constantize` {segment_class_name}.
98
+ #
99
+ # The `Class` used to parse each segment of the IPv4 address.
100
+ #
101
+ # @return [Class]
102
+ def self.segment_class
103
+ @segment_class = segment_class_name.constantize
104
+ end
105
+
106
+ # @note Call {segment} to set {segment_class_name}
107
+ #
108
+ # The name of {segment_class}
109
+ #
110
+ # @return [String] a `Class#name` for {segment_class}.
111
+ def self.segment_class_name
112
+ @segment_class_name
113
+ end
114
+
115
+ # (see SEGMENT_COUNT)
116
+ #
117
+ # @return [Integer]
118
+ def self.segment_count
119
+ SEGMENT_COUNT
120
+ end
121
+
122
+ #
123
+ # Instance methods
124
+ #
125
+
126
+ # Compare this segment IPv4 address to `other`.
127
+ #
128
+ # @return [1] if {#segments} are greater than {#segments} of `other`.
129
+ # @return [0] if {#segments} are equal to {#segments} of `other`.
130
+ # @return [-1] if {#segments} are less than {#segments} of `other`.
131
+ # @return [nil] if `other` isn't the same `Class`
132
+ def <=>(other)
133
+ if other.is_a? self.class
134
+ segments <=> other.segments
135
+ else
136
+ # The interface for <=> requires nil be returned if other is incomparable
137
+ nil
138
+ end
139
+ end
140
+
141
+ # Array of segments.
142
+ #
143
+ # @return [Array] if {#value} is an `Array`.
144
+ # @return [[]] if {#value} is not an `Array`.
145
+ def segments
146
+ if value.is_a? Array
147
+ value
148
+ else
149
+ []
150
+ end
151
+ end
152
+
153
+ # Set {#segments}.
154
+ #
155
+ # @param segments [Array] `Array` of {segment_class} instances
156
+ # @return [Array] `Array` of {segment_class} instances
157
+ def segments=(segments)
158
+ @value = segments
159
+ end
160
+
161
+ # Segments joined with {SEPARATOR}.
162
+ #
163
+ # @return [String]
164
+ def to_s
165
+ segments.map(&:to_s).join(SEPARATOR)
166
+ end
167
+
168
+ # @note Set {#segments} if value is not formatted, but already broken into an `Array` of {segment_class} instances.
169
+ #
170
+ # Sets {#value} by parsing its segments.
171
+ #
172
+ # @param formatted_value [#to_s]
173
+ def value=(formatted_value)
174
+ string = formatted_value.to_s
175
+ match = self.class.match_regexp.match(string)
176
+
177
+ if match
178
+ segments = string.split(SEPARATOR)
179
+
180
+ @value = segments.map { |segment|
181
+ self.class.segment_class.new(value: segment)
182
+ }
183
+ else
184
+ @value = formatted_value
185
+ end
186
+ end
187
+
188
+ private
189
+
190
+ # Validates that all segments in {#segments} are valid.
191
+ #
192
+ # @return [void]
193
+ def segments_valid
194
+ segments.each_with_index do |segment, index|
195
+ unless segment.valid?
196
+ errors.add(:segments, :segment_invalid, index: index, segment: segment)
197
+ end
198
+ end
199
+ end
200
+ end