aipp 0.2.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -0
  3. data/CHANGELOG.md +38 -0
  4. data/README.md +222 -88
  5. data/exe/aip2aixm +2 -2
  6. data/exe/aip2ofmx +2 -2
  7. data/lib/aipp/aip.rb +113 -31
  8. data/lib/aipp/border.rb +77 -46
  9. data/lib/aipp/debugger.rb +101 -0
  10. data/lib/aipp/downloader.rb +39 -26
  11. data/lib/aipp/executable.rb +41 -22
  12. data/lib/aipp/parser.rb +94 -21
  13. data/lib/aipp/patcher.rb +5 -2
  14. data/lib/aipp/pdf.rb +1 -1
  15. data/lib/aipp/regions/LF/README.md +49 -0
  16. data/lib/aipp/regions/LF/aerodromes.rb +223 -0
  17. data/lib/aipp/regions/LF/d_p_r_airspaces.rb +56 -0
  18. data/lib/aipp/regions/LF/dangerous_activities.rb +49 -0
  19. data/lib/aipp/regions/LF/designated_points.rb +47 -0
  20. data/lib/aipp/regions/LF/fixtures/aerodromes.yml +608 -0
  21. data/lib/aipp/regions/LF/helipads.rb +122 -0
  22. data/lib/aipp/regions/LF/helpers/base.rb +218 -0
  23. data/lib/aipp/regions/LF/helpers/surface.rb +49 -0
  24. data/lib/aipp/regions/LF/helpers/usage_limitation.rb +20 -0
  25. data/lib/aipp/regions/LF/navigational_aids.rb +85 -0
  26. data/lib/aipp/regions/LF/obstacles.rb +153 -0
  27. data/lib/aipp/regions/LF/serviced_airspaces.rb +70 -0
  28. data/lib/aipp/regions/LF/services.rb +172 -0
  29. data/lib/aipp/t_hash.rb +4 -5
  30. data/lib/aipp/version.rb +1 -1
  31. data/lib/aipp.rb +11 -5
  32. data/lib/core_ext/enumerable.rb +9 -9
  33. data/lib/core_ext/hash.rb +21 -5
  34. data/lib/core_ext/nokogiri.rb +54 -0
  35. data/lib/core_ext/string.rb +38 -66
  36. data.tar.gz.sig +2 -0
  37. metadata +180 -188
  38. metadata.gz.sig +0 -0
  39. data/.gitignore +0 -8
  40. data/.ruby-version +0 -1
  41. data/.travis.yml +0 -8
  42. data/.yardopts +0 -3
  43. data/Guardfile +0 -7
  44. data/TODO.md +0 -6
  45. data/aipp.gemspec +0 -44
  46. data/gems.rb +0 -3
  47. data/lib/aipp/airac.rb +0 -55
  48. data/lib/aipp/regions/LF/AD-1.3.rb +0 -162
  49. data/lib/aipp/regions/LF/AD-1.6.rb +0 -31
  50. data/lib/aipp/regions/LF/AD-2.rb +0 -313
  51. data/lib/aipp/regions/LF/AD-3.1.rb +0 -185
  52. data/lib/aipp/regions/LF/ENR-2.1.rb +0 -92
  53. data/lib/aipp/regions/LF/ENR-4.1.rb +0 -97
  54. data/lib/aipp/regions/LF/ENR-4.3.rb +0 -28
  55. data/lib/aipp/regions/LF/ENR-5.1.rb +0 -75
  56. data/lib/aipp/regions/LF/ENR-5.5.rb +0 -53
  57. data/lib/aipp/regions/LF/fixtures/AD-1.3.yml +0 -511
  58. data/lib/aipp/regions/LF/fixtures/AD-2.yml +0 -185
  59. data/lib/aipp/regions/LF/fixtures/AD-3.1.yml +0 -10
  60. data/lib/aipp/regions/LF/helpers/AD_radio.rb +0 -90
  61. data/lib/aipp/regions/LF/helpers/URL.rb +0 -26
  62. data/lib/aipp/regions/LF/helpers/common.rb +0 -217
  63. data/lib/core_ext/object.rb +0 -43
  64. data/rakefile.rb +0 -12
  65. data/spec/fixtures/archive.zip +0 -0
  66. data/spec/fixtures/border.geojson +0 -201
  67. data/spec/fixtures/document.pdf +0 -0
  68. data/spec/fixtures/document.pdf.json +0 -1
  69. data/spec/fixtures/new.html +0 -6
  70. data/spec/fixtures/new.pdf +0 -0
  71. data/spec/fixtures/new.txt +0 -1
  72. data/spec/lib/aipp/airac_spec.rb +0 -98
  73. data/spec/lib/aipp/border_spec.rb +0 -135
  74. data/spec/lib/aipp/downloader_spec.rb +0 -81
  75. data/spec/lib/aipp/patcher_spec.rb +0 -46
  76. data/spec/lib/aipp/pdf_spec.rb +0 -124
  77. data/spec/lib/aipp/t_hash_spec.rb +0 -44
  78. data/spec/lib/aipp/version_spec.rb +0 -7
  79. data/spec/lib/core_ext/enumberable_spec.rb +0 -76
  80. data/spec/lib/core_ext/hash_spec.rb +0 -27
  81. data/spec/lib/core_ext/integer_spec.rb +0 -15
  82. data/spec/lib/core_ext/nil_class_spec.rb +0 -11
  83. data/spec/lib/core_ext/string_spec.rb +0 -112
  84. data/spec/sounds/failure.mp3 +0 -0
  85. data/spec/sounds/success.mp3 +0 -0
  86. data/spec/spec_helper.rb +0 -28
