aixm 1.2.1 → 1.3.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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +14 -3
  4. data/README.md +30 -2
  5. data/exe/ckmid +1 -7
  6. data/exe/mkmid +1 -7
  7. data/lib/aixm/classes.rb +2 -1
  8. data/lib/aixm/component/address.rb +12 -15
  9. data/lib/aixm/component/approach_lighting.rb +11 -16
  10. data/lib/aixm/component/fato.rb +22 -34
  11. data/lib/aixm/component/frequency.rb +10 -15
  12. data/lib/aixm/component/geometry/arc.rb +2 -3
  13. data/lib/aixm/component/geometry/border.rb +6 -10
  14. data/lib/aixm/component/geometry/circle.rb +4 -4
  15. data/lib/aixm/component/geometry/point.rb +4 -4
  16. data/lib/aixm/component/geometry/rhumb_line.rb +4 -4
  17. data/lib/aixm/component/geometry.rb +4 -4
  18. data/lib/aixm/component/helipad.rb +13 -20
  19. data/lib/aixm/component/layer.rb +6 -8
  20. data/lib/aixm/component/lighting.rb +12 -17
  21. data/lib/aixm/component/runway.rb +26 -38
  22. data/lib/aixm/component/service.rb +12 -16
  23. data/lib/aixm/component/surface.rb +8 -10
  24. data/lib/aixm/component/timesheet.rb +9 -10
  25. data/lib/aixm/component/timetable.rb +6 -7
  26. data/lib/aixm/component/vasis.rb +6 -8
  27. data/lib/aixm/component/vertical_limit.rb +8 -8
  28. data/lib/aixm/component.rb +3 -2
  29. data/lib/aixm/concerns/association.rb +381 -0
  30. data/lib/aixm/concerns/memoize.rb +107 -0
  31. data/lib/aixm/concerns/xml_builder.rb +34 -0
  32. data/lib/aixm/document.rb +52 -21
  33. data/lib/aixm/feature/airport.rb +44 -47
  34. data/lib/aixm/feature/airspace.rb +27 -34
  35. data/lib/aixm/feature/generic.rb +67 -0
  36. data/lib/aixm/feature/navigational_aid/designated_point.rb +11 -13
  37. data/lib/aixm/feature/navigational_aid/dme.rb +12 -15
  38. data/lib/aixm/feature/navigational_aid/marker.rb +12 -15
  39. data/lib/aixm/feature/navigational_aid/ndb.rb +13 -16
  40. data/lib/aixm/feature/navigational_aid/tacan.rb +15 -17
  41. data/lib/aixm/feature/navigational_aid/vor.rb +16 -19
  42. data/lib/aixm/feature/navigational_aid.rb +7 -7
  43. data/lib/aixm/feature/obstacle.rb +20 -21
  44. data/lib/aixm/feature/obstacle_group.rb +19 -20
  45. data/lib/aixm/feature/organisation.rb +11 -12
  46. data/lib/aixm/feature/unit.rb +16 -18
  47. data/lib/aixm/feature.rb +26 -7
  48. data/lib/aixm/object.rb +1 -1
  49. data/lib/aixm/refinements.rb +57 -0
  50. data/lib/aixm/version.rb +1 -1
  51. data/lib/aixm.rb +4 -3
  52. data/schemas/ofmx/0.1/OFMX-Snapshot.xsd +6 -1
  53. data.tar.gz.sig +3 -3
  54. metadata +7 -19
  55. metadata.gz.sig +0 -0
  56. data/lib/aixm/association.rb +0 -378
  57. data/lib/aixm/memoize.rb +0 -105
