aipp 1.0.0 → 2.0.0

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