data/lib/aipp/aip.rb CHANGED
@@ -3,13 +3,18 @@ module AIPP
3
3
  # @abstract
4
4
  class AIP
5
5
  extend Forwardable
6
+ include AIPP::Debugger
6
7
  include AIPP::Patcher
7
8
 
8
9
  DEPENDS = []
9
10
 
10
- # @return [String] AIP name (e.g. "ENR-2.1")
11
+ # @return [String] AIP name (equal to the parser file name without its
12
+ # file extension such as "ENR-2.1" implemented in the file "ENR-2.1.rb")
11
13
  attr_reader :aip
12
14
 
15
+ # @return [String] AIP file as passed and possibly updated by `url_for`
16
+ attr_reader :aip_file
17
+
13
18
  # @return [Object] Fixture read from YAML file
14
19
  attr_reader :fixture
15
20
 
@@ -30,55 +35,132 @@ module AIPP
30
35
 
31
36
  def initialize(aip:, downloader:, fixture:, parser:)
32
37
  @aip, @downloader, @fixture, @parser = aip, downloader, fixture, parser
33
- self.class.include ("AIPP::%s::Helpers::URL" % options[:region]).constantize
38
+ setup if respond_to? :setup
39
+ end
40
+
41
+ # @return [String]
42
+ def inspect
43
+ "#<AIPP::AIP #{aip}>"
34
44
  end
35
45
 
36
46
  # Read an AIP source file
37
47
  #
38
- # Depending on whether a local copy of the file exists, either:
39
- # * download from URL to local storage and read from local archive
40
- # * read from local archive
48
+ # Read the cached source file if it exists in the source archive. Otherwise,
49
+ # download it from URL and cache it.
41
50
  #
42
- # An URL builder method +url_for(aip_file)+ must be defined either in
43
- # +helper.rb+ or in the AIP parser definition (e.g. +ENR-2.1.rb+).
51
+ # An URL builder method +url_for(aip_file)+ must be implemented by the AIP
52
+ # parser definition (e.g. +ENR-2.1.rb+).
44
53
  #
45
- # @param aip_file [String] e.g. "ENR-2.1" or "AD-2.LFMV" (default: +aip+)
46
- # @return [Nokogiri::HTML5::Document, String] HTML as Nokogiri document,
47
- # PDF or TXT as String
54
+ # @param aip_file [String] e.g. "ENR-2.1" or "AD-2.LFMV" (default: +aip+
55
+ # with section stripped e.g. "AD-1.3-2" -> "AD-1.3")
56
+ # @return [Nokogiri::XML::Document, Nokogiri::HTML5::Document,
57
+ # Roo::Spreadsheet, String] XML/HTML as Nokogiri document, XLSX/ODS/CSV
58
+ # as Roo document, PDF and TXT as String
48
59
  def read(aip_file=nil)