@@ -0,0 +1,381 @@
1
+ using AIXM::Refinements
2
+
3
+ module AIXM
4
+ module Concerns
5
+
6
+ # Associate features and components with a minimalistic implementation of
7
+ # +has_many+, +has_one+ and +belongs_to+ associations.
8
+ #
9
+ # When adding or assigning an object on the associator (where the +has_many+
10
+ # or +has_one+ declaration is made), the object is verified and must be an
11
+ # instance of the declared class or a superclass thereof.
12
+ #
13
+ # When assigning an object on the associated (where the +belongs_to+
14
+ # declaration is made), the object is not verified. However, since the actual
15
+ # assignment is always delegated to the associator, unacceptable objects will
16
+ # raise errors.
17
+ #
18
+ # @example Simple +has_many+ association
19
+ # class Blog
20
+ # has_many :posts # :post has to be a key in AIXM::CLASSES
21
+ # end
22
+ # class Post
23
+ # belongs_to :blog
24
+ # end
25
+ # blog, post = Blog.new, Post.new
26
+ # # --either--
27
+ # blog.add_post(post) # => Blog
28
+ # blog.posts.count # => 1
29
+ # blog.posts.first == post # => true
30
+ # post.blog == blog # => true
31
+ # blog.remove_post(post) # => Blog
32
+ # blog.posts.count # => 0
33
+ # # --or--
34
+ # post.blog = blog # => Blog
35
+ # blog.posts.count # => 1
36
+ # blog.posts.first == post # => true
37
+ # post.blog == blog # => true
38
+ # post.blog = nil # => nil
39
+ # blog.posts.count # => 0
40
+ # # --or--
41
+ # post_2 = Post.new
42
+ # blog.add_posts([post, post_2])
43
+ # blog.posts.count # => 2
44
+ # blog.posts == [post, post_2] # => true
45
+ # blog.remove_posts([post_2, post])
46
+ # blog.posts.count # => 0
47
+ #
48
+ # @example Simple +has_one+ association
49
+ # class Blog
50
+ # has_one :posts # :post has to be a key in AIXM::CLASSES
51
+ # end
52
+ # class Post
53
+ # belongs_to :blog
54
+ # end
55
+ # blog, post = Blog.new, Post.new
56
+ # # --either--
57
+ # blog.post = post # => Post (standard assignment)
58
+ # blog.add_post(post) # => Blog (alternative for chaining)
59
+ # blog.post == post # => true
60
+ # post.blog == blog # => true
61
+ # blog.post = nil # => nil
62
+ # blog.post # => nil
63
+ # post.blog # => nil
64
+ # # --or--
65
+ # post.blog = blog # => Blog (standard assignment)
66
+ # post.add_blog(blog) # => Post (alternative for chaining)
67
+ # post.blog == blog # => true
68
+ # blog.post == post # => true
69
+ # post.blog = nil # => nil
70
+ # post.blog # => nil
71
+ # blog.post # => nil
72
+ #
73
+ # @example Association with readonly +belongs_to+ (idem for +has_one+)
74
+ # class Blog
75
+ # has_many :posts # :post has to be a key in AIXM::CLASSES
76
+ # end
77
+ # class Post
78
+ # belongs_to :blog, readonly: true
79
+ # end
80
+ # blog, post = Blog.new, Post.new
81
+ # post.blog = blog # => NoMethodError
82
+ #
83
+ # @example Association with explicit class (idem for +has_one+)
84
+ # class Blog
85
+ # include AIXM::Concerns::Association
86
+ # has_many :posts, accept: 'Picture'
87
+ # end
88
+ # class Picture
89
+ # include AIXM::Concerns::Association
90
+ # belongs_to :blog
91
+ # end
92
+ # blog, picture = Blog.new, Picture.new
93
+ # blog.add_post(picture)
94
+ # blog.posts.first == picture # => true
95
+ #
96
+ # @example Polymorphic associator (idem for +has_one+)
97
+ # class Blog
98
+ # has_many :posts, as: :postable
99
+ # end
100
+ # class Feed
101
+ # has_many :posts, as: :postable
102
+ # end
103
+ # class Post
104
+ # belongs_to :postable
105
+ # end
106
+ # blog, feed, post_1, post_2, post_3 = Blog.new, Feed.new, Post.new, Post.new, Post.new
107
+ # blog.add_post(post_1)
108
+ # post_1.postable == blog # => true
109
+ # feed.add_post(post_2)
110
+ # post_2.postable == feed # => true
111
+ # post_3.postable = blog # => NoMethodError
112
+ #
113
+ # @example Polymorphic associated (idem for +has_one+)
114
+ # class Blog
115
+ # include AIXM::Concerns::Association
116
+ # has_many :items, accept: ['Post', :picture]
117
+ # end
118
+ # class Post
119
+ # include AIXM::Concerns::Association
120
+ # belongs_to :blog, as: :item
121
+ # end
122
+ # class Picture
123
+ # include AIXM::Concerns::Association
124
+ # belongs_to :blog, as: :item
125
+ # end
126
+ # blog, post, picture = Blog.new, Post.new, Picture.new
127
+ # blog.add_item(post)
128
+ # blog.add_item(picture)
129
+ # blog.items.count # => 2
130
+ # blog.items.first == post # => true
131
+ # blog.items.last == picture # => true
132
+ # post.blog == blog # => true
133
+ # picture.blog == blog # => true
134
+ #
135
+ # @example Add method which enriches passed associated object (+has_many+ only)
136
+ # class Blog
137
+ # has_many :posts do |post, related_to: nil| # this defines the signature of add_post
138
+ # post.related_to = related_to || @posts.last # executes in the context of the current blog
139
+ # end
140
+ # end
141
+ # class Post
142
+ # belongs_to :blog
143
+ # attr_accessor :related_to
144
+ # end
145
+ # blog, post_1, post_2, post_3 = Blog.new, Post.new, Post.new, Post.new
146
+ # blog.add_post(post_1)
147
+ # post_1.related_to # => nil
148
+ # blog.add_post(post_2)
149
+ # post_2.related_to == post_1 # => true
150
+ # blog.add_post(post_3, related_to: post_1)
151
+ # post_3.related_to == post_1 # => true
152
+ #
153
+ # @example Add method which builds and yields new associated object (+has_many+ only)
154
+ # class Blog
155
+ # include AIXM::Concerns::Association
156
+ # has_many :posts do |post, title:| end
157
+ # end
158
+ # class Post
159
+ # include AIXM::Concerns::Association
160
+ # belongs_to :blog
161
+ # attr_accessor :title, :text
162
+ # def initialize(title:) # same signature as "has_many" block above
163
+ # @title = title
164
+ # end
165
+ # end
166
+ # blog = Blog.new
167
+ # blog.add_post(title: "title") do |post| # note that no post instance is passed
168
+ # post.text = "text"
169
+ # end
170
+ # blog.posts.first.title # => "title"
171
+ # blog.posts.first.text # => "text"
172
+ module Association
173
+ module ClassMethods
174
+ attr_reader :has_many_attributes, :has_one_attributes, :belongs_to_attributes
175
+
176
+ def has_many(attribute, as: nil, accept: nil, &association_block)
177
+ association = attribute.to_s.inflect(:singularize)
178
+ inversion = as || self.to_s.inflect(:demodulize, :tableize, :singularize)
179
+ class_names = [accept || association].flatten.map { AIXM::CLASSES[_1.to_sym] || _1 }
180
+ (@has_many_attributes ||= []) << attribute
181
+ # features
182
+ define_method(attribute) do
183
+ instance_variable_get(:"@#{attribute}") || AIXM::Concerns::Association::Array.new
184
+ end
185
+ # add_feature
186
+ define_method(:"add_#{association}") do |object=nil, **options, &add_block|
187
+ unless object
188
+ fail(ArgumentError, "must pass object to add") if class_names.count > 1
189
+ object = class_names.first.to_class.new(**options)
190
+ add_block.call(object) if add_block
191
+ end
192
+ instance_exec(object, **options, &association_block) if association_block
193
+ fail(ArgumentError, "#{object.__class__} not allowed") unless class_names.any? { |c| object.is_a?(c.to_class) }
194
+ instance_eval("@#{attribute} ||= AIXM::Concerns::Association::Array.new")
195
+ send(attribute).send(:push, object)
196
+ object.instance_variable_set(:"@#{inversion}", self)
197
+ self
198
+ end
199
+ # add_features
200
+ define_method(:"add_#{attribute}") do |objects=[], **options, &add_block|
201
+ objects.each { send(:"add_#{association}", _1, **options, &add_block) }
202
+ self
203
+ end
204
+ # remove_feature
205
+ define_method(:"remove_#{association}") do |object|
206
+ send(attribute).send(:delete, object)
207
+ object.instance_variable_set(:"@#{inversion}", nil)
208
+ self
209
+ end
210
+ # remove_features
211
+ define_method(:"remove_#{attribute}") do |objects=[]|
212
+ objects.each { send(:"remove_#{association}", _1) }
213
+ self
214
+ end
215
+ end
216
+
217
+ def has_one(attribute, as: nil, accept: nil, allow_nil: false)
218
+ association = attribute.to_s
219
+ inversion = (as || self.to_s.inflect(:demodulize, :tableize, :singularize)).to_s
220
+ class_names = [accept || association].flatten.map { AIXM::CLASSES[_1.to_sym] || _1 }
221
+ class_names << 'NilClass' if allow_nil
222
+ (@has_one_attributes ||= []) << attribute
223
+ # feature
224
+ attr_reader attribute
225
+ # feature=
226
+ define_method(:"#{association}=") do |object|
227
+ fail(ArgumentError, "#{object.__class__} not allowed") unless class_names.any? { |c| object.is_a?(c.to_class) }
228
+ instance_variable_get(:"@#{attribute}")&.instance_variable_set(:"@#{inversion}", nil)
229
+ instance_variable_set(:"@#{attribute}", object)
230
+ object&.instance_variable_set(:"@#{inversion}", self)
231
+ object
232
+ end
233
+ # add_feature
234
+ define_method(:"add_#{association}") do |object|
235
+ send("#{association}=", object)
236
+ self
237
+ end
238
+ # remove_feature
239
+ define_method(:"remove_#{association}") do |_|
240
+ send(:"#{association}=", nil)
241
+ self
242
+ end
243
+ end
244
+
245
+ def belongs_to(attribute, as: nil, readonly: false)
246
+ association = self.to_s.inflect(:demodulize, :tableize, :singularize)
247
+ inversion = (as || association).to_s
248
+ (@belongs_to_attributes ||= []) << attribute
249
+ # feature
250
+ attr_reader attribute
251
+ unless readonly
252
+ # feature=
253
+ define_method(:"#{attribute}=") do |object|
254
+ instance_variable_get(:"@#{attribute}")&.send(:"remove_#{inversion}", self)
255
+ object&.send(:"add_#{inversion}", self)
256
+ object
257
+ end
258
+ # add_feature
259
+ define_method(:"add_#{attribute}") do |object|
260
+ send("#{attribute}=", object)
261
+ self
262
+ end
263
+ end
264
+ end
265
+ end
266
+
267
+ def self.included(base)
268
+ base.extend(ClassMethods)
269
+ end
270
+
271
+ class Array < ::Array
272
+ private :<<, :push, :append, :unshift, :prepend
273
+ private :delete, :pop, :shift
274
+
275
+ # Find objects of the given class and optionally with the given
276
+ # attribute values on a has_many association.
277
+ #
278
+ # The class can either be declared by passing the class itself or by
279
+ # passing a shortcut symbol as listed in +AIXM::CLASSES+.
280
+ #
281
+ # @example
282
+ # class Blog
283
+ # include AIXM::Concerns::Association
284
+ # has_many :items, accept: %i(post picture)
285
+ # end
286
+ # class Post
287
+ # include AIXM::Concerns::Association
288
+ # belongs_to :blog, as: :item
289
+ # attr_accessor :title
290
+ # end
291
+ # class Picture
292
+ # include AIXM::Concerns::Association
293
+ # belongs_to :blog, as: :item
294
+ # end
295
+ # blog, post, picture = Blog.new, Post.new, Picture.new
296
+ # post.title = "title"
297
+ # blog.add_item(post)
298
+ # blog.add_item(picture)
299
+ # blog.items.find_by(:post) == [post] # => true
300
+ # blog.items.find_by(Post) == [post] # => true
301
+ # blog.items.find_by(:post, title: "title") == [post] # => true
302
+ # blog.items.find_by(Object) == [post, picture] # => true
303
+ #
304
+ # @param klass [Class, Symbol] class (e.g. AIXM::Feature::Airport,
305
+ # AIXM::Feature::NavigationalAid::VOR) or shortcut symbol (e.g.
306
+ # :airport or :vor) as listed in AIXM::CLASSES
307
+ # @param attributes [Hash] search attributes by their values
308
+ # @return [AIXM::Concerns::Association::Array]
309
+ def find_by(klass, attributes={})
310
+ if klass.is_a? Symbol
311
+ klass = AIXM::CLASSES[klass]&.to_class || fail(ArgumentError, "unknown class shortcut `#{klass}'")
312
+ end
313
+ self.class.new(
314
+ select do |element|
315
+ if element.kind_of? klass
316
+ attributes.all? { |a, v| element.send(a) == v }
317
+ end
318
+ end
319
+ )
320
+ end
321
+
322
+ # Find equal objects on a has_many association.
323
+ #
324
+ # This may seem redundant at first, but keep in mind that two instances
325
+ # of +AIXM::CLASSES+ which implement `#to_uid` are considered equal if
326
+ # they are instances of the same class and both their UIDs as calculated
327
+ # by `#to_uid` are equal. Attributes which are not part of the `#to_uid`
328
+ # calculation are irrelevant!
329
+ #
330
+ # @example
331
+ # class Blog
332
+ # include AIXM::Concerns::Association
333
+ # has_many :items, accept: %i(post picture)
334
+ # end
335
+ # class Post
336
+ # include AIXM::Concerns::Association
337
+ # belongs_to :blog, as: :item
338
+ # attr_accessor :title
339
+ # end
340
+ # blog, post = Blog.new, Post.new
341
+ # blog.add_item(post)
342
+ # blog.items.find(post) == [post] # => true
343
+ #
344
+ # @param object [Object] instance of class listed in AIXM::CLASSES
345
+ # @return [AIXM::Concerns::Association::Array]
346
+ def find(object)
347
+ klass = object.__class__
348
+ self.class.new(
349
+ select do |element|
350
+ element.kind_of?(klass) && element == object
351
+ end
352
+ )
353
+ end
354
+
355
+ # Find equal or identical duplicates on a has_many association.
356
+ #
357
+ # @example
358
+ # class Blog
359
+ # include AIXM::Concerns::Association
360
+ # has_many :posts
361
+ # end
362
+ # class Post
363
+ # include AIXM::Concerns::Association
364
+ # belongs_to :blog
365
+ # end
366
+ # blog, post = Blog.new, Post.new
367
+ # duplicate_post = post.dup
368
+ # blog.add_posts([post, duplicate_post])
369
+ # blog.posts.duplicates # => [[post, duplicate_post]]
370
+ #
371
+ # @return [Array<Array<AIXM::Feature>>]
372
+ def duplicates
373
+ AIXM::Concerns::Memoize.method :to_uid do
374
+ group_by { _1.to_uid.to_s }.select { |_, a| a.count > 1 }.map(&:last)
375
+ end
376
+ end
377
+ end
378
+ end
379
+
380
+ end
381
+ end
@@ -0,0 +1,107 @@
1
+ module AIXM
2
+ module Concerns
3
+
4
+ # Memoize the return value of a specific method across multiple instances for
5
+ # the duration of a block.
6
+ #
7
+ # The method signature is taken into account, therefore calls of the same
8
+ # method with different positional and/or keyword arguments are cached
9
+ # independently. On the other hand, when calling the method with a block,
10
+ # no memoization is performed at all.
11
+ #
12
+ # Nested memoization of the same method is allowed and won't reset the
13
+ # memoization cache.
14
+ #
15
+ # @example
16
+ # class Either
17
+ # include AIXM::Concerns::Memoize
18
+ #
19
+ # def either(argument=nil, keyword: nil, &block)
20
+ # $entropy || argument || keyword || (block.call if block)
21
+ # end
22
+ # memoize :either
23
+ # end
24
+ #
25
+ # a, b, c = Either.new, Either.new, Either.new
26
+ #
27
+ # # No memoization before the block
28
+ # $entropy = nil
29
+ # a.either(1) # => 1
30
+ # b.either(keyword: 2) # => 2
31
+ # c.either { 3 } # => 3
32
+ # $entropy = :not_nil
33
+ # a.either(1) # => :not_nil
34
+ # b.either(keyword: 2) # => :not_nil
35
+ # c.either { 3 } # => :not_nil
36
+ #
37
+ # # Memoization inside the block
38
+ # AIXM::Concerns::Memoize.method :either do
39
+ # $entropy = nil
40
+ # a.either(1) # => 1
41
+ # b.either(keyword: 2) # => 2
42
+ # c.either { 3 } # => 3
43
+ # $entropy = :not_nil
44
+ # a.either(1) # => 1 (memoized)
45
+ # b.either(keyword: 2) # => 2 (memoized)
46
+ # c.either { 3 } # => :not_nil (cannot be memoized)
47
+ # end
48
+ #
49
+ # # No memoization after the block
50
+ # $entropy = nil
51
+ # a.either(1) # => 1
52
+ # $entropy = :not_nil
53
+ # a.either(1) # => :not_nil
54
+ module Memoize
55
+ module ClassMethods
56
+ def memoize(method)
57
+ unmemoized_method = :"unmemoized_#{method}"
58
+ alias_method unmemoized_method, method
59
+ define_method method do |*args, **kargs, &block|
60
+ if block || !AIXM::Concerns::Memoize.cache.has_key?(method)
61
+ send(unmemoized_method, *args, **kargs, &block)
62
+ else
63
+ cache = AIXM::Concerns::Memoize.cache[method]
64
+ id = object_id.hash ^ args.hash ^ kargs.hash
65
+ if cache.has_key?(id)
66
+ cache[id]
67
+ else
68
+ cache[id] = send(unmemoized_method, *args, **kargs)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ class << self
76
+ attr_reader :cache
77
+
78
+ def included(base)
79
+ base.extend(ClassMethods)
80
+ @cache = {}
81
+ end
82
+
83
+ def method(method, &block) # TODO: [ruby-3.1] use anonymous block "&" on this and next line
84
+ send(:"call_with#{:out if cached?(method)}_cache", method, &block)
85
+ end
86
+
87
+ private
88
+
89
+ def cached?(method)
90
+ cache.has_key?(method)
91
+ end
92
+
93
+ def call_without_cache(method)
94
+ yield
95
+ end
96
+
97
+ def call_with_cache(method)
98
+ cache[method] = {}
99
+ yield
100
+ ensure
101
+ cache.delete(method)
102
+ end
103
+ end
104
+ end
105
+
106
+ end
107
+ end
@@ -0,0 +1,34 @@
1
+ module AIXM
2
+ module Concerns
3
+
4
+ # Adds the XML builder wrapped to generate a document fragment.
5
+ module XMLBuilder
6
+ include AIXM::Concerns::Memoize
7
+
8
+ # Build a XML document fragment.
9
+ #
10
+ # @yield [Nokogiri::XML::Builder]
11
+ # @return [Nokogiri::XML::DocumentFragment]
12
+ def build_fragment
13
+ Nokogiri::XML::DocumentFragment.parse('').tap do |document|
14
+ Nokogiri::XML::Builder.with(document) do |fragment|
15
+ yield fragment
16
+ end
17
+ document.elements.each { _1.add_next_sibling("\n") } # add newline between tags on top level
18
+ end
19
+ end
20
+
21
+ # @return [Nokogiri::XML::DocumentFragment] UID fragment
22
+ def to_uid(...)
23
+ build_fragment { add_uid_to(_1, ...) }
24
+ end
25
+ memoize :to_uid
26
+
27
+ # @return [String] AIXM or OFMX fragment
28
+ def to_xml(...)
29
+ build_fragment { add_to(_1, ...) }.to_xml.strip.concat("\n")
30
+ end
31
+
32
+ end
33
+ end
34
+ end
data/lib/aixm/document.rb CHANGED
@@ -10,12 +10,13 @@ module AIXM
10
10
  # namespace: String (UUID)
