stac 0.2.0 → 0.3.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +10 -0
  4. data/GETTING_STARTED.md +384 -0
  5. data/Gemfile.lock +48 -37
  6. data/README.md +12 -59
  7. data/Rakefile +62 -4
  8. data/Steepfile +2 -0
  9. data/lib/stac/asset.rb +3 -1
  10. data/lib/stac/catalog.rb +78 -8
  11. data/lib/stac/collection.rb +43 -9
  12. data/lib/stac/common_metadata.rb +1 -1
  13. data/lib/stac/errors.rb +2 -5
  14. data/lib/stac/extension.rb +34 -0
  15. data/lib/stac/extensions/electro_optical.rb +67 -0
  16. data/lib/stac/extensions/projection.rb +42 -0
  17. data/lib/stac/extensions/scientific_citation.rb +84 -0
  18. data/lib/stac/extensions/view_geometry.rb +38 -0
  19. data/lib/stac/extent.rb +39 -31
  20. data/lib/stac/file_writer.rb +31 -0
  21. data/lib/stac/hash_like.rb +74 -0
  22. data/lib/stac/item.rb +56 -22
  23. data/lib/stac/link.rb +51 -9
  24. data/lib/stac/object_resolver.rb +4 -4
  25. data/lib/stac/properties.rb +3 -1
  26. data/lib/stac/provider.rb +5 -1
  27. data/lib/stac/simple_http_client.rb +3 -0
  28. data/lib/stac/stac_object.rb +138 -36
  29. data/lib/stac/version.rb +1 -1
  30. data/lib/stac.rb +12 -1
  31. data/sig/stac/asset.rbs +1 -3
  32. data/sig/stac/catalog.rbs +28 -5
  33. data/sig/stac/collection.rbs +13 -5
  34. data/sig/stac/common_metadata.rbs +2 -2
  35. data/sig/stac/errors.rbs +1 -4
  36. data/sig/stac/extension.rbs +12 -0
  37. data/sig/stac/extensions/electro_optical.rbs +40 -0
  38. data/sig/stac/extensions/projection.rbs +32 -0
  39. data/sig/stac/extensions/scientific_citation.rbs +38 -0
  40. data/sig/stac/extensions/view_geometry.rbs +22 -0
  41. data/sig/stac/extent.rbs +13 -16
  42. data/sig/stac/file_writer.rbs +13 -0
  43. data/sig/stac/hash_like.rbs +13 -0
  44. data/sig/stac/item.rbs +16 -7
  45. data/sig/stac/link.rbs +20 -12
  46. data/sig/stac/object_resolver.rbs +2 -2
  47. data/sig/stac/properties.rbs +1 -3
  48. data/sig/stac/provider.rbs +2 -3
  49. data/sig/stac/simple_http_client.rbs +3 -0
  50. data/sig/stac/stac_object.rbs +33 -10
  51. data/stac.gemspec +1 -1
  52. metadata +18 -3
data/lib/stac/extent.rb CHANGED
@@ -1,18 +1,50 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'hash_like'
4
+
3
5
  module STAC
4
- # Represents \STAC extent object, which describes the spatio-temporal extents of a Collection.
6
+ # Represents \STAC extent object, which describes the spatio-temporal extents of a collection.
5
7
  #
6
8
  # Specification: https://github.com/radiantearth/stac-spec/blob/master/collection-spec/collection-spec.md#extent-object
7
9
  class Extent
8
- # Describes the spatial extents of a Collection
10
+ include HashLike
11
+
12
+ class << self
13
+ # Deserializes an Extent from a Hash.
14
+ def from_hash(hash)
15
+ transformed = hash.transform_keys(&:to_sym)
16
+ transformed[:spatial] = Spatial.from_hash(transformed.fetch(:spatial))
17
+ transformed[:temporal] = Temporal.from_hash(transformed.fetch(:temporal))
18
+ new(**transformed)
19
+ end
20
+ end
21
+
22
+ attr_accessor :spatial, :temporal
23
+
24
+ def initialize(spatial:, temporal:, **extra)
25
+ @spatial = spatial
26
+ @temporal = temporal
27
+ @extra = extra.transform_keys(&:to_s)
28
+ end
29
+
30
+ # Serializes self to a Hash.
31
+ def to_h
32
+ {
33
+ 'spatial' => spatial.to_h,
34
+ 'temporal' => temporal.to_h,
35
+ }.merge(extra)
36
+ end
37
+
38
+ # Describes the spatial extents of a collection
9
39
  class Spatial