49
- aip_file ||= aip
50
- @downloader.read(document: aip_file, url: url_for(aip_file))
60
+ @aip_file = aip_file || aip.remove(/(?<![A-Z])-\d+$/)
61
+ url = url_for(@aip_file) # may update aip_file string
62
+ @downloader.read(document: @aip_file, url: url)
51
63
  end
52
64
 
53
65
  # Add feature to AIXM
54
66
  #
55
67
  # @param feature [AIXM::Feature] e.g. airport or airspace
68
+ # @return [AIXM::Feature] added feature
56
69
  def add(feature)
57
- verbose_info "Adding #{feature.inspect}"
58
- aixm.features << feature
70
+ verbose_info "adding #{feature.inspect}"
71
+ aixm.add_feature feature
72
+ feature
59
73
  end
60
74
 
61
- # Search features previously written to AIXM and return those matching the
62
- # given class and attribute values
75
+ # @!method find_by(klass, attributes={})
76
+ # Find objects of the given class and optionally with the given attribute
77
+ # values previously written to AIXM.
78
+ #
79
+ # @note This method is delegated to +AIXM::Association::Array+.
80
+ # @see https://www.rubydoc.info/gems/aixm/AIXM/Association/Array#find_by-instance_method
81
+ #
82
+ # @!method find(object)
83
+ # Find equal objects previously written to AIXM.
84
+ #
85
+ # @note This method is delegated to +AIXM::Association::Array+.
86
+ # @see https://www.rubydoc.info/gems/aixm/AIXM/Association/Array#find-instance_method
87
+ %i(find_by find).each do |method|
88
+ define_method method do |*args|
89
+ aixm.features.send(method, *args)
90
+ end
91
+ end
92
+
93
+ # @overload given(*objects)
94
+ # Return +objects+ unless at least one of them equals nil
95
+ #
96
+ # @example
97
+ # # Instead of this:
98
+ # first, last = unless ((first = expensive_first).nil? || (last = expensive_last).nil?)
99
+ # [first, last]
100
+ # end
101
+ #
102
+ # # Use the following:
103
+ # first, last = given(expensive_first, expensive_last)
104
+ #
105
+ # @param *objects [Array<Object>] any objects really
106
+ # @return [Object] nil if at least one of the objects is nil, given
107
+ # objects otherwise
108
+ #
109
+ # @overload given(*objects)
110
+ # Yield +objects+ unless at least one of them equals nil
111
+ #
112
+ # @example
113
+ # # Instead of this:
114
+ # name = unless ((first = expensive_first.nil? || (last = expensive_last.nil?)
115
+ # "#{first} #{last}"
116
+ # end
117
+ #
118
+ # # Use any of the following:
119
+ # name = given(expensive_first, expensive_last) { |f, l| "#{f} #{l}" }
120
+ # name = given(expensive_first, expensive_last) { "#{_1} #{_2}" }
121
+ #
122
+ # @param *objects [Array<Object>] any objects really
123
+ # @yield [Array<Object>] objects passed as parameter
124
+ # @return [Object] nil if at least one of the objects is nil, return of
125
+ # block otherwise
126
+ def given(*objects)
127
+ if objects.none?(&:nil?)
128
+ block_given? ? yield(*objects) : objects
129
+ end
130
+ end
131
+
132
+ # Build and optionally check a Markdown link
63
133
  #
64
134
  # @example