11
11
  # created_at: Time or Date or String
12
12
  # effective_at: Time or Date or String
13
+ # expiration_at: Time or Date or String or nil
13
14
  # )
14
15
  # document.add_feature(AIXM::Feature)
15
16
  #
16
17
  # @see https://gitlab.com/openflightmaps/ofmx/wikis/Snapshot
17
18
  class Document
18
- include AIXM::Association
19
+ include AIXM::Concerns::Association
19
20
 
20
21
  NAMESPACE_RE = /\A[a-f\d]{8}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{12}\z/.freeze
21
22
 
@@ -36,7 +37,7 @@ module AIXM
36
37
  # @param value [String]
37
38
  attr_reader :namespace
38
39
 
39
- # Creation date and time
40
+ # Creation date and UTC time
40
41
  #
41
42
  # @overload created_at
42
43
  # @return [Time]
@@ -44,7 +45,7 @@ module AIXM
44
45
  # @param value [Time] default: {#effective_at} or now
45
46
  attr_reader :created_at
46
47
 
47
- # Effective after date and time
48
+ # Effective after date and UTC time
48
49
  #
49
50
  # @overload effective_at
50
51
  # @return [Time]
@@ -52,10 +53,19 @@ module AIXM
52
53
  # @param value [Time] default: {#created_at} or now
