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
data/exe/aip2aixm CHANGED
@@ -1,11 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'bundler/inline'
3
+ require 'aipp'
4
4
 
5
- gemfile do
6
- source 'https://rubygems.org'
7
- ruby '>= 3.0.0'
8
- gem 'aipp', '~> 1'
9
- end
10
-
11
- AIPP::Executable.new(schema: File.basename($0)[4..-1].to_sym).run
5
+ AIPP::AIP::Executable.new(File.basename($0)).run
data/exe/aip2ofmx CHANGED
@@ -1,11 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'bundler/inline'
3
+ require 'aipp'
4
4
 
5
- gemfile do
6
- source 'https://rubygems.org'
7
- ruby '>= 3.0.0'
8
- gem 'aipp', '~> 1'
9
- end
10
-
11
- AIPP::Executable.new(schema: File.basename($0)[4..-1].to_sym).run
5
+ AIPP::AIP::Executable.new(File.basename($0)).run
data/exe/notam2aixm ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'aipp'
4
+
5
+ AIPP::NOTAM::Executable.new(File.basename($0)).run
data/exe/notam2ofmx ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'aipp'
4
+
5
+ AIPP::NOTAM::Executable.new(File.basename($0)).run
@@ -0,0 +1,10 @@
1
+ # AIPP AIP Module
2
+
3
+ ## Cache Time Window
4
+
5
+ The default time window for AIP is the AIRAC cycle. This means:
6
+
7
+ * Source data is downloaded and cached once for every AIRAC cycle.
8
+ * The effective date and time is rounded down to the first day midnight of the AIRAC cycle.
9
+
10
+ To force a rebuild within this time window, you have to clean the cache using the `-c` command line argument.
@@ -0,0 +1,40 @@
1
+ module AIPP
2
+ module AIP
3
+
4
+ class Executable < AIPP::Executable
5
+
6
+ def initialize(exe_file)
7
+ super
8
+ AIPP.options.merge(
9
+ module: 'AIP',
10
+ airac: AIRAC::Cycle.new,
11
+ region_options: []
12
+ )
13
+ OptionParser.new do |o|
14
+ o.banner = <<~END
15
+ Download online AIP and convert it to #{AIPP.options.schema.upcase}.
16
+ Usage: #{File.basename($0)} [options]
17
+ END
18
+ common_options(o)
19
+ o.on('-a', '--airac (DATE|INTEGER)', String, %Q[AIRAC date or delta e.g. "+1" (default: "#{AIPP.options.airac.date.xmlschema}")]) { AIPP.options.airac = airac_for(_1) }
20
+ if AIPP.options.schema == :ofmx
21
+ o.on('-g', '--[no-]grouped-obstacles', 'group obstacles (default: false)') { AIPP.options.grouped_obstacles = _1 }
22
+ end
23
+ o.on('-O', '--region-options STRING', String, %Q[comma separated region specific options]) { AIPP.options.region_options = _1.split(',') }
24
+ developer_options(o)
25
+ end.parse!
26
+ end
27
+
28
+ private
29
+
30
+ def airac_for(argument)
31
+ if argument.match?(/^[+-]\d+$/) # delta
32
+ AIRAC::Cycle.new + argument.to_i
33
+ else # date
34
+ AIRAC::Cycle.new(argument)
35
+ end
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,9 @@
1
+ module AIPP
2
+ module AIP
3
+
4
+ # @abstract
5
+ class Parser < AIPP::Parser
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,85 @@
1
+ module AIPP
2
+ module AIP
3
+
4
+ class Runner < AIPP::Runner
5
+
6
+ def effective_at
7
+ AIPP.options.airac.effective.begin
8
+ end
9
+
10
+ def expiration_at
11
+ AIPP.options.airac.effective.end
12
+ end
13
+
14
+ def run
15
+ info("AIP AIRAC #{AIPP.options.airac.id} effective #{effective_at}", color: :green)
16
+ read_config
17
+ read_region
18
+ read_parsers
19
+ parse_sections
20
+ validate_aixm
21
+ write_build
22
+ write_aixm(AIPP.options.output_file || output_file)
23
+ write_config
24
+ end
25
+
26
+ private
27
+
28
+ # Parse AIP by invoking the parser classes for the current region.
29
+ def parse_sections
30
+ super
31
+ if AIPP.options.grouped_obstacles
32
+ info("grouping obstacles")
33
+ aixm.group_obstacles!
34
+ end
35
+ end
36
+
37
+ # Write the AIXM document and context information.
38
+ def write_build
39
+ if AIPP.options.section
40
+ super
41
+ else
42
+ info("writing build")
43
+ builds_dir.mkpath
44
+ build_file = builds_dir.join("#{AIPP.options.airac.date.xmlschema}.zip")
45
+ Dir.mktmpdir do |tmp_dir|
46
+ tmp_dir = Pathname(tmp_dir)
47
+ # AIXM/OFMX file
48
+ AIXM.config.mid = true
49
+ File.write(tmp_dir.join(output_file), aixm.to_xml)
50
+ # Build details
51
+ File.write(
52
+ tmp_dir.join('build.yaml'), {
53
+ version: AIPP::VERSION,
54
+ config: AIPP.config,
55
+ options: AIPP.options,
56
+ }.to_yaml
57
+ )
58
+ # Manifest
59
+ manifest = ['AIP','Feature', 'Comment', 'Short Uid Hash', 'Short Feature Hash'].to_csv
60
+ manifest += aixm.features.map do |feature|
61
+ xml = feature.to_xml
62
+ element = xml.first_match(/<(\w{3})\s/)
63
+ [
64
+ feature.source.split('|')[2],
65
+ element,
66
+ xml.match(/<!-- (.*?) -->/)[1],
67
+ AIXM::PayloadHash.new(xml.match(%r(<#{element}Uid\s.*?</#{element}Uid>)m).to_s).to_uuid[0,8],
68
+ AIXM::PayloadHash.new(xml).to_uuid[0,8]
69
+ ].to_csv
70
+ end.sort.join
71
+ File.write(tmp_dir.join('manifest.csv'), manifest)
72
+ # Zip it
73
+ build_file.delete if build_file.exist?
74
+ Zip::File.open(build_file, Zip::File::CREATE) do |zip|
75
+ tmp_dir.children.each do |entry|
76
+ zip.add(entry.basename.to_s, entry) unless entry.basename.to_s[0] == '.'
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ end
85
+ end
data/lib/aipp/border.rb CHANGED
@@ -35,7 +35,7 @@ module AIPP
35
35
  # }
36
36
  #
37
37
  # Please note that GeoJSON orders coordinate tuples in mathematical order
38
- # as `[longitude, latitude]`!
38
+ # as +[longitude, latitude]+!
39
39
  #
40
40
  # @param file [Pathname, String] GeoJSON file
41
41
  #
@@ -60,7 +60,7 @@ module AIPP
60
60
  # New border object from array of points
61
61
  #
62
62
  # The array must contain coordinate tuples in geographical order as
63
- # `latitude longitude` separated by whitespace and/or commas.
63
+ # +latitude longitude+ separated by whitespace and/or commas.
64
64
  #
65
65
  # @param array [Array<Array<String>>] one or more arrays of coordinate pairs
66
66
  #
data/lib/aipp/debugger.rb CHANGED
@@ -22,14 +22,14 @@ module AIPP
22
22
  # @param verbose [Boolean] print verbose info, print unsevere warnings
23
23
  # and re-raise rescued errors
24
24
  # @yield Block the debugger is watching
25
- def with_debugger(**options, &)
26
- DEBUGGER__.instance_variable_set(:@options__, options.merge(counter: 0))
25
+ def with_debugger(&)
26
+ AIPP.cache.debug_counter = 0
27
27
  case
28
- when id = debugger_options[:debug_on_warning]
28
+ when id = AIPP.options.debug_on_warning
29
29
  puts instructions_for(@id == true ? 'warning' : "warning #{id}")
30
30
  DEBUGGER__::start(no_sigint_hook: true, nonstop: true)
31
31
  call_with_rescue(&)
32
- when debugger_options[:debug_on_error]
32
+ when AIPP.options.debug_on_error
33
33
  puts instructions_for('error')
34
34
  DEBUGGER__::start(no_sigint_hook: true, nonstop: true, postmortem: true)
35
35
  call_without_rescue(&)
@@ -39,20 +39,17 @@ module AIPP
39
39
  end
40
40
  end
41
41
 
42
- def self.included(*)
43
- DEBUGGER__.instance_variable_set(:@options__, {})
44
- end
42
+ alias_method :original_warn, :warn
45
43
 
46
44
  # Issue a warning and maybe open a debug session.
47
45
  #
48
46
  # @param message [String] warning message
49
47
  # @param severe [Boolean] whether this problem must be fixed or not
50
- alias_method :original_warn, :warn
51
48
  def warn(message, severe: true)
52
- if severe || debugger_options[:verbose]
53
- debugger_options[:counter] += 1
54
- original_warn "WARNING #{debugger_options[:counter]}: #{message.upcase_first} #{'(unsevere)' unless severe}".red
55
- debugger if debugger_options[:debug_on_warning] == true || debugger_options[:debug_on_warning] == debugger_options[:counter]
49
+ if severe || AIPP.options.verbose
50
+ AIPP.cache.debug_counter += 1
51
+ original_warn "WARNING #{AIPP.cache.debug_counter}: #{message.upcase_first} #{'(unsevere)' unless severe}".red
52
+ debugger if AIPP.options.debug_on_warning == true || AIPP.options.debug_on_warning == AIPP.cache.debug_counter
56
53
  end
57
54
  end
58
55
 
@@ -61,7 +58,9 @@ module AIPP
61
58
  # @param message [String] informational message
62
59
  # @param color [Symbol] message color
63
60
  def info(message, color: nil)
64
- puts color ? message.upcase_first.send(color) : message.upcase_first
61
+ unless AIPP.options.quiet
62
+ puts color ? message.upcase_first.send(color) : message.upcase_first
63
+ end
65
64
  end
66
65
 
67
66
  # Issue a verbose informational message.
@@ -69,21 +68,17 @@ module AIPP
69
68
  # @param message [String] verbose informational message
70
69
  # @param color [Symbol] message color
71
70
  def verbose_info(message, color: :blue)
72
- info(message, color: color) if debugger_options[:verbose]
71
+ info(message, color: color) if AIPP.options.verbose
73
72
  end
74
73
 
75
74
  private
76
75
 
77
- def debugger_options
78
- DEBUGGER__.instance_variable_get(:@options__)
79
- end
80
-
81
76
  def call_with_rescue(&block)
82
77
  block.call
83
78
  rescue => error
84
79
  message = error.respond_to?(:original_message) ? error.original_message : error.message
85
80
  puts "ERROR: #{message}".magenta
86
- raise if debugger_options[:verbose]
81
+ raise if AIPP.options.verbose
87
82
  end
88
83
 
89
84
  def call_without_rescue(&block)
@@ -0,0 +1,57 @@
1
+ module AIPP
2
+ class Downloader
3
+
4
+ # Local file
5
+ class File
6
+ def initialize(archive: nil, file:, type: nil)
7
+ @archive = Pathname(archive) if archive
8
+ @file, @type = Pathname(file), type&.to_s
9
+ end
10
+
11
+ def fetch_to(path)
12
+ path.join(fetched_file).tap do |target|
13
+ if @archive
14
+ fail NotFoundError unless @archive.exist?
15
+ extract(@file, from: @archive, as: target)
16
+ else
17
+ fail NotFoundError unless @file.exist?
18
+ FileUtils.cp(@file, target)
19
+ end
20
+ end
21
+ self
22
+ end
23
+
24
+ def fetched_file
25
+ [name, type].join('.')
26
+ end
27
+
28
+ private
29
+
30
+ def name
31
+ @file.basename(@file.extname).to_s
32
+ end
33
+
34
+ def type
35
+ @type || @file.extname[1..] || fail("type must be declared")
36
+ end
37
+
38
+ def extract(file, from:, as:)
39
+ if respond_to?(extractor = 'un' + from.extname[1..], true)
40
+ send(extractor, file, from: from, as: as) or fail NotFoundError
41
+ else
42
+ fail "archive type not recognized"
43
+ end
44
+ end
45
+
46
+ # @return [Boolean] whether a file was extracted
47
+ def unzip(file, from:, as:)
48
+ Zip::File.open(from).inject(nil) do |_, entry|
49
+ if file.to_s == entry.name
50
+ break entry.extract(as)
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,29 @@
1
+ module AIPP
2
+ class Downloader
3
+
4
+ # Remote file via HTTP
5
+ class GraphQL < File
6
+ def initialize(client:, query:, variables:)
7
+ @client, @query, @variables = client, query, variables
8
+ end
9
+
10
+ def fetch_to(path)
11
+ @client.query(@query, variables: @variables).tap do |result|
12
+ ::File.write(path.join(fetched_file), result.data.to_h.to_json)
13
+ end
14
+ self
15
+ end
16
+
17
+ private
18
+
19
+ def name
20
+ [@client, @query, @variables].map(&:to_s).join('|').to_digest
21
+ end
22
+
23
+ def type
24
+ :json
25
+ end
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,48 @@
1
+ module AIPP
2
+ class Downloader
3
+
4
+ # Remote file via HTTP
5
+ class HTTP < File
6
+ ARCHIVE_MIME_TYPES = {
7
+ 'application/zip' => :zip
8
+ }.freeze
9
+
10
+ def initialize(archive: nil, file:, type: nil, headers: {})
11
+ @archive = URI(archive) if archive
12
+ @file, @type, @headers = URI(file), type&.to_s, headers
13
+ @digest = (archive || file).to_digest
14
+ end
15
+
16
+ # @param path [Pathname] directory where to write the fetched file
17
+ # @return [File] fetched file
18
+ def fetch_to(path)
19
+ response = Excon.get((@archive || @file).to_s, headers: @headers)
20
+ fail NotFoundError if response.status == 404
21
+ mime_type = ARCHIVE_MIME_TYPES.fetch(response.headers['Content-Type'], :dat)
22
+ downloaded_file = path.join([@digest, mime_type].join('.'))
23
+ ::File.write(downloaded_file, response.body)
24
+ path.join(fetched_file).tap do |target|
25
+ if @archive
26
+ extract(@file, from: downloaded_file, as: target)
27
+ ::File.delete(downloaded_file)
28
+ else
29
+ ::File.rename(downloaded_file, target)
30
+ end
31
+ end
32
+ self
33
+ end
34
+
35
+ private
36
+
37
+ def name
38
+ path = Pathname(@file.path)
39
+ path.basename(path.extname).to_s.blank_to_nil || @digest
40
+ end
41
+
42
+ def type
43
+ @type || Pathname(@file.path).extname[1..].blank_to_nil || fail("type must be declared")
44
+ end
45
+ end
46
+
47
+ end
48
+ end
@@ -4,28 +4,53 @@ module AIPP
4
4
  #
5
5
  # The downloader operates in the +storage+ directory where it creates two
6
6
  # subdirectories "sources" and "work". The initializer looks for the +source+
7
- # archive in "sources" and (if found) unzips its contents into "work". When
7
+ # archive in "sources" and (if found) unpacks its contents into "work". When
8
8
  # reading a +document+, the downloader looks for the +document+ in "work" and
9
- # (unless found) downloads it from +url+. HTML documents are parsed to
10
- # +Nokogiri::HTML5::Document+, PDF documents are parsed to +AIPP::PDF+.
11
- # Finally, the contents of "work" are written back to the +source+ archive.
9
+ # (if not found or the clean option is set) downloads it from +origin+.
10
+ # Finally, the contents of "work" are packed back into the +source+ archive.
11
+ #
12
+ # Origins are defined as instances of downloader origin objects:
13
+ #
14
+ # * {AIXM::Downloader::File} – local file or archive
15
+ # * {AIXM::Downloader::HTTP} – remote file or archive via HTTP
16
+ # * {AIXM::Downloader::GraphQL} – GraphQL query
17
+ #
18
+ # The following archives are recognized:
19
+ #
20
+ # [.zip] ZIP archive
21
+ #
22
+ # The following file types are recognised:
23
+ #
24
+ # [.ofmx] Parsed by Nokogiri returning an instance of {Nokogiri::XML::Document}[https://www.rubydoc.info/gems/nokogiri/Nokogiri/XML/Document]
25
+ # [.xml] Parsed by Nokogiri returning an instance of {Nokogiri::XML::Document}[https://www.rubydoc.info/gems/nokogiri/Nokogiri/XML/Document]
26
+ # [.html] Parsed by Nokogiri returning an instance of {Nokogiri::HTML5::Document}[https://www.rubydoc.info/gems/nokogiri/Nokogiri/HTML5/Document]
27
+ # [.pdf] Converted to text – see {AIPP::PDF}
28
+ # [.json] Deserialized JSON e.g. as response to a GraphQL query
29
+ # [.xlsx] Parsed by Roo returning an instance of {Roo::Excelx}[https://www.rubydoc.info/gems/roo/Roo/Excelx]
30
+ # [.ods] Parsed by Roo returning an instance of {Roo::OpenOffice}[https://www.rubydoc.info/gems/roo/Roo/OpenOffice]
31
+ # [.csv] Parsed by Roo returning an instance of {Roo::CSV}[https://www.rubydoc.info/gems/roo/Roo/CSV]
32
+ # [.txt] Instance of +String+
12
33
  #
13
34
  # @example
14
- # AIPP::Downloader.new(storage: options[:storage], source: "2018-11-08") do |downloader|
35
+ # AIPP::Downloader.new(storage: AIPP.options.storage, source: "2018-11-08") do |downloader|
15
36
  # html = downloader.read(
16
37
  # document: 'ENR-5.1',
17
- # url: 'https://www.sia.aviation-civile.gouv.fr/dvd/eAIP_08_NOV_2018/FRANCE/AIRAC-2018-11-08/html/eAIP/FR-ENR-5.1-fr-FR.html'
38
+ # origin: AIPP::Downloader::HTTP.new(
39
+ # file: 'https://www.sia.aviation-civile.gouv.fr/dvd/eAIP_08_NOV_2018/FRANCE/AIRAC-2018-11-08/html/eAIP/FR-ENR-5.1-fr-FR.html'
40
+ # )
18
41
  # )
19
42
  # pdf = downloader.read(
20
43
  # document: 'VAC-LFMV',
21
- # url: 'https://www.sia.aviation-civile.gouv.fr/dvd/eAIP_08_NOV_2018/Atlas-VAC/PDF_AIPparSSection/VAC/AD/AD-2.LFMV.pdf'
44
+ # origin: AIPP::Downloader::HTTP.new(
45
+ # file: 'https://www.sia.aviation-civile.gouv.fr/dvd/eAIP_08_NOV_2018/Atlas-VAC/PDF_AIPparSSection/VAC/AD/AD-2.LFMV.pdf'
46
+ # )
22
47
  # )
23
48
  # end
24
49
  class Downloader
25
- extend Forwardable
26
50
  include AIPP::Debugger
27
51
 
28
- # Error when URL results in "404 Not Found" HTTP status
52
+ # Error raised when any kind of downloader fails to find the resource e.g.
53
+ # because the local file does not exist or the remote file is unavailable.
29
54
  class NotFoundError < StandardError; end
30
55
 
31
56
  # @return [Pathname] directory to operate within
@@ -44,9 +69,15 @@ module AIPP
44
69
  fail(ArgumentError, 'bad storage directory') unless Dir.exist? storage
45
70
  @source_file = sources_path.join("#{@source}.zip")
46
71
  prepare
47
- unzip if @source_file.exist?
72
+ if @source_file.exist?
73
+ if AIPP.options.clean
74
+ @source_file.delete
75
+ else
76
+ unpack
77
+ end
78
+ end
48
79
  yield self
49
- zip
80
+ pack
50
81
  ensure
51
82
  teardown
52
83
  end
@@ -59,17 +90,14 @@ module AIPP
59
90
  # Download and read +document+
60
91
  #
61
92
  # @param document [String] document to read (without extension)
62
- # @param url [String] URL to download the document from
63
- # @param type [Symbol, nil] document type: +nil+ (default) to derive it from
64
- # the URL, :html, :pdf, :xlsx, :ods or :csv
65
- # @return [Nokogiri::HTML5::Document, AIPP::PDF, Roo::Spreadsheet]
66
- def read(document:, url:, type: nil)
67
- type ||= Pathname(URI(url).path).extname[1..-1].to_sym
68
- file = work_path.join([document, type].join('.'))
93
+ # @param origin [AIPP::Downloader::File, AIPP::Downloader::HTTP,
94
+ # AIPP::Downloader::GraphQL] origin to download the document from
95
+ # @return [Object]
96
+ def read(document:, origin:)
97
+ file = work_path.join(origin.fetched_file)
69
98
  unless file.exist?
70
99
  verbose_info "downloading #{document}"
71
- uri = URI.open(url)
72
- IO.copy_stream(uri, file)
100
+ origin.fetch_to(work_path)
73
101
  end
74
102
  convert file
75
103
  end
@@ -97,13 +125,11 @@ module AIPP
97
125
  end
98
126
  end
99
127
 
100
- def unzip
101
- Zip::File.open(source_file).each do |entry|
102
- entry.extract(work_path.join(entry.name))
103
- end
128
+ def unpack
129
+ extract(source_file) or fail
104
130
  end
105
131
 
106
- def zip
132
+ def pack
107
133
  backup_file = source_file.sub(/$/, '.old') if source_file.exist?
108
134
  source_file.rename(backup_file) if backup_file
109
135
  Zip::File.open(source_file, Zip::File::CREATE) do |zip|
@@ -114,15 +140,38 @@ module AIPP
114
140
  backup_file&.delete
115
141
  end
116
142
 
143
+ def extract(archive, only_entry: nil)
144
+ case archive.extname
145
+ when '.zip' then unzip(archive, only_entry: only_entry)
146
+ else fail(ArgumentError, "unrecognized archive type")
147
+ end
148
+ end
149
+
150
+ # @return [Boolean] whether at least one file was extracted
151
+ def unzip(archive, only_entry:)
152
+ Zip::File.open(archive).inject(false) do |_, entry|
153
+ case
154
+ when only_entry && only_entry == entry.name
155
+ break !!entry.extract(work_path.join(Pathname(entry.name).basename))
156
+ when !only_entry
157
+ !!entry.extract(work_path.join(entry.name))
158
+ else
159
+ false
160
+ end
161
+ end
162
+ end
163
+
117
164
  def convert(file)
118
165
  case file.extname
119
- when '.xml' then Nokogiri.XML(File.open(file))
120
- when '.html' then Nokogiri.HTML5(File.open(file))
166
+ when '.xml', '.ofmx' then Nokogiri.XML(::File.open(file), &:noblanks)
167
+ when '.html' then Nokogiri.HTML5(::File.open(file))
168
+ when '.json' then JSON.load_file(file)
121
169
  when '.pdf' then AIPP::PDF.new(file)
122
170
  when '.xlsx', '.ods', '.csv' then Roo::Spreadsheet.open(file.to_s)
123
- else
124
- fail(ArgumentError, "invalid document type")
171
+ when '.txt' then ::File.read(file)
172
+ else fail(ArgumentError, "unrecognized file type")
125
173
  end
126
174
  end
175
+
127
176
  end
128
177
  end
@@ -0,0 +1,88 @@
1
+ module AIPP
2
+
3
+ # Runtime environment
4
+ #
5
+ # Runtime environment objects inherit from OpenStruct but feature some
6
+ # extensions:
7
+ #
8
+ # * Use +replace+ to replace the current key/value table with the given hash.
9
+ # * Use +merge+ to merge the given hash into the current key/value hash.
10
+ # * When reading a value using square brackets, the key is implicitly
11
+ # converted to Symbol.
12
+ #
13
+ # @example
14
+ # AIPP.config # => AIPP::Environment::Config
15
+ # AIPP.config.foo # => nil
16
+ # AIPP.config.foo = :bar # => :bar
17
+ # AIPP.config.replace(fii: :bir)
18
+ # AIPP.config.foo # => nil
19
+ # AIPP.config.fii # => :bir
20
+ # AIPP.config.read! # method defined on Config class
21
+ class Environment
22
+ include Singleton
23
+
24
+ # Cache to store transient objects
25
+ class Cache < OpenStruct
26
+ def [](key)
27
+ super(key.to_s.to_sym)
28
+ end
29
+
30
+ def replace(hash)
31
+ @table = hash
32
+ end
33
+
34
+ def merge(hash)
35
+ @table.merge! hash
36
+ end
37
+ end
38
+
39
+ # Borders read from directory containing GeoJSON files
40
+ class Borders < Cache
41
+ def read!(dir)
42
+ @table.clear
43
+ dir.glob('*.geojson').each do |file|
44
+ @table[file.basename('.geojson').to_s.to_sym] = AIPP::Border.from_file(file)
45
+ end
46
+ end
47
+ end
48
+
49
+ # Fixtures read from directory containing YAML files
50
+ class Fixtures < Cache
51
+ def read!(dir)
52
+ @table.clear
53
+ dir.glob('*.yml').each do |file|
54
+ @table[file.basename('.yml').to_s.to_sym] = YAML.load_file(file)
55
+ end
56
+ end
57
+ end
58
+
59
+ # Options set via the CLI executable
60
+ class Options < Cache
61
+ end
62
+
63
+ # Config read from config.yml file
64
+ class Config < Cache
65
+ def read!(file)
66
+ @table = YAML.safe_load_file(file, symbolize_names: true, fallback: {}) if file.exist?
67
+ @table[:namespace] ||= SecureRandom.uuid
68
+ end
69
+
70
+ def write!(file)
71
+ File.write(file, @table.transform_keys(&:to_s).to_yaml)
72
+ end
73
+ end
74
+
75
+ def initialize
76
+ [Cache, Borders, Fixtures, Options, Config].each do |klass|
77
+ attribute = klass.to_s.split('::').last.downcase
78
+ instance_variable_set("@#{attribute}", klass.new)
79
+ AIPP.define_singleton_method(attribute) do
80
+ Environment.instance.instance_variable_get "@#{attribute}"
81
+ end
82
+ end
83
+ end
84
+
85
+ end
86
+ end
87
+
88
+ AIPP::Environment.instance