65
- # select(:airport, id: "LFNT")
66
- #
67
- # @param klass [Class, Symbol] feature class like AIXM::Feature::Airport or
68
- # AIXM::Feature::NavigationalAid::VOR, shorthand notations as symbols
69
- # e.g. :airport or :vor as listed in AIXM::CLASSES are recognized as well
70
- # @param attributes [Hash] filter by these attributes and their values
71
- # @return [Array<AIXM::Feature>]
72
- def select(klass, attributes={})
73
- klass = AIXM::CLASSES.fetch(klass) if klass.is_a? Symbol
74
- aixm.features.select do |feature|
75
- if feature.is_a? klass
76
- attributes.reduce(true) do |memo, (attribute, value)|
77
- memo && feature.send(attribute) == value
78
- end
135
+ # options[:check_links] = false
136
+ # link_to('foo', 'https://bar.com/exists') # => "[foo](https://bar.com/exists)"
137
+ # link_to('foo', 'https://bar.com/not-found') # => "[foo](https://bar.com/not-found)"
138
+ # options[:check_links] = true
139
+ # link_to('foo', 'https://bar.com/exists') # => "[foo](https://bar.com/exists)"
140
+ # link_to('foo', 'https://bar.com/not-found') # => nil
141
+ #
142
+ # @params body [String] body text of the link
143
+ # @params url [String] URL of the link
144
+ # @return [String, nil] Markdown link
145
+ def link_to(body, url)
146
+ "[#{body}](#{url})" if !options[:check_links] || url_exists?(url)
147
+ end
148
+
149
+ private
150
+
151
+ def url_exists?(url)
152
+ uri = URI.parse(url)
153
+ Net::HTTP.new(uri.host, uri.port).tap do |request|
154
+ request.use_ssl = (uri.scheme == 'https')
155
+ path = uri.path.present? ? uri.path : '/'
156
+ result = request.request_head(path)
157
+ if result.kind_of? Net::HTTPRedirection
158
+ url_exist?(result['location'])
159
+ else
160
+ result.code == '200'
79
161
  end
80
162
  end
81
163
  end
82
- end
83
164
 
165
+ end
84
166
  end
data/lib/aipp/border.rb CHANGED
@@ -1,51 +1,90 @@
1
1
  module AIPP
2
2
 
3
- # Border GeoJSON file reader
3
+ # Custom border geometries
4
4
  #
5
- # The border GeoJSON files must be a geometry collection of one or more
6
- # line strings:
7
- #
8
- # {
9
- # "type": "GeometryCollection",
10
- # "geometries": [
11
- # {
12
- # "type": "LineString",
13
- # "coordinates": [
14
- # [6.009531650000042, 45.12013319700009],
15
- # [6.015747738000073, 45.12006702600007]
16
- # ]
17
- # }
18
- # ]
19
- # }
20
- #
21
- # @example
22
- # border = AIPP::Border.new("/path/to/file.geojson")
23
- # border.geometries
24
- # # => [[#<AIXM::XY 45.12013320N 006.00953165E>, <AIXM::XY 45.12006703N 006.01574774E>]]
5
+ # The border consists of one ore more open or closed geometries which are
6
+ # defined by either a GeoJSON file or arrays of coordinate pairs.
25
7
  class Border
26
- attr_reader :file
8
+
9
+ # @return [Array<AIXM::XY>]
27
10
  attr_reader :geometries
28
11
 
29
- def initialize(file)
30
- @file = file.is_a?(Pathname) ? file : Pathname(file)
31
- fail(ArgumentError, "file must have extension .geojson") unless @file.extname == '.geojson'
32
- @geometries = load_geometries
12
+ def initialize(geometries)
13
+ @geometries = geometries
33
14
  end
34
15
 