53
54
  attr_reader :effective_at
54
55
 
56
+ # Expiration after date and UTC time
57
+ #
58
+ # @overload expiration_at
59
+ # @return [Time, nil]
60
+ # @overload expiration_at=(value)
61
+ # @param value [Time, nil]
62
+ attr_reader :expiration_at
63
+
55
64
  # See the {cheat sheet}[AIXM::Document] for examples on how to create
56
65
  # instances of this class.
57
- def initialize(namespace: nil, created_at: nil, effective_at: nil)
58
- self.namespace, self.created_at, self.effective_at = namespace, created_at, effective_at
66
+ def initialize(namespace: nil, created_at: nil, effective_at: nil, expiration_at: nil)
67
+ self.namespace = namespace
68
+ self.created_at, self.effective_at, self.expiration_at = created_at, effective_at, expiration_at
59
69
  end
60
70
 
61
71
  # @return [String]
@@ -69,11 +79,29 @@ module AIXM
69
79
  end
70
80
 
71
81
  def created_at=(value)
72
- @created_at = value&.to_time || effective_at || Time.now
82
+ @created_at = if time = value&.to_time
83
+ fail(ArgumentError, "must be UTC") unless time.utc_offset.zero?
84
+ time.round
85
+ else
86
+ Time.now.utc.round
87
+ end
73
88
  end
