aipp 1.0.0 → 2.0.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 (60) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -2
  3. data/CHANGELOG.md +17 -1
  4. data/README.md +269 -150
  5. data/exe/aip2aixm +2 -8
  6. data/exe/aip2ofmx +2 -8
  7. data/exe/notam2aixm +5 -0
  8. data/exe/notam2ofmx +5 -0
  9. data/lib/aipp/aip/README.md +10 -0
  10. data/lib/aipp/aip/executable.rb +40 -0
  11. data/lib/aipp/aip/parser.rb +9 -0
  12. data/lib/aipp/aip/runner.rb +85 -0
  13. data/lib/aipp/border.rb +2 -2
  14. data/lib/aipp/debugger.rb +14 -19
  15. data/lib/aipp/downloader/file.rb +57 -0
  16. data/lib/aipp/downloader/graphql.rb +29 -0
  17. data/lib/aipp/downloader/http.rb +48 -0
  18. data/lib/aipp/downloader.rb +78 -29
  19. data/lib/aipp/environment.rb +88 -0
  20. data/lib/aipp/executable.rb +36 -53
  21. data/lib/aipp/notam/README.md +25 -0
  22. data/lib/aipp/notam/executable.rb +27 -0
  23. data/lib/aipp/notam/parser.rb +9 -0
  24. data/lib/aipp/notam/runner.rb +28 -0
  25. data/lib/aipp/parser.rb +133 -160
  26. data/lib/aipp/patcher.rb +4 -5
  27. data/lib/aipp/regions/LF/README.md +6 -2
  28. data/lib/aipp/regions/LF/aip/aerodromes.rb +220 -0
  29. data/lib/aipp/regions/LF/aip/d_p_r_airspaces.rb +53 -0
  30. data/lib/aipp/regions/LF/aip/dangerous_activities.rb +48 -0
  31. data/lib/aipp/regions/LF/aip/designated_points.rb +44 -0
  32. data/lib/aipp/regions/LF/aip/helipads.rb +119 -0
  33. data/lib/aipp/regions/LF/aip/navigational_aids.rb +82 -0
  34. data/lib/aipp/regions/LF/aip/obstacles.rb +150 -0
  35. data/lib/aipp/regions/LF/aip/serviced_airspaces.rb +67 -0
  36. data/lib/aipp/regions/LF/aip/services.rb +169 -0
  37. data/lib/aipp/regions/LF/fixtures/aerodromes.yml +2 -2
  38. data/lib/aipp/regions/LF/helpers/base.rb +32 -32
  39. data/lib/aipp/regions/LS/README.md +59 -0
  40. data/lib/aipp/regions/LS/helpers/base.rb +111 -0
  41. data/lib/aipp/regions/LS/notam/ENR.rb +173 -0
  42. data/lib/aipp/runner.rb +152 -0
  43. data/lib/aipp/version.rb +1 -1
  44. data/lib/aipp.rb +30 -11
  45. data/lib/core_ext/array.rb +13 -0
  46. data/lib/core_ext/nokogiri.rb +56 -8
  47. data/lib/core_ext/string.rb +63 -1
  48. data.tar.gz.sig +0 -0
  49. metadata +115 -64
  50. metadata.gz.sig +0 -0
  51. data/lib/aipp/aip.rb +0 -166
  52. data/lib/aipp/regions/LF/aerodromes.rb +0 -223
  53. data/lib/aipp/regions/LF/d_p_r_airspaces.rb +0 -56
  54. data/lib/aipp/regions/LF/dangerous_activities.rb +0 -49
  55. data/lib/aipp/regions/LF/designated_points.rb +0 -47
  56. data/lib/aipp/regions/LF/helipads.rb +0 -122
  57. data/lib/aipp/regions/LF/navigational_aids.rb +0 -85
  58. data/lib/aipp/regions/LF/obstacles.rb +0 -153
  59. data/lib/aipp/regions/LF/serviced_airspaces.rb +0 -70
  60. data/lib/aipp/regions/LF/services.rb +0 -172