35
- # @return [String]
36
- def inspect
37
- %Q(#<#{self.class} file=#{@file}>)
16
+ class << self
17
+ undef_method :new
18
+
19
+ # New border object from GeoJSON file
20
+ #
21
+ # The border GeoJSON files must be a geometry collection of one or more
22
+ # line strings:
23
+ #
24
+ # {
25
+ # "type": "GeometryCollection",
26
+ # "geometries": [
27
+ # {
28
+ # "type": "LineString",
29
+ # "coordinates": [
30
+ # [6.009531650000042, 45.12013319700009],
31
+ # [6.015747738000073, 45.12006702600007]
32
+ # ]
33
+ # }
34
+ # ]
35
+ # }
36
+ #
37
+ # Please note that GeoJSON orders coordinate tuples in mathematical order
38
+ # as `[longitude, latitude]`!
39
+ #
40
+ # @param file [Pathname, String] GeoJSON file
41
+ #
42
+ # @example
43
+ # border = AIPP::Border.from_file("/path/to/national_park.geojson")
44
+ # border.geometries
45
+ # # => [[#<AIXM::XY 45.12013320N 006.00953165E>, <AIXM::XY 45.12006703N 006.01574774E>]]
46
+ def from_file(file)
47
+ file = Pathname(file) unless file.is_a? Pathname
48
+ fail(ArgumentError, "file must have extension .geojson") unless file.extname == '.geojson'
49
+ geometries = JSON.load(file)['geometries'].map do |collection|
50
+ collection['coordinates'].map do |long, lat|
51
+ AIXM.xy(lat: lat, long: long)
52
+ end
53
+ end
54
+ allocate.instance_eval do
55
+ initialize(geometries)
56
+ self
57
+ end
58
+ end
59
+
60
+ # New border object from array of points
61
+ #
62
+ # The array must contain coordinate tuples in geographical order as
63
+ # `latitude longitude` separated by whitespace and/or commas.
64
+ #
65
+ # @param array [Array<Array<String>>] one or more arrays of coordinate pairs
66
+ #
67
+ # @example
68
+ # border = AIPP::Border.from_array([["45.1201332 6.00953165", "45.12006703 6.01574774"]])
69
+ # border.geometries
70
+ # # => [[#<AIXM::XY 45.12013320N 006.00953165E>, <AIXM::XY 45.12006703N 006.01574774E>]]
71
+ def from_array(array)
72
+ geometries = array.map do |collection|
73
+ collection.map do |coordinates|
74
+ lat, long = coordinates.split(/[\s,]+/)
75
+ AIXM.xy(lat: lat.to_f, long: long.to_f)
76
+ end
77
+ end
78
+ allocate.instance_eval do
79
+ initialize(geometries)
80
+ self
81
+ end
82
+ end
38
83
  end
39
84
 
40
- # Name of the border
41
- #
42
- # By convention, the name of the border is taken from the filename with
43
- # both the extension .geojson and all non alphanumeric characters dropped
44
- # and the resulting string upcased.
45
- #
46
85
  # @return [String]
47
- def name
48
- @file.basename('.geojson').to_s.gsub(/\W/, '').upcase
86
+ def inspect
87
+ %Q(#<#{self.class} #{@geometries.count} geometries>)
49
88
  end
50
89
 
51
90
  # Whether the given geometry is closed or not
@@ -72,7 +111,7 @@ module AIPP
72
111
  @geometries.each.with_index do |geometry, g_index|
73
112
  next unless geometry_index.nil? || geometry_index == g_index
74
113
  geometry.each.with_index do |coordinates, c_index|
75
- distance = xy.distance(coordinates).dist
114
+ distance = xy.distance(coordinates).dim
76
115
  if distance < min_distance
77
116
  position = Position.new(geometries: geometries, geometry_index: g_index, coordinates_index: c_index)
78
117
  min_distance = distance
@@ -107,14 +146,6 @@ module AIPP
107
146
 
108
147
  private
109
148
 
110
- def load_geometries
111
- JSON.load(@file)['geometries'].map do |line_string|
112
- line_string['coordinates'].map do |long, lat|
113
- AIXM.xy(long: long, lat: lat)
114
- end
115
- end
116
- end
117
-
118
149
  # Position defines an exact point on a border
119
150
  #
120
151
  # @example
@@ -0,0 +1,101 @@
1
+ module AIPP
2
+ module Debugger
3
+
4
+ # Start a debugger session and watch for warnings etc
5
+ #
6
+ # @note The debugger session persists beyond the scope of the given block
7
+ # because there's no +DEBUGGER__.stop+ as of now.
8
+ #
9
+ # @example
10
+ # include AIPP::Debugger
11
+ # with_debugger(verbose: true) do
12
+ # (...)
13
+ # warn("all hell broke loose", severe: true)
14
+ # end
15
+ #
16
+ # @overload with_debugger(debug_on_warning:, debug_on_error:, verbose:, &block)
17
+ # @param debug_on_warning [Boolean, Integer] start a debugger session
18
+ # which opens a console on the warning with the given integer ID or on
19
+ # all warnings if +true+ is given
20
+ # @param debug_on_error [Boolean] start a debugger session which opens
21
+ # a console when an error is raised (postmortem)
22
+ # @param verbose [Boolean] print verbose info, print unsevere warnings
23
+ # and re-raise rescued errors
24
+ # @yield Block the debugger is watching
25
+ def with_debugger(**options, &)
26
+ DEBUGGER__.instance_variable_set(:@options__, options.merge(counter: 0))
27
+ case
28
+ when id = debugger_options[:debug_on_warning]
29
+ puts instructions_for(@id == true ? 'warning' : "warning #{id}")
30
+ DEBUGGER__::start(no_sigint_hook: true, nonstop: true)
31
+ call_with_rescue(&)
32
+ when debugger_options[:debug_on_error]
33
+ puts instructions_for('error')
34
+ DEBUGGER__::start(no_sigint_hook: true, nonstop: true, postmortem: true)
35
+ call_without_rescue(&)
36
+ else
37
+ DEBUGGER__::start(no_sigint_hook: true, nonstop: true)
38
+ call_with_rescue(&)
39
+ end
40
+ end
41
+
42
+ def self.included(*)
43
+ DEBUGGER__.instance_variable_set(:@options__, {})
44
+ end
45
+
46
+ # Issue a warning and maybe open a debug session.
47
+ #
48
+ # @param message [String] warning message
49
+ # @param severe [Boolean] whether this problem must be fixed or not
50
+ alias_method :original_warn, :warn
51
+ 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]
56
+ end
57
+ end
58
+
59
+ # Issue an informational message.
60
+ #
61
+ # @param message [String] informational message
62
+ # @param color [Symbol] message color
63
+ def info(message, color: nil)
64
+ puts color ? message.upcase_first.send(color) : message.upcase_first
65
+ end
66
+
67
+ # Issue a verbose informational message.
68
+ #
69
+ # @param message [String] verbose informational message
70
+ # @param color [Symbol] message color
71
+ def verbose_info(message, color: :blue)
72
+ info(message, color: color) if debugger_options[:verbose]
73
+ end
74
+
75
+ private
76
+
77
+ def debugger_options
78
+ DEBUGGER__.instance_variable_get(:@options__)
79
+ end
80
+
81
+ def call_with_rescue(&block)
82
+ block.call
83
+ rescue => error
84
+ message = error.respond_to?(:original_message) ? error.original_message : error.message
85
+ puts "ERROR: #{message}".magenta
86
+ raise if debugger_options[:verbose]
87
+ end
88
+
89
+ def call_without_rescue(&block)
90
+ block.call
91
+ end
92
+
93
+ def instructions_for(trigger)
94
+ <<~END.strip.red
95
+ Debug on #{trigger} enabled.
96
+ Remember: Type "up" to enter caller frames.
97
+ END
98
+ end
99
+
100
+ end
101
+ end
@@ -3,15 +3,15 @@ module AIPP
3
3
  # AIP downloader infrastructure
4
4
  #
5
5
  # The downloader operates in the +storage+ directory where it creates two
6
- # subdirectories "archive" and "work". The initializer looks for +archive+
7
- # in "archives" and (if found) unzips its contents into "work". When reading
8
- # a +document+, the downloader looks for the +document+ in "work" and
6
+ # subdirectories "sources" and "work". The initializer looks for the +source+
7
+ # archive in "sources" and (if found) unzips its contents into "work". When
8
+ # reading a +document+, the downloader looks for the +document+ in "work" and
9
9
  # (unless found) downloads it from +url+. HTML documents are parsed to
10
10
  # +Nokogiri::HTML5::Document+, PDF documents are parsed to +AIPP::PDF+.
11
- # Finally, the contents of "work" are written back to +archive+.
11
+ # Finally, the contents of "work" are written back to the +source+ archive.
12
12
  #
13
13
  # @example
14
- # AIPP::Downloader.new(storage: options[:storage], archive: "2018-11-08") do |downloader|
14
+ # AIPP::Downloader.new(storage: options[:storage], source: "2018-11-08") do |downloader|
15
15
  # html = downloader.read(
16
16
  # document: 'ENR-5.1',
17
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'
@@ -22,51 +22,62 @@ module AIPP
22
22
  # )
23
23
  # end
24
24
  class Downloader
25
+ extend Forwardable
26
+ include AIPP::Debugger
27
+
28
+ # Error when URL results in "404 Not Found" HTTP status
29
+ class NotFoundError < StandardError; end
25
30
 
26
31
  # @return [Pathname] directory to operate within
27
32
  attr_reader :storage
28
33
 
29
- # @return [String] name of the archive (without extension ".zip")
30
- attr_reader :archive
34
+ # @return [String] name of the source archive (without extension ".zip")
35
+ attr_reader :source
31
36
 
32
- # @return [Pathname] full path to the archive
33
- attr_reader :archive_file
37
+ # @return [Pathname] full path to the source archive
38
+ attr_reader :source_file
34
39
 
35
40
  # @param storage [Pathname] directory to operate within
36
- # @param archive [String] name of the archive (without extension ".zip")
37
- def initialize(storage:, archive:)
38
- @storage, @archive = storage, archive
41
+ # @param source [String] name of the source archive (without extension ".zip")
42
+ def initialize(storage:, source:)
43
+ @storage, @source = storage, source
39
44
  fail(ArgumentError, 'bad storage directory') unless Dir.exist? storage
40
- @archive_file = archives_path.join("#{@archive}.zip")
45
+ @source_file = sources_path.join("#{@source}.zip")
41
46
  prepare
42
- unzip if @archive_file.exist?
47
+ unzip if @source_file.exist?
43
48
  yield self
44
49
  zip
45
50
  ensure
46
51
  teardown
47
52
  end
48
53
 
54
+ # @return [String]
55
+ def inspect
56
+ "#<AIPP::Downloader>"
57
+ end
58
+
49
59
  # Download and read +document+
50
60
  #
51
61
  # @param document [String] document to read (without extension)
52
62
  # @param url [String] URL to download the document from
53
63
  # @param type [Symbol, nil] document type: +nil+ (default) to derive it from
54
- # the URL, :html, or :pdf
55
- # @return [Nokogiri::HTML5::Document, AIPP::PDF]
64
+ # the URL, :html, :pdf, :xlsx, :ods or :csv
65
+ # @return [Nokogiri::HTML5::Document, AIPP::PDF, Roo::Spreadsheet]
56
66
  def read(document:, url:, type: nil)
57
67
  type ||= Pathname(URI(url).path).extname[1..-1].to_sym
58
68
  file = work_path.join([document, type].join('.'))
59
69
  unless file.exist?
60
- verbose_info "Downloading #{document}"
61
- IO.copy_stream(Kernel.open(url), file)
70
+ verbose_info "downloading #{document}"
71
+ uri = URI.open(url)
72
+ IO.copy_stream(uri, file)
62
73
  end
63
74
  convert file
64
75
  end
65
76
 
66
77
  private
67
78
 
68
- def archives_path
69
- @storage.join('archives')
79
+ def sources_path
80
+ @storage.join('sources')
70
81
  end
71
82
 
72
83
  def work_path
@@ -75,7 +86,7 @@ module AIPP
75
86
 
76
87
  def prepare
77
88
  teardown
78
- archives_path.mkpath
89
+ sources_path.mkpath
79
90
  work_path.mkpath
80
91
  end
81
92
 
@@ -87,15 +98,15 @@ module AIPP
87
98
  end
88
99
 
89
100
  def unzip
90
- Zip::File.open(archive_file).each do |entry|
101
+ Zip::File.open(source_file).each do |entry|
91
102
  entry.extract(work_path.join(entry.name))
92
103
  end
93
104
  end
94
105
 
95
106
  def zip
96
- backup_file = archive_file.sub(/$/, '.old') if archive_file.exist?
97
- archive_file.rename(backup_file) if backup_file
98
- Zip::File.open(archive_file, Zip::File::CREATE) do |zip|
107
+ backup_file = source_file.sub(/$/, '.old') if source_file.exist?
108
+ source_file.rename(backup_file) if backup_file
109
+ Zip::File.open(source_file, Zip::File::CREATE) do |zip|
99
110
  work_path.children.each do |entry|
100
111
  zip.add(entry.basename.to_s, entry) unless entry.basename.to_s[0] == '.'
101
112
  end
@@ -105,8 +116,10 @@ module AIPP
105
116
 
106
117
  def convert(file)
107
118
  case file.extname
108
- when '.html' then Nokogiri.HTML5(file)
119
+ when '.xml' then Nokogiri.XML(File.open(file))
120
+ when '.html' then Nokogiri.HTML5(File.open(file))
109
121
  when '.pdf' then AIPP::PDF.new(file)
122
+ when '.xlsx', '.ods', '.csv' then Roo::Spreadsheet.open(file.to_s)
110
123
  else
111
124
  fail(ArgumentError, "invalid document type")
112
125
  end