aipp 0.2.4 → 1.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 (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