@@ -1,60 +1,28 @@
1
1
  module AIPP
2
2
 
3
- # Executable instantiated by the console tools
3
+ # @abstract
4
4
  class Executable
5
5
  include AIPP::Debugger
6
6
 
7
- attr_reader :options
8
-
9
- def initialize(**options)
10
- @options = options.merge(
11
- airac: AIRAC::Cycle.new,
12
- region_options: [],
7
+ def initialize(exe_file)
8
+ AIPP.options.replace(
9
+ schema: exe_file.split('2').last.to_sym,
13
10
  storage: Pathname(Dir.home).join('.aipp'),
11
+ clean: false,
14
12
  force: false,
15
13
  mid: false,
14
+ quiet: false,
16
15
  verbose: false,
17
16
  debug_on_warning: false,
18
17
  debug_on_error: false
19
18
  )
20
- OptionParser.new do |o|
21
- o.banner = <<~END
22
- Download online AIP and convert it to #{options[:schema].upcase}.
23
- Usage: #{File.basename($0)} [options]
24
- END
25
- o.on('-d', '--airac (DATE|INTEGER)', String, %Q[AIRAC date or delta e.g. "+1" (default: "#{@options[:airac].date.xmlschema}")]) { @options[:airac] = airac_for(_1) }
26
- o.on('-r', '--region STRING', String, 'region (e.g. "LF")') { @options[:region] = _1.upcase }
27
- o.on('-a', '--aip STRING', String, 'process this AIP only (e.g. "ENR-5.1")') { @options[:aip] = _1 }
28
- if options[:schema] == :ofmx
29
- o.on('-g', '--[no-]grouped-obstacles', 'group obstacles (default: false)') { @options[:grouped_obstacles] = _1 }
30
- o.on('-m', '--[no-]mid', 'insert mid attributes into all Uid elements (default: false)') { @options[:mid] = _1 }
31
- end
32
- o.on('-o', '--region-options STRING', String, %Q[comma separated region specific options]) { @options[:region_options] = _1.split(',') }
33
- o.on('-s', '--storage DIR', String, 'storage directory (default: "~/.aipp")') { @options[:storage] = Pathname(_1) }
34
- o.on('-h', '--[no-]check-links', 'check all links with HEAD requests') { @options[:check_links] = _1 }
35
- o.on('-f', '--[no-]force', 'ignore XML schema validation (default: false)') { @options[:force] = _1 }
36
- o.on('-v', '--[no-]verbose', 'verbose output including unsevere warnings (default: false)') { @options[:verbose] = _1 }
37
- o.on('-w', '--debug-on-warning [ID]', Integer, 'open debug session on warning with ID (default: false)') { @options[:debug_on_warning] = _1 || true }
38
- o.on('-e', '--[no-]debug-on-error', 'open debug session on error (default: false)') { @options[:debug_on_error] = _1 }
39
- o.on('-A', '--about', 'show author/license information and exit') { about }
40
- o.on('-R', '--readme', 'show README and exit') { readme }
41
- o.on('-L', '--list', 'list implemented regions and AIPs') { list }
42
- o.on('-V', '--version', 'show version and exit') { version }
43
- end.parse!
44
19
  end
45
20
 
46
21
  def run
47
- with_debugger(**options.slice(:verbose, :debug_on_warning, :debug_on_error)) do
22
+ with_debugger do
23
+ String.disable_colorization = !STDOUT.tty?
48
24
  starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
49
- AIPP::Parser.new(options: options).tap do |parser|
50
- parser.read_config
51
- parser.read_region
52
- parser.parse_aip
53
- parser.validate_aixm
54
- parser.write_build
55
- parser.write_aixm
56
- parser.write_config
57
- end
25
+ [:AIPP, AIPP.options.module, :Runner].constantize.new.run
58
26
  ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
59
27
  info("finished after %s" % Time.at(ending - starting).utc.strftime("%H:%M:%S"))
60
28
  end
@@ -62,12 +30,28 @@ module AIPP
62
30
 
63
31
  private
64
32
 
65
- def airac_for(argument)
66
- if argument.match?(/^[+-]\d+$/) # delta
67
- AIRAC::Cycle.new + argument.to_i
68
- else # date
69
- AIRAC::Cycle.new(argument)
33
+ def common_options(o)
34
+ o.on('-r', '--region STRING', String, 'region (e.g. "LF")') { AIPP.options.region = _1.upcase }
35
+ o.on('-s', '--section STRING', String, 'process this section only') { AIPP.options.section = _1.classify }
36
+ o.on('-d', '--storage DIR', String, 'storage directory (default: "~/.aipp")') { AIPP.options.storage = Pathname(_1) }
37
+ o.on('-o', '--output FILE', String, 'output file') { AIPP.options.output_file = _1 }
38
+ end
39
+
40
+ def developer_options(o)
41
+ if AIPP.options.schema == :ofmx
42
+ o.on('-m', '--[no-]mid', 'insert mid attributes into all Uid elements (default: false)') { AIPP.options.mid = _1 }
70
43
  end
44
+ o.on('-h', '--[no-]check-links', 'check all links with HEAD requests') { AIPP.options.check_links = _1 }
45
+ o.on('-c', '--[no-]clean', 'clean cache and download from sources anew (default: false)') { AIPP.options.clean = _1 }
46
+ o.on('-f', '--[no-]force', 'continue on non-fatal errors (default: false)') { AIPP.options.force = _1 }
47
+ o.on('-q', '--[no-]quiet', 'suppress all informational output (default: false)') { AIPP.options.quiet = _1 }
48
+ o.on('-v', '--[no-]verbose', 'verbose output including unsevere warnings (default: false)') { AIPP.options.verbose = _1 }
49
+ o.on('-w', '--debug-on-warning [ID]', Integer, 'open debug session on warning with ID (default: false)') { AIPP.options.debug_on_warning = _1 || true }
50
+ o.on('-e', '--[no-]debug-on-error', 'open debug session on error (default: false)') { AIPP.options.debug_on_error = _1 }
51
+ o.on('-A', '--about', 'show author/license information and exit') { about }
52
+ o.on('-R', '--readme', 'show README and exit') { readme }
53
+ o.on('-L', '--list', 'list implemented regions') { list }
54
+ o.on('-V', '--version', 'show version and exit') { version }
71
55
  end
72
56
 
73
57
  def about
@@ -76,19 +60,18 @@ module AIPP
76
60
  end
77
61
 
78
62
  def readme
79
- readme_path = Pathname($0).dirname.join('..', 'gems', "aipp-#{AIPP::VERSION}", 'README.md')
80
- puts IO.read(readme_path)
63
+ puts IO.read(Pathname(__dir__).join('README.md'))
81
64
  exit
82
65
  end
83
66
 
84
67
  def list
85
- regions_path = Pathname($0).dirname.join('..', 'gems', "aipp-#{AIPP::VERSION}", 'lib', 'aipp', 'regions')
86
- hash = Dir.each_child(regions_path).each.with_object({}) do |region, hash|
87
- hash[region] = Dir.children(regions_path.join(region)).sort.map do |aip|
88
- File.basename(aip, '.rb') if File.file?(regions_path.join(region, aip))
68
+ hash = Pathname(__dir__).join('regions').glob('*').each.with_object({}) do |dir, hash|
69
+ region = "Sections for region #{dir.basename}"
70
+ hash[region] = dir.join(AIPP.options.module.downcase).glob('*.rb').map do |file|
71
+ File.basename(file, '.rb')
89
72
  end.compact
90
73
  end
91
- puts hash.to_yaml.sub(/\A\W*/, '')
74
+ puts hash.to_yaml.lines[1..]
92
75
  exit
93
76
  end
94
77
 
@@ -0,0 +1,25 @@
1
+ # AIPP NOTAM Module
2
+
3
+ ## Cache Time Window
4
+
5
+ The default time window for NOTAM is the hour of day. This means:
6
+
7
+ * Source data is downloaded and cached based on the hour of the day.
8
+ * The effective date and time is rounded down to the previous full hour.
9
+
10
+ To force a rebuild within this time window, you have to clean the cache using the `-c` command line argument.
11
+
12
+ ### Soft Fail and Crossload
13
+
14
+ Malformed NOTAM which cannot be processed normally cause the build to fail. The `-f` command line argument changes this behaviour to skip the malformed NOTAM, issue a warning and continue.
15
+
16
+ To fix broken NOTAM, you can set a crossload directory with `-x`. The contents of this directory must adhere to the following convention:
17
+
18
+ ```
19
+ / ⬅︎ custom crossload directory
20
+ ├── LS ⬅︎ region
21
+ │   └── W2479_22.txt ⬅︎ NOTAM ID (replace "/" with "_")
22
+ └── ED ⬅︎ other region
23
+ ```
24
+
25
+ If a matching file is found in the crossload directory, it will be used in place of the original, malformed NOTAM.
@@ -0,0 +1,27 @@
1
+ module AIPP
2
+ module NOTAM
3
+
4
+ class Executable < AIPP::Executable
5
+
6
+ def initialize(exe_file)
7
+ super
8
+ now = Time.now.utc.round
9
+ AIPP.options.merge(
10
+ module: 'NOTAM',
11
+ effective_at: now - now.sec - (now.min * 60) # previous full hour
12
+ )
13
+ OptionParser.new do |o|
14
+ o.banner = <<~END
15
+ Download online NOTAM and convert it to #{AIPP.options.schema.upcase}.
16
+ Usage: #{File.basename($0)} [options]
17
+ END
18
+ common_options(o)
19
+ o.on('-t', '--effective (TIME)', String, %Q[effective after this point in time (default: #{AIPP.options.effective_at})]) { AIPP.options.effective_at = Time.parse(_1) }
20
+ o.on('-x', '--crossload DIR', String, 'crossload directory') { AIPP.options.crossload = Pathname(_1) }
21
+ developer_options(o)
22
+ end.parse!
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,9 @@
1
+ module AIPP
2
+ module NOTAM
3
+
4
+ # @abstract
5
+ class Parser < AIPP::Parser
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,28 @@
1
+ module AIPP
2
+ module NOTAM
3
+
4
+ class Runner < AIPP::Runner
5
+
6
+ def effective_at
7
+ AIPP.options.effective_at
8
+ end
9
+
10
+ def expiration_at
11
+ effective_at.end_of_day.round - 1
12
+ end
13
+
14
+ def run
15
+ info("NOTAM effective #{effective_at}", color: :green)
16
+ read_config
17
+ read_region
18
+ read_parsers
19
+ parse_sections
20
+ validate_aixm
21
+ write_aixm(AIPP.options.output_file || output_file)
22
+ write_config
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+ end
data/lib/aipp/parser.rb CHANGED
@@ -1,196 +1,169 @@
1
1
  module AIPP
2
2
 
3
- # AIP parser infrastructure
3
+ # @abstract
4
4
  class Parser
5
- extend Forwardable
6
5
  include AIPP::Debugger
7
- using AIXM::Refinements
6
+ include AIPP::Patcher
8
7
 
9
- # @return [Hash] passed command line arguments
10
- attr_reader :options
8
+ # @return [AIXM::Document] AIXM document instance
9
+ attr_reader :aixm
11
10
 
12
- # @return [Hash] configuration read from config.yml
13
- attr_reader :config
11
+ class << self
12
+ # Declare a dependency
13
+ #
14
+ # @param dependencies [Array<String>] class names of other parsers this
15
+ # parser depends on
16
+ def depends_on(*dependencies)
17
+ @dependencies = dependencies.map(&:to_s)
18
+ end
14
19
 
15
- # @return [AIXM::Document] target document
16
- attr_reader :aixm
20
+ # Declared dependencies
21
+ #
22
+ # @return [Array<String>] class names of other parsers this parser
23
+ # depends on
24
+ def dependencies
25
+ @dependencies || []
26
+ end
27
+ end
17
28
 
18
- # @return [Hash] map from AIP name to fixtures
19
- attr_reader :fixtures
20
-
21
- # @return [Hash] map from border names to border objects
22
- attr_reader :borders
23
-
24
- # @return [OpenStruct] object cache
25
- attr_reader :cache
26
-
27
- def initialize(options:)
28
- @options = options
29
- @options[:storage] = options[:storage].join(options[:region])
30
- @options[:storage].mkpath
31
- @config = {}
32
- @aixm = AIXM.document(effective_at: @options[:airac].date)
33
- @dependencies = THash.new
34
- @fixtures = {}
35
- @borders = {}
36
- @cache = OpenStruct.new
37
- AIXM.send("#{options[:schema]}!")
38
- AIXM.config.region = options[:region]
29
+ def initialize(downloader:, aixm:)
30
+ @downloader, @aixm = downloader, aixm
31
+ setup if respond_to? :setup
39
32
  end
40
33
 
41
34
  # @return [String]
42
35
  def inspect
43
- "#<AIPP::Parser>"
36
+ "#<AIPP::Parser #{section}>"
44
37
  end
45
38
 
46
- # Read the configuration from config.yml.
47
- def read_config
48
- info("reading config.yml")
49
- @config = YAML.load_file(config_file, symbolize_names: true, fallback: {}) if config_file.exist?
50
- @config[:namespace] ||= SecureRandom.uuid
51
- @aixm.namespace = @config[:namespace]
39
+ # @return [String]
40
+ def section
41
+ self.class.to_s.sectionize
52
42
  end
53
43
 
54
- # Read the region directory and build the dependency list.
55
- def read_region
56
- info("reading region #{options[:region]}")
57
- dir = Pathname(__FILE__).dirname.join('regions', options[:region])
58
- fail("unknown region `#{options[:region]}'") unless dir.exist?
59
- # Fixtures
60
- dir.glob('fixtures/*.yml').each do |file|
61
- verbose_info "reading fixture fixtures/#{file.basename}"
62
- fixture = YAML.load_file(file)
63
- @fixtures[file.basename('.yml').to_s] = fixture
64
- end
65
- # Borders
66
- dir.glob('borders/*.geojson').each do |file|
67
- verbose_info "reading border borders/#{file.basename}"
68
- border = AIPP::Border.from_file(file)
69
- @borders[file.basename] = border
70
- end
71
- # Helpers
72
- dir.glob('helpers/*.rb').each do |file|
73
- verbose_info "reading helper helpers/#{file.basename}"
74
- require file
75
- end
76
- # Parsers
77
- dir.glob('*.rb').each do |file|
78
- verbose_info "requiring #{file.basename}"
79
- require file
80
- aip = file.basename('.*').to_s
81
- @dependencies[aip] = ("AIPP::%s::%s::DEPENDS" % [options[:region], aip.remove(/\W/).camelcase]).constantize
82
- end
44
+ # @abstract
45
+ def origin_for(*)
46
+ fail "origin_for method must be implemented in parser"
83
47
  end
84
48
 
85
- # Parse AIP by invoking the parser classes for the current region.
86
- def parse_aip
87
- info("AIRAC #{options[:airac].id} effective #{options[:airac].date}", color: :green)
88
- AIPP::Downloader.new(storage: options[:storage], source: options[:airac].date.xmlschema) do |downloader|
89
- @dependencies.tsort(options[:aip]).each do |aip|
90
- info("parsing #{aip}")
91
- ("AIPP::%s::%s" % [options[:region], aip.remove(/\W/).camelcase]).constantize.new(
92
- aip: aip,
93
- downloader: downloader,
94
- fixture: @fixtures[aip],
95
- parser: self
96
- ).attach_patches.tap(&:parse).detach_patches
97
- end
98
- end
99
- if options[:grouped_obstacles]
100
- info("grouping obstacles")
101
- aixm.group_obstacles!
102
- end
103
- info("counting #{aixm.features.count} features")
49
+ # Read a source document
50
+ #
51
+ # Read the cached document if it exists in the source archive. Otherwise,
52
+ # download and cache it.
53
+ #
54
+ # An origin builder method +origin_for+ must be implemented by the parser
55
+ # definition.
56
+ #
57
+ # @param document [String] e.g. "ENR-2.1" or "aerodromes" (default: current
58
+ # +section+)
59
+ # @return [Nokogiri::XML::Document, Nokogiri::HTML5::Document,
60
+ # Roo::Spreadsheet, String] document
61
+ def read(document=section)
62
+ @downloader.read(
63
+ document: document,
64
+ origin: origin_for(document)
65
+ )
104
66
  end
105
67
 
106
- # Validate the AIXM document.
68
+ # Add feature to AIXM
107
69
  #
108
- # @raise [RuntimeError] if the document is not valid
109
- def validate_aixm
110
- info("detecting duplicates")
111
- if (duplicates = aixm.features.duplicates).any?
112
- message = "duplicates found:\n" + duplicates.map { "#{_1.inspect} from #{_1.source}" }.join("\n")
113
- @options[:force] ? warn(message) : fail(message)
114
- end
115
- info("validating #{options[:schema].upcase}")
116
- unless aixm.valid?
117
- message = "invalid #{options[:schema].upcase} document:\n" + aixm.errors.map(&:message).join("\n")
118
- @options[:force] ? warn(message) : fail(message)
119
- end
70
+ # @param feature [AIXM::Feature] e.g. airport or airspace
71
+ # @return [AIXM::Feature] added feature
72
+ def add(feature)
73
+ verbose_info "adding #{feature.inspect}"
74
+ aixm.add_feature feature
75
+ feature
120
76
  end
121
77
 
122
- # Write the AIXM document and context information.
123
- def write_build
124
- if @options[:aip]
125
- info ("skipping build")
126
- else
127
- info("writing build")
128
- builds_path.mkpath
129
- build_file = builds_path.join("#{@options[:airac].date.xmlschema}.zip")
130
- Dir.mktmpdir do |tmp_dir|
131
- tmp_dir = Pathname(tmp_dir)
132
- # AIXM/OFMX file
133
- AIXM.config.mid = true
134
- File.write(tmp_dir.join(aixm_file), aixm.to_xml)
135
- # Build details
136
- File.write(
137
- tmp_dir.join('build.yaml'), {
138
- version: AIPP::VERSION,
139
- config: @config,
140
- options: @options
141
- }.to_yaml
142
- )
143
- # Manifest
144
- manifest = ['AIP','Feature', 'Comment', 'Short Uid Hash', 'Short Feature Hash'].to_csv
145
- manifest += aixm.features.map do |feature|
146
- xml = feature.to_xml
147
- element = xml.first_match(/<(\w{3})\s/)
148
- [
149
- feature.source.split('|')[2],
150
- element,
151
- xml.match(/<!-- (.*?) -->/)[1],
152
- AIXM::PayloadHash.new(xml.match(%r(<#{element}Uid\s.*?</#{element}Uid>)m).to_s).to_uuid[0,8],
153
- AIXM::PayloadHash.new(xml).to_uuid[0,8]
154
- ].to_csv
155
- end.sort.join
156
- File.write(tmp_dir.join('manifest.csv'), manifest)
157
- # Zip it
158
- build_file.delete if build_file.exist?
159
- Zip::File.open(build_file, Zip::File::CREATE) do |zip|
160
- tmp_dir.children.each do |entry|
161
- zip.add(entry.basename.to_s, entry) unless entry.basename.to_s[0] == '.'
162
- end
163
- end
164
- end
78
+ # @!method find_by(klass, attributes={})
79
+ # Find objects of the given class and optionally with the given attribute
80
+ # values previously written to AIXM.
81
+ #
82
+ # @note This method is delegated to +AIXM::Association::Array+.
83
+ # @see https://www.rubydoc.info/gems/aixm/AIXM/Association/Array#find_by-instance_method
84
+ #
85
+ # @!method find(object)
86
+ # Find equal objects previously written to AIXM.
87
+ #
88
+ # @note This method is delegated to +AIXM::Association::Array+.
89
+ # @see https://www.rubydoc.info/gems/aixm/AIXM/Association/Array#find-instance_method
90
+ %i(find_by find).each do |method|
91
+ define_method method do |*args|
92
+ aixm.features.send(method, *args)
165
93
  end
166
94
  end
167
95
 
168
- # Write the AIXM document.
169
- def write_aixm
170
- info("writing #{aixm_file}")
171
- AIXM.config.mid = options[:mid]
172
- File.write(aixm_file, aixm.to_xml)
96
+ # @overload given(*objects)
97
+ # Return +objects+ unless at least one of them equals nil
98
+ #
99
+ # @example
100
+ # # Instead of this:
101
+ # first, last = unless ((first = expensive_first).nil? || (last = expensive_last).nil?)
102
+ # [first, last]
103
+ # end
104
+ #
105
+ # # Use the following:
106
+ # first, last = given(expensive_first, expensive_last)
107
+ #
108
+ # @param *objects [Array<Object>] any objects really
109
+ # @return [Object] nil if at least one of the objects is nil, given
110
+ # objects otherwise
111
+ #
112
+ # @overload given(*objects)
113
+ # Yield +objects+ unless at least one of them equals nil
114
+ #
115
+ # @example
116
+ # # Instead of this:
117
+ # name = unless ((first = expensive_first.nil? || (last = expensive_last.nil?)
118
+ # "#{first} #{last}"
119
+ # end
120
+ #
121
+ # # Use any of the following:
122
+ # name = given(expensive_first, expensive_last) { |f, l| "#{f} #{l}" }
123
+ # name = given(expensive_first, expensive_last) { "#{_1} #{_2}" }
124
+ #
125
+ # @param *objects [Array<Object>] any objects really
126
+ # @yield [Array<Object>] objects passed as parameter
127
+ # @return [Object] nil if at least one of the objects is nil, return of
128
+ # block otherwise
129
+ def given(*objects)
130
+ if objects.none?(&:nil?)
131
+ block_given? ? yield(*objects) : objects
132
+ end
173
133
  end
174
134
 
175
- # Write the configuration to config.yml.
176
- def write_config
177
- info("writing config.yml")
178
- File.write(config_file, config.to_yaml)
135
+ # Build and optionally check a Markdown link
136
+ #
137
+ # @example
138
+ # AIPP.options.check_links = false
139
+ # link_to('foo', 'https://bar.com/exists') # => "[foo](https://bar.com/exists)"
140
+ # link_to('foo', 'https://bar.com/not-found') # => "[foo](https://bar.com/not-found)"
141
+ # AIPP.options.check_links = true
142
+ # link_to('foo', 'https://bar.com/exists') # => "[foo](https://bar.com/exists)"
143
+ # link_to('foo', 'https://bar.com/not-found') # => nil
144
+ #
145
+ # @param body [String] body text of the link
146
+ # @param url [String] URL of the link
147
+ # @return [String, nil] Markdown link
148
+ def link_to(body, url)
149
+ "[#{body}](#{url})" if !AIPP.options.check_links || url_exists?(url)
179
150
  end
180
151
 
181
152
  private
182
153
 
183
- def aixm_file
184
- "#{options[:region]}_#{options[:airac].date.xmlschema}.#{options[:schema]}"
185
- end
186
-
187
- def builds_path
188
- options[:storage].join('builds')
154
+ def url_exists?(url)
155
+ uri = URI.parse(url)
156
+ Net::HTTP.new(uri.host, uri.port).tap do |request|
157
+ request.use_ssl = (uri.scheme == 'https')
158
+ path = uri.path.present? ? uri.path : '/'
159
+ result = request.request_head(path)
160
+ if result.kind_of? Net::HTTPRedirection
161
+ url_exist?(result['location'])
162
+ else
163
+ result.code == '200'
164
+ end
165
+ end
189
166
  end
190
167
 
191
- def config_file
192
- options[:storage].join('config.yml')
193
- end
194
168
  end
195
-
196
169
  end
data/lib/aipp/patcher.rb CHANGED
@@ -1,9 +1,9 @@
1
1
  module AIPP
2
2
  module Patcher
3
3
 
4
- def self.included(klass)
5
- klass.extend(ClassMethods)
6
- klass.class_variable_set(:@@patches, {})
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ base.class_variable_set(:@@patches, {})
7
7
  end
8
8
 
9
9
  module ClassMethods
@@ -17,14 +17,13 @@ module AIPP
17
17
  end
18
18
 
19
19
  def attach_patches
20
- parser = self
21
20
  verbose_info_method = method(:verbose_info)
22
21
  self.class.patches[self.class]&.each do |(klass, attribute, block)|
23
22
  klass.instance_eval do
24
23
  alias_method :"original_#{attribute}=", :"#{attribute}="
25
24
  define_method(:"#{attribute}=") do |value|
26
25
  error = catch :abort do
27
- value = block.call(parser, self, value)
26
+ value = block.call(self, value)
28
27
  verbose_info_method.call("Patching #{self.inspect} with #{attribute}=#{value.inspect}", color: :magenta)
29
28
  end
30
29
  fail "patching #{self.inspect} with #{attribute}=#{value.inspect} failed: #{error}" if error
@@ -1,4 +1,4 @@
1
- # AIP LF – Mainland France
1
+ # LF – France Mainland
2
2
 
3
3
  ## Prerequisites
4
4
 
@@ -7,7 +7,7 @@ This parser requires the XML data dump from SIA. It is available free of charge,
7
7
  1. Browse to the [SIA web shop](https://www.sia.aviation-civile.gouv.fr/produits-numeriques-en-libre-disposition/les-bases-de-donnees-sia.html).
8
8
  2. Shop the desired dump named «données aéronautiques XML AIRAC ii/yy».
9
9
  3. Browse to [your customer page](https://www.sia.aviation-civile.gouv.fr/customer/account/#orders-and-proposals).
10
- 4. In section «mes produits téléchargeables» download the desired dump.
10
+ 4. On page «mes produits téléchargeables» download the desired dump.
11
11
  5. Unzip the downloaded ZIP archive.
12
12
  6. Move the file «XML_SIA_yyyy-mm-dd.xml» to the directory in which you will execute the parser.
13
13
 
@@ -41,6 +41,10 @@ grep "<Revetement>" XML_SIA_2021-12-02_UTF.xml | sort | uniq
41
41
  (...)
42
42
  ```
43
43
 
44
+ ## Error Reports
45
+
46
+ Feedback and error reports should be sent to SIA. However, their [contact form](https://www.sia.aviation-civile.gouv.fr/contact) is sometimes broken. If you don't receive an automatic reception confirmation, you should send it directly to sia-qualite@aviation-civile.gouv.fr instead.
47
+
44
48
  ## References
45
49
 
46
50
  * [SIA – AIP publisher](https://www.sia.aviation-civile.gouv.fr)