74
89
 
75
90
  def effective_at=(value)
76
- @effective_at = value&.to_time || created_at || Time.now
91
+ @effective_at = if time = value&.to_time
92
+ fail(ArgumentError, "must be UTC") unless time.utc_offset.zero?
93
+ time.round
94
+ else
95
+ created_at || Time.now.utc.round
96
+ end
97
+ end
98
+
99
+ def expiration_at=(value)
100
+ @expiration_at = value&.to_time
101
+ @expiration_at = if time = value&.to_time
102
+ fail(ArgumentError, "must be UTC") unless time.utc_offset.zero?
103
+ time.round
104
+ end
77
105
  end
78
106
 
79
107
  # Regions used throughout this document.
@@ -128,8 +156,8 @@ module AIXM
128
156
  end
129
157
  end
130
158
 
131
- # @return [String] AIXM or OFMX markup
132
- def to_xml
159
+ # @return [Nokogiri::XML::Document] Nokogiri AIXM or OFMX document
160
+ def document
133
161
  meta = {
134
162
  'xmlns:xsi': AIXM.schema(:namespace),
135
163
  version: AIXM.schema(:version),
@@ -137,21 +165,24 @@ module AIXM
137
165
  namespace: (namespace if AIXM.ofmx?),
138
166
  regions: (regions.join(' '.freeze) if AIXM.ofmx?),
139
167
  created: @created_at.xmlschema,
140
- effective: @effective_at.xmlschema
168
+ effective: @effective_at.xmlschema,
169
+ expiration: (@expiration_at&.xmlschema if AIXM.ofmx?)
141
170
  }.compact
142
- builder = Builder::XmlMarkup.new(indent: 2)
143
- builder.instruct!
144
- builder.tag!(AIXM.schema(:root), meta) do |root|
145
- AIXM::Memoize.method :to_uid do
146
- root << features.map(&:to_xml).join.indent(2)
171
+ Nokogiri::XML::Builder.new do |builder|
172
+ builder.send(AIXM.schema(:root), meta) do |root|
173
+ AIXM::Concerns::Memoize.method :to_uid do
174
+ features.each { _1.add_to(root) }
175
+ end
176
+ if AIXM.ofmx? && AIXM.config.mid
177
+ AIXM::PayloadHash::Mid.new(builder.doc).insert_mid
178
+ end
147
179
  end
148
- end
149
- if AIXM.ofmx? && AIXM.config.mid
150
- AIXM::PayloadHash::Mid.new(builder.target!).insert_mid.to_xml
151
- else
152
- builder.target!
153
- end
180
+ end.doc
154
181
  end
155
182
 
183
+ # @return [String] AIXM or OFMX markup
184
+ def to_xml
185
+ document.pretty.to_xml
186
+ end
156
187
  end
157
188
  end