40
+ include HashLike
41
+
10
42
  # Deserializes a Spatial from a Hash.
11
43
  def self.from_hash(hash)
12
44
  new(**hash.transform_keys(&:to_sym))
13
45
  end
14
46
 
15
- attr_accessor :bbox, :extra
47
+ attr_accessor :bbox
16
48
 
17
49
  def initialize(bbox:, **extra)
18
50
  @bbox = bbox
@@ -27,14 +59,16 @@ module STAC
27
59
  end
28
60
  end
29
61
 
30
- # Describes the temporal extents of a Collection.
62
+ # Describes the temporal extents of a collection.
31
63
  class Temporal
64
+ include HashLike
65
+
32
66
  # Deserializes a Temporal from a Hash.
33
67
  def self.from_hash(hash)
34
68
  new(**hash.transform_keys(&:to_sym))
35
69
  end
36
70
 
37
- attr_accessor :interval, :extra
71
+ attr_accessor :interval
38
72
 
39
73
  def initialize(interval:, **extra)
40
74
  @interval = interval
@@ -48,31 +82,5 @@ module STAC
48
82
  }.merge(extra)
49
83
  end
50
84
  end
51
-
52
- class << self
53
- # Deserializes an Extent from a Hash.
54
- def from_hash(hash)
55
- transformed = hash.transform_keys(&:to_sym)
56
- transformed[:spatial] = Spatial.from_hash(transformed.fetch(:spatial))
57
- transformed[:temporal] = Temporal.from_hash(transformed.fetch(:temporal))
58
- new(**transformed)
59
- end
60
- end
61
-
62
- attr_accessor :spatial, :temporal, :extra
63
-
64
- def initialize(spatial:, temporal:, **extra)
65
- @spatial = spatial
66
- @temporal = temporal
67
- @extra = extra.transform_keys(&:to_s)
68
- end
69
-
70
- # Serializes self to a Hash.
71
- def to_h
72
- {
73
- 'spatial' => spatial.to_h,
74
- 'temporal' => temporal.to_h,
75
- }.merge(extra)
76
- end
77
85
  end
78
86
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'json'
5
+ require 'uri'
6
+ require_relative 'errors'
7
+
8
+ module STAC
9
+ # Class to write a hash as JSON on a file.
10
+ class FileWriter
11
+ def initialize(hash_to_json: ->(hash) { JSON.generate(hash) })
12
+ @hash_to_json = hash_to_json
13
+ end
14
+
15
+ # Creates a file on `dest` with the given hash as JSON.
16
+ def write(hash, dest:)
17
+ dest_uri = URI.parse(dest)
18
+ path = if dest_uri.relative?
19
+ dest
20
+ elsif dest_uri.is_a?(URI::File)
21
+ dest_uri.path.to_s
22
+ else
23
+ raise NotSupportedURISchemeError, "not supported URI scheme: #{dest}"
24
+ end
25
+
26
+ pathname = Pathname(path)
27
+ pathname.dirname.mkpath
28
+ pathname.write(@hash_to_json.call(hash))
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module STAC
6
+ # Enables included class to behave like Hash.
7
+ module HashLike
8
+ # Extra fields that do not belong to the STAC core specification.
9
+ attr_reader :extra
10
+
11
+ # When there is an attribute with the given name, returns the attribute value.
12
+ # Otherwise, calls `extra [key]`.
13
+ def [](key)
14
+ if respond_to?(key) && method(key).arity.zero?
15
+ public_send(key)
16
+ else
17
+ extra[key.to_s]
18
+ end
19
+ end
20
+
21
+ # When there is an attribute writer with the given name, assigns the value to the attribute.
22
+ # Otherwise, adds the given key-value pair to `extra` hash.
23
+ def []=(key, value)
24
+ method = "#{key}="
25
+ if respond_to?(method)
26
+ public_send(method, value)
27
+ else
28
+ extra[key.to_s] = value
29
+ end
30
+ end
31
+
32
+ # Sets the attributes (like ActiveModel::AttributeAssignment#assign_attributes)
33
+ # or merges the args into `extra` hash (like Hash#update).
34
+ def update(**options)
35
+ options.each do |key, value|
36
+ self[key] = value
37
+ end
38
+ self
39
+ end
40
+
41
+ def to_hash # :nodoc:
42
+ to_h
43
+ end
44
+
45
+ # Serializes self to a Hash.
46
+ def to_h
47
+ extra
48
+ end
49
+
50
+ # Serializes self to a JSON string.
51
+ def to_json(...)
52
+ to_h.to_json(...)
53
+ end
54
+
55
+ # Returns `true` if all of the followings are true:
56
+ # - the given object is an instance of tha same class
57
+ # - `self.to_hash == other.to_hash`
58
+ #
59
+ # Otherwise, returns `false`.
60
+ def ==(other)
61
+ other.instance_of?(self.class) && to_hash == other.to_hash
62
+ end
63
+
64
+ # Returns a copy of self by serializes self to a JSON and desirializes it by `.from_hash`.
65
+ def deep_dup
66
+ unless self.class.respond_to?(:from_hash)
67
+ raise NotImplementedError, "#{self.class} must implement `.from_hash(hash)` to use `HashLike#deep_dup`"
68
+ end
69
+
70
+ hash = JSON.parse(to_json)
71
+ self.class.from_hash(hash)
72
+ end
73
+ end
74
+ end
data/lib/stac/item.rb CHANGED
@@ -10,25 +10,26 @@ module STAC
10
10
  #
