aixm 1.2.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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