11
11
  # \STAC \Item Specification: https://github.com/radiantearth/stac-spec/tree/master/item-spec
12
12
  class Item < STAC::STACObject
13
- self.type = 'Feature'
13
+ @type = 'Feature'
14
14
 
15
15
  class << self
16
16
  def from_hash(hash)
17
- h = hash.dup
18
- h['properties'] = Properties.from_hash(h.fetch('properties'))
19
- h['assets'] = h.fetch('assets').transform_values { |v| Asset.from_hash(v) }
17
+ h = hash.transform_keys(&:to_sym)
18
+ h[:properties] = Properties.from_hash(h.fetch(:properties, {}))
19
+ h[:assets] = h.fetch(:assets, {}).transform_values { |v| Asset.from_hash(v) }
20
20
  super(h)
21
21
  rescue KeyError => e
22
22
  raise ArgumentError, "required field not found: #{e.key}"
23
23
  end
24
24
  end
25
25
 
26
- attr_accessor :id, :geometry, :bbox, :properties, :assets, :collection_id
26
+ attr_accessor :id, :geometry, :bbox, :collection_id
27
+
28
+ attr_reader :properties, :assets
27
29
 
28
30
  def initialize(
29
- id:, geometry:, properties:, links:, assets:, bbox: nil, collection: nil, stac_extensions: nil, **extra
31
+ id:, geometry:, properties:, links: [], assets: {}, bbox: nil, collection: nil, stac_extensions: [], **extra
30
32
  )
31
- super(links: links, stac_extensions: stac_extensions, **extra)
32
33
  @id = id
33
34
  @geometry = geometry
34
35
  @properties = properties
@@ -40,6 +41,16 @@ module STAC
40
41
  else
41
42
  @collection_id = collection
42
43
  end
44
+ super(links: links, stac_extensions: stac_extensions, **extra)
45
+ end
46
+
47
+ def [](key)
48
+ value = super
49
+ if value.nil?
50
+ properties.extra[key.to_s]
51
+ else
52
+ value
53
+ end
43
54
  end
44
55
 
45
56
  def to_h
@@ -58,11 +69,6 @@ module STAC
58
69
  )
59
70
  end
60
71
 
61
- # Returns datetime from #properties.
62
- def datetime
63
- properties.datetime
64
- end
65
-
66
72
  # Returns a rel="collection" link as a collection object if it exists.
67
73
  def collection
68
74
  link = find_link(rel: 'collection')
@@ -71,17 +77,45 @@ module STAC
71
77
 
72
78
  # Overwrites rel="collection" link and #collection_id attribute.
73
79
  def collection=(collection)
74
- raise ArgumentError, 'collection must have a rel="self" link' unless (collection_href = collection.self_href)
75
-
76
80
  @collection_id = collection.id
77
- collection_link = Link.new(
78
- rel: 'collection',
79
- href: collection_href,
80
- type: 'application/json',
81
- title: collection.title,
82
- )
83
- remove_link(rel: 'collection')
84
- add_link(collection_link)
81
+ remove_links(rel: 'collection')
82
+ add_link(collection, rel: 'collection', type: 'application/json', title: collection.title)
83
+ end
84
+
85
+ # Adds an asset with the given key.
86
+ #
87
+ # When the item has extendable stac_extensions, make the asset extend the extension modules.
88
+ def add_asset(key:, href:, title: nil, description: nil, type: nil, roles: nil, **extra)
89
+ asset = Asset.new(href: href, title: title, description: description, type: type, roles: roles, **extra)
90
+ extensions.each do |extension|
91
+ asset.extend(extension::Asset) if extension.const_defined?(:Asset)
92
+ end
93
+ assets[key] = asset
94
+ self
95
+ end
96
+
97
+ private
98
+
99
+ def respond_to_missing?(symbol, include_all)
100
+ if properties.respond_to?(symbol)
101
+ true
102
+ else
103
+ super
104
+ end
105
+ end
106
+
107
+ def method_missing(symbol, *args, **options, &block)
108
+ if properties.respond_to?(symbol)
109
+ properties.public_send(symbol, *args, **options, &block)
110
+ else
111
+ super
112
+ end
113
+ end
114
+
115
+ def apply_extension!(extension)
116
+ super
117
+ properties.extend(extension::Properties) if extension.const_defined?(:Properties)
118
+ assets.each_value { |asset| asset.extend(extension::Asset) } if extension.const_defined?(:Asset)
85
119
  end
86
120
  end
87
121
  end
data/lib/stac/link.rb CHANGED
@@ -3,10 +3,23 @@
3
3
  require 'pathname'
4
4
  require 'uri'
5
5
  require_relative 'errors'
6
+ require_relative 'hash_like'
6
7
 
7
8
  module STAC
9
+ # Raised when a link does not have href or owner.
10
+ class LinkHrefError < Error
11
+ attr_reader :link
12
+
13
+ def initialize(msg = nil, link:)
14
+ super(msg)
15
+ @link = link
16
+ end
17
+ end
18
+
8
19
  # Represents \STAC link object, which describes a relationship with another entity.
9
20
  class Link
21
+ include HashLike
22
+
10
23
  class << self
11
24
  # Deserializes a Link from a Hash.
12
25
  def from_hash(hash)
@@ -14,14 +27,15 @@ module STAC
14
27
  end
15
28
  end
16
29
 
17
- attr_accessor :rel, :href, :type, :title, :extra
30
+ attr_accessor :rel, :type, :title
31
+
32
+ attr_writer :href
18
33
 
19
34
  # Owner object of this link.
20
35
  attr_accessor :owner
21
36
 
22
- attr_writer :resolver # :nodoc:
23
-
24
- def initialize(rel:, href:, type: nil, title: nil, **extra)
37
+ def initialize(target = nil, rel:, href:, type: nil, title: nil, **extra)
38
+ @target = target
25
39
  @rel = rel
26
40
  @href = href
27
41
  @type = type
@@ -39,14 +53,30 @@ module STAC
39
53
  }.merge(extra).compact
40
54
  end
41
55
 
56
+ # Determines if the link's target is a resolved STACObject.
57
+ def resolved?
58
+ !@target.nil?
59
+ end
60
+
61
+ def href
62
+ @href || @target&.self_href
63
+ end
64
+
42
65
  # Returns the absolute HREF for this link.
43
- #
44
- # When it could not assemble the absolute HREF, it returns nil.
45
66
  def absolute_href
46
- if URI(href).absolute?
47
- href
67
+ if URI(href!).absolute?
68
+ href!
69
+ elsif (base_href = owner&.self_href)
70
+ Pathname(base_href).dirname.join(href!).to_s
71
+ end
72
+ end
73
+
74
+ # Returns the relative HREF for this link.
75
+ def relative_href
76
+ if URI(href!).relative?
77
+ href!
48
78
  elsif (base_href = owner&.self_href)
49
- Pathname(base_href).dirname.join(href).to_s
79
+ Pathname(href!).relative_path_from(Pathname(base_href).dirname).to_s
50
80
  end
51
81
  end
52
82
 
@@ -60,5 +90,17 @@ module STAC
60
90
  object
61
91
  end
62
92
  end
93
+
94
+ def href! # :nodoc:
95
+ href or raise LinkHrefError.new('href is nil', link: self)
96
+ end
97
+
98
+ def absolute_href! # :nodoc:
99
+ absolute_href or raise LinkHrefError.new('could not assemble absolute href', link: self)
100
+ end
101
+
102
+ def relative_href! # :nodoc:
103
+ relative_href or raise LinkHrefError.new('could not assemble relative href', link: self)
104
+ end
63
105
  end
64
106
  end
@@ -11,10 +11,10 @@ module STAC
11
11
  class ObjectResolver
12
12
  class << self
13
13
  # Resolvable classes. Default is Catalog, Collection and Item.
14
- attr_accessor :resolvables
14
+ attr_reader :resolvables
15
15
  end
16
16
 
17
- self.resolvables = [Catalog, Collection, Item]
17
+ @resolvables = [Catalog, Collection, Item]
18
18
 
19
19
  attr_reader :http_client
20
20
 
@@ -30,7 +30,7 @@ module STAC
30
30
  # - file
31
31
  #
32
32
  # Raises:
33
- # - STAC::UnknownURISchemeError when a URL with unsupported scheme was given
33
+ # - STAC::NotSupportedURISchemeError when a URL with not supported scheme was given
34
34
  # - STAC::TypeError when it could not resolve any \STAC objects
35
35
  def resolve(url)
36
36
  hash = read(url)
@@ -53,7 +53,7 @@ module STAC
53
53
  str = File.read(uri.path.to_s)
54
54
  JSON.parse(str)
55
55
  else
56
- raise UnknownURISchemeError, "unknown URI scheme: #{url}"
56
+ raise NotSupportedURISchemeError, "not supported URI scheme: #{url}"
57
57
  end
58
58
  end
59
59
  end
@@ -2,12 +2,14 @@
2
2
 
3
3
  require 'time'
4
4
  require_relative 'common_metadata'
5
+ require_relative 'hash_like'
5
6
 
6
7
  module STAC
7
8
  # Represents \STAC properties object, which is additional metadata for Item.
8
9
  #
9
10
  # Specification: https://github.com/radiantearth/stac-spec/blob/master/item-spec/item-spec.md#properties-object
10
11
  class Properties
12
+ include HashLike
11
13
  include CommonMetadata
12
14
 
13
15
  class << self
@@ -23,7 +25,7 @@ module STAC
23
25
 
24
26
  def initialize(datetime:, **extra)
25
27
  @datetime = datetime
26
- self.extra = extra.transform_keys(&:to_s)
28
+ @extra = extra.transform_keys(&:to_s)
27
29
  end
28
30
 
29
31
  # Serializes self to a Hash.
data/lib/stac/provider.rb CHANGED
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'hash_like'
4
+
3
5
  module STAC
4
6
  # Represents \STAC provider object, which provides information about a provider.
5
7
  #
6
8
  # Specicication: https://github.com/radiantearth/stac-spec/blob/master/collection-spec/collection-spec.md#provider-object
7
9
  class Provider
10
+ include HashLike
11
+
8
12
  class << self
9
13
  # Deserializes a Provider from a Hash.
10
14
  def from_hash(hash)
@@ -12,7 +16,7 @@ module STAC
12
16
  end
13
17
  end
14
18
 
15
- attr_accessor :name, :description, :roles, :url, :extra
19
+ attr_accessor :name, :description, :roles, :url
16
20
 
17
21
  def initialize(name:, description: nil, roles: nil, url: nil, **extra)
18
22
  @name = name
@@ -6,6 +6,9 @@ require_relative 'errors'
6
6
  require_relative 'version'
7
7
 
8
8
  module STAC
9
+ # Raised when a HTTP request failed.
10
+ class HTTPError < Error; end
11
+
9
12
  # Simple HTTP Client using OpenURI.
10
13
  class SimpleHTTPClient
11
14
  attr_reader :options