capsium 0.1.1 → 0.2.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.
- checksums.yaml +4 -4
- data/.github/workflows/rake.yml +1 -4
- data/.github/workflows/release.yml +1 -2
- data/.gitignore +1 -0
- data/.rubocop.yml +34 -3
- data/CHANGELOG.md +58 -0
- data/Gemfile +4 -4
- data/LICENSE +21 -0
- data/README.adoc +177 -135
- data/Rakefile +8 -1
- data/bin/console +1 -1
- data/capsium.gemspec +16 -23
- data/exe/capsium +1 -1
- data/lib/capsium/cli/convert.rb +18 -0
- data/lib/capsium/cli/package.rb +86 -0
- data/lib/capsium/cli/reactor.rb +25 -0
- data/lib/capsium/cli.rb +5 -79
- data/lib/capsium/converters/jekyll.rb +62 -31
- data/lib/capsium/converters.rb +7 -0
- data/lib/capsium/package/dataset.rb +76 -56
- data/lib/capsium/package/manifest.rb +41 -50
- data/lib/capsium/package/manifest_config.rb +64 -0
- data/lib/capsium/package/metadata.rb +11 -29
- data/lib/capsium/package/metadata_config.rb +105 -0
- data/lib/capsium/package/routes.rb +51 -111
- data/lib/capsium/package/routes_config.rb +150 -0
- data/lib/capsium/package/security.rb +123 -0
- data/lib/capsium/package/security_config.rb +60 -0
- data/lib/capsium/package/storage.rb +26 -26
- data/lib/capsium/package/storage_config.rb +106 -0
- data/lib/capsium/package/validator.rb +149 -0
- data/lib/capsium/package.rb +84 -70
- data/lib/capsium/packager.rb +60 -62
- data/lib/capsium/reactor/introspection.rb +88 -0
- data/lib/capsium/reactor.rb +108 -36
- data/lib/capsium/version.rb +1 -1
- data/lib/capsium.rb +7 -9
- data/sig/capsium/package/dataset.rbs +28 -0
- data/sig/capsium/package/manifest.rbs +34 -0
- data/sig/capsium/package/manifest_config.rbs +38 -0
- data/sig/capsium/package/metadata.rbs +26 -0
- data/sig/capsium/package/metadata_config.rbs +49 -0
- data/sig/capsium/package/routes.rbs +37 -0
- data/sig/capsium/package/routes_config.rbs +74 -0
- data/sig/capsium/package/security.rbs +65 -0
- data/sig/capsium/package/security_config.rbs +57 -0
- data/sig/capsium/package/storage.rbs +28 -0
- data/sig/capsium/package/storage_config.rbs +61 -0
- data/sig/capsium/package/validator.rbs +29 -0
- data/sig/capsium/package.rbs +53 -0
- data/sig/capsium/packager.rbs +23 -0
- data/sig/capsium/reactor/introspection.rbs +31 -0
- data/sig/capsium/reactor.rbs +30 -0
- data/sig/capsium.rbs +3 -1
- metadata +63 -146
- data/images/005df7415a331c466ad2d9a42efdd212b6f42fe50b5fd3b3174c86c706f58244.png +0 -0
- data/images/0374025b3af99b8a473282c8cbbf9fcd29573cf41e586982f328f86c0690f43d.png +0 -0
- data/images/0bb4da785be40ef58e219470ebccb979325928b75453dc46bac23c6ee8a7a7cb.png +0 -0
- data/images/6aa294dccc81af594aacbe804e7ddffdc17eacc28357338108aea5d021d831ff.png +0 -0
- data/images/72dd3fbf3f4b475e27a0e7fb8137c475c32c41f8d222bcf62d6a9ccf102d9532.png +0 -0
- data/images/8772b6961d169738d7b0fa0b669b06fc2f40632d4c62586c7634fc17b93182a3.png +0 -0
- data/images/a998d842405933d45723606ff3f70162ec95b4ef30db25464a366184fd08fb9b.png +0 -0
- data/images/aa8980547e8c003d33273ab4d80e62da7f317bd7581b293c06d67f5331f24f31.png +0 -0
- data/images/bb78a872b539e0e9b2d80dee58acbb688f3f2727b324a5bf8bf417a69d94a166.png +0 -0
- data/images/c48fc83b17725d85fbb64d971196ebfccd8c5c757fe6aa5845303f6e315879b6.png +0 -0
- data/images/f08ef07308d08119ac2124bb7428c8bef17ef1ca70045696604d6e83015a9b91.png +0 -0
- data/images/f7514206111b695647eae9adfcf498ba3e0ff83ecfe25f3fc3ed8e9f04c5c726-1.png +0 -0
- data/images/f7514206111b695647eae9adfcf498ba3e0ff83ecfe25f3fc3ed8e9f04c5c726.png +0 -0
- data/lib/capsium/protector.rb +0 -95
|
@@ -1,60 +1,63 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require "json"
|
|
3
|
+
require "forwardable"
|
|
5
4
|
require "marcel"
|
|
6
|
-
require "
|
|
5
|
+
require "pathname"
|
|
7
6
|
|
|
8
7
|
module Capsium
|
|
9
8
|
class Package
|
|
10
9
|
class Manifest
|
|
11
|
-
|
|
10
|
+
extend Forwardable
|
|
11
|
+
|
|
12
|
+
attr_reader :path, :content_path, :config
|
|
13
|
+
|
|
14
|
+
def_delegators :@config, :to_hash
|
|
12
15
|
|
|
13
16
|
def initialize(path)
|
|
14
17
|
@path = path
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
ManifestData.from_json(File.read(path))
|
|
22
|
-
else
|
|
23
|
-
ManifestData.new(content: generate_manifest)
|
|
24
|
-
end
|
|
18
|
+
@content_path = File.join(File.dirname(@path), Package::CONTENT_DIR)
|
|
19
|
+
@config = if File.exist?(path)
|
|
20
|
+
ManifestConfig.from_json(File.read(path))
|
|
21
|
+
else
|
|
22
|
+
ManifestConfig.new(resources: generate_manifest)
|
|
23
|
+
end
|
|
25
24
|
end
|
|
26
25
|
|
|
26
|
+
# Auto-generation (ARCHITECTURE.md section 3): scan content/
|
|
27
|
+
# recursively, detect MIME types, default visibility "exported",
|
|
28
|
+
# deterministic (sorted) output.
|
|
27
29
|
def generate_manifest
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
content_files.sort.to_h do |file_path|
|
|
31
|
+
[relative_path(file_path),
|
|
32
|
+
Resource.new(type: mime_from_path(file_path), visibility: "exported")]
|
|
30
33
|
end
|
|
34
|
+
end
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
file: relative_path(file_path),
|
|
35
|
-
mime: mime_from_path(file_path)
|
|
36
|
-
)
|
|
37
|
-
end
|
|
36
|
+
def resources
|
|
37
|
+
@config.resources
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
def lookup(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
def lookup(path)
|
|
41
|
+
resources[path]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def type_for(path)
|
|
45
|
+
resource = lookup(path)
|
|
46
|
+
resource&.type
|
|
44
47
|
end
|
|
45
48
|
|
|
46
49
|
def to_json(*_args)
|
|
47
|
-
@
|
|
50
|
+
@config.to_json
|
|
48
51
|
end
|
|
49
52
|
|
|
50
53
|
def save_to_file(output_path = @path)
|
|
51
|
-
File.
|
|
52
|
-
file.write(to_json)
|
|
53
|
-
end
|
|
54
|
+
File.write(output_path, to_json)
|
|
54
55
|
end
|
|
55
56
|
|
|
56
57
|
def path_to_content_file(path)
|
|
57
|
-
|
|
58
|
+
raise TypeError, "Path cannot be nil" if path.nil?
|
|
59
|
+
|
|
60
|
+
Pathname.new(File.dirname(@path)).join(path)
|
|
58
61
|
end
|
|
59
62
|
|
|
60
63
|
def content_file_exists?(path)
|
|
@@ -62,11 +65,17 @@ module Capsium
|
|
|
62
65
|
end
|
|
63
66
|
|
|
64
67
|
def relative_path(path)
|
|
65
|
-
Pathname.new(path).relative_path_from(@
|
|
68
|
+
Pathname.new(path).relative_path_from(File.dirname(@path)).to_s
|
|
66
69
|
end
|
|
67
70
|
|
|
68
71
|
private
|
|
69
72
|
|
|
73
|
+
def content_files
|
|
74
|
+
Dir.glob(File.join(@content_path, "**", "*")).select do |file|
|
|
75
|
+
File.file?(file)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
70
79
|
def mime_from_path(path)
|
|
71
80
|
Marcel::MimeType.for(
|
|
72
81
|
Pathname.new(path),
|
|
@@ -75,23 +84,5 @@ module Capsium
|
|
|
75
84
|
)
|
|
76
85
|
end
|
|
77
86
|
end
|
|
78
|
-
|
|
79
|
-
class ManifestDataItem < Shale::Mapper
|
|
80
|
-
attribute :file, Shale::Type::String
|
|
81
|
-
attribute :mime, Shale::Type::String
|
|
82
|
-
|
|
83
|
-
# json do
|
|
84
|
-
# map "file", to: :file
|
|
85
|
-
# map "mime", to: :mime
|
|
86
|
-
# end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
class ManifestData < Shale::Mapper
|
|
90
|
-
attribute :content, ManifestDataItem, collection: true
|
|
91
|
-
|
|
92
|
-
# json do
|
|
93
|
-
# map "content", to: :content
|
|
94
|
-
# end
|
|
95
|
-
end
|
|
96
87
|
end
|
|
97
88
|
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "lutaml/model"
|
|
5
|
+
|
|
6
|
+
module Capsium
|
|
7
|
+
class Package
|
|
8
|
+
# A single manifest resource entry (ARCHITECTURE.md section 3).
|
|
9
|
+
class Resource < Lutaml::Model::Serializable
|
|
10
|
+
VISIBILITIES = %w[exported private].freeze
|
|
11
|
+
|
|
12
|
+
attribute :type, :string
|
|
13
|
+
attribute :visibility, :string, values: VISIBILITIES, default: "exported"
|
|
14
|
+
attribute :version, :string
|
|
15
|
+
|
|
16
|
+
json do
|
|
17
|
+
map :type, to: :type
|
|
18
|
+
map :visibility, to: :visibility
|
|
19
|
+
map :version, to: :version
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Canonical manifest.json model: an object keyed by package-relative
|
|
24
|
+
# resource path. The legacy gem form ({"content": [{file, mime}]}) is
|
|
25
|
+
# accepted on read and normalized; writers emit only the object form.
|
|
26
|
+
class ManifestConfig < Lutaml::Model::Serializable
|
|
27
|
+
attribute :resources, :hash, default: {}
|
|
28
|
+
|
|
29
|
+
json do
|
|
30
|
+
map :resources, with: { from: :resources_from_json, to: :resources_to_json }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.from_json(json)
|
|
34
|
+
doc = JSON.parse(json)
|
|
35
|
+
doc["resources"] ||= legacy_resources(doc.delete("content")) if doc.key?("content")
|
|
36
|
+
super(JSON.generate(doc))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.legacy_resources(content)
|
|
40
|
+
(content || []).to_h do |item|
|
|
41
|
+
[item["file"], { "type" => item["mime"], "visibility" => "exported" }]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
private_class_method :legacy_resources
|
|
45
|
+
|
|
46
|
+
def resources_from_json(model, value)
|
|
47
|
+
model.resources = (value || {}).to_h do |path, attributes|
|
|
48
|
+
[path, Resource.from_json(JSON.generate(attributes))]
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def resources_to_json(model, doc)
|
|
53
|
+
doc["resources"] = model.resources.sort.to_h do |path, resource|
|
|
54
|
+
[path, JSON.parse(resource.to_json)]
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def sort!
|
|
59
|
+
self.resources = resources.sort.to_h
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -1,47 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# lib/capsium/package/metadata.rb
|
|
4
|
-
require "shale"
|
|
5
3
|
require "forwardable"
|
|
6
4
|
|
|
7
5
|
module Capsium
|
|
8
6
|
class Package
|
|
9
|
-
class Dependency < Shale::Mapper
|
|
10
|
-
attribute :name, Shale::Type::String
|
|
11
|
-
attribute :version, Shale::Type::String
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
class MetadataData < Shale::Mapper
|
|
15
|
-
attribute :name, Shale::Type::String
|
|
16
|
-
attribute :version, Shale::Type::String
|
|
17
|
-
attribute :dependencies, Dependency, collection: true
|
|
18
|
-
end
|
|
19
|
-
|
|
20
7
|
class Metadata
|
|
21
|
-
attr_reader :path, :
|
|
8
|
+
attr_reader :path, :config
|
|
22
9
|
|
|
23
10
|
extend Forwardable
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
11
|
+
|
|
12
|
+
def_delegators :@config, :to_json, :to_hash, :name, :version,
|
|
13
|
+
:description, :guid, :uuid, :author, :license,
|
|
14
|
+
:repository, :dependencies
|
|
27
15
|
|
|
28
16
|
def initialize(path)
|
|
29
17
|
@path = path
|
|
30
|
-
@
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def to_json(*_args)
|
|
38
|
-
@data.to_json
|
|
18
|
+
@config = if File.exist?(path)
|
|
19
|
+
MetadataData.from_json(File.read(path))
|
|
20
|
+
else
|
|
21
|
+
MetadataData.new
|
|
22
|
+
end
|
|
39
23
|
end
|
|
40
24
|
|
|
41
25
|
def save_to_file(output_path = @path)
|
|
42
|
-
File.
|
|
43
|
-
file.write(to_json)
|
|
44
|
-
end
|
|
26
|
+
File.write(output_path, to_json)
|
|
45
27
|
end
|
|
46
28
|
end
|
|
47
29
|
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "lutaml/model"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Capsium
|
|
8
|
+
class Package
|
|
9
|
+
class Repository < Lutaml::Model::Serializable
|
|
10
|
+
attribute :type, :string
|
|
11
|
+
attribute :url, :string
|
|
12
|
+
|
|
13
|
+
json do
|
|
14
|
+
map :type, to: :type
|
|
15
|
+
map :url, to: :url
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Canonical metadata.json model (ARCHITECTURE.md section 2).
|
|
20
|
+
#
|
|
21
|
+
# The legacy gem form of "dependencies" (an array of {name, version}
|
|
22
|
+
# objects) is accepted on read and normalized to the canonical object
|
|
23
|
+
# form; writers emit only the object form.
|
|
24
|
+
class MetadataData < Lutaml::Model::Serializable
|
|
25
|
+
KEBAB_CASE_PATTERN = /\A[a-z0-9]+(-[a-z0-9]+)*\z/
|
|
26
|
+
SEMVER_PATTERN = /\A\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?\z/
|
|
27
|
+
UUID_PATTERN = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
|
|
28
|
+
|
|
29
|
+
attribute :name, :string
|
|
30
|
+
attribute :version, :string
|
|
31
|
+
attribute :description, :string
|
|
32
|
+
attribute :guid, :string
|
|
33
|
+
attribute :uuid, :string
|
|
34
|
+
attribute :author, :string
|
|
35
|
+
attribute :license, :string
|
|
36
|
+
attribute :repository, Repository
|
|
37
|
+
attribute :dependencies, :hash, default: {}
|
|
38
|
+
attribute :read_only, :boolean
|
|
39
|
+
|
|
40
|
+
json do
|
|
41
|
+
map :name, to: :name
|
|
42
|
+
map :version, to: :version
|
|
43
|
+
map :description, to: :description
|
|
44
|
+
map :guid, to: :guid
|
|
45
|
+
map :uuid, to: :uuid
|
|
46
|
+
map :author, to: :author
|
|
47
|
+
map :license, to: :license
|
|
48
|
+
map :repository, to: :repository
|
|
49
|
+
map :dependencies, to: :dependencies
|
|
50
|
+
map "readOnly", to: :read_only
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.from_json(json)
|
|
54
|
+
doc = JSON.parse(json)
|
|
55
|
+
doc["dependencies"] = normalize_dependencies(doc["dependencies"])
|
|
56
|
+
super(JSON.generate(doc))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.normalize_dependencies(dependencies)
|
|
60
|
+
return {} if dependencies.nil?
|
|
61
|
+
return dependencies unless dependencies.is_a?(Array)
|
|
62
|
+
|
|
63
|
+
dependencies.to_h { |dep| [dep["name"], dep["version"]] }
|
|
64
|
+
end
|
|
65
|
+
private_class_method :normalize_dependencies
|
|
66
|
+
|
|
67
|
+
# Field-level format validations (ARCHITECTURE.md section 2).
|
|
68
|
+
# Returns a list of human-readable problems; empty when valid.
|
|
69
|
+
def format_errors
|
|
70
|
+
presence_errors + format_field_errors
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def presence_errors
|
|
76
|
+
problems = []
|
|
77
|
+
problems << "name is missing" if name.to_s.empty?
|
|
78
|
+
problems << "version is missing" if version.to_s.empty?
|
|
79
|
+
problems << "description is missing" if description.to_s.empty?
|
|
80
|
+
problems << "guid is missing" if guid.to_s.empty?
|
|
81
|
+
problems << "uuid is missing" if uuid.to_s.empty?
|
|
82
|
+
problems
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def format_field_errors
|
|
86
|
+
problems = []
|
|
87
|
+
problems << "name must be kebab-case" if invalid?(name, KEBAB_CASE_PATTERN)
|
|
88
|
+
problems << "version must be semver" if invalid?(version, SEMVER_PATTERN)
|
|
89
|
+
problems << "guid must be a URI" if guid && !uri?(guid)
|
|
90
|
+
problems << "uuid is not a valid UUID" if invalid?(uuid, UUID_PATTERN)
|
|
91
|
+
problems
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def invalid?(value, pattern)
|
|
95
|
+
!value.nil? && !value.match?(pattern)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def uri?(value)
|
|
99
|
+
URI.parse(value).is_a?(URI::Generic)
|
|
100
|
+
rescue URI::InvalidURIError
|
|
101
|
+
false
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -1,156 +1,96 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require "json"
|
|
5
|
-
require "fileutils"
|
|
3
|
+
require "forwardable"
|
|
6
4
|
|
|
7
5
|
module Capsium
|
|
8
6
|
class Package
|
|
9
|
-
class
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def fs_path(manifest)
|
|
13
|
-
manifest.path_to_content_file(manifest.lookup(file).file)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def mime(manifest)
|
|
17
|
-
manifest.lookup(file).mime
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def validate(manifest)
|
|
21
|
-
target_path = fs_path(manifest)
|
|
22
|
-
return if File.exist?(target_path)
|
|
23
|
-
|
|
24
|
-
raise "Route target does not exist: #{target_path}"
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
class Route < Shale::Mapper
|
|
29
|
-
attribute :path, Shale::Type::String
|
|
30
|
-
attribute :target, RouteTarget
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
class RoutesData < Shale::Mapper
|
|
34
|
-
attribute :routes, Route, collection: true
|
|
35
|
-
|
|
36
|
-
def resolve(route)
|
|
37
|
-
routes.detect do |r|
|
|
38
|
-
r.path == route
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def add(route, target)
|
|
43
|
-
target = RouteTarget.new(file: target) if target.is_a?(String)
|
|
44
|
-
|
|
45
|
-
@routes << Route.new(path: route, target: target)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def update(route, updated_route, _updated_target)
|
|
49
|
-
r = @routes.resolve(route)
|
|
50
|
-
r.path = updated_route
|
|
51
|
-
r.target = target
|
|
52
|
-
r
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def remove(route)
|
|
56
|
-
r = @routes.resolve(route)
|
|
57
|
-
@routes.remove(r)
|
|
58
|
-
end
|
|
7
|
+
class Routes
|
|
8
|
+
extend Forwardable
|
|
59
9
|
|
|
60
|
-
|
|
61
|
-
@routes.sort_by!(&:path)
|
|
62
|
-
self
|
|
63
|
-
end
|
|
64
|
-
end
|
|
10
|
+
attr_reader :path, :config, :manifest, :storage
|
|
65
11
|
|
|
66
|
-
|
|
67
|
-
attr_reader :path, :data, :index, :manifest
|
|
12
|
+
def_delegators :@config, :to_hash
|
|
68
13
|
|
|
69
|
-
ROUTES_FILE = "routes.json"
|
|
70
|
-
DEFAULT_INDEX_TARGET = "index.html"
|
|
71
14
|
INDEX_ROUTE = "/"
|
|
15
|
+
INDEX_RESOURCE = "content/index.html"
|
|
16
|
+
DATASET_ROUTE_PREFIX = "/api/v1/data/"
|
|
72
17
|
|
|
73
|
-
def initialize(path, manifest)
|
|
18
|
+
def initialize(path, manifest, storage)
|
|
74
19
|
@path = path
|
|
75
|
-
@dir = File.dirname(path)
|
|
76
20
|
@manifest = manifest
|
|
77
|
-
@
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
validate
|
|
21
|
+
@storage = storage
|
|
22
|
+
@config = if File.exist?(path)
|
|
23
|
+
RoutesConfig.from_json(File.read(path))
|
|
24
|
+
else
|
|
25
|
+
generate_routes
|
|
26
|
+
end
|
|
84
27
|
end
|
|
85
28
|
|
|
86
29
|
def resolve(url_path)
|
|
87
|
-
@
|
|
30
|
+
@config.resolve(url_path)
|
|
88
31
|
end
|
|
89
32
|
|
|
90
33
|
def add_route(route, target)
|
|
91
|
-
|
|
92
|
-
@data.add(route, target)
|
|
34
|
+
@config.add(route, target)
|
|
93
35
|
end
|
|
94
36
|
|
|
95
37
|
def update_route(route, updated_route, updated_target)
|
|
96
|
-
|
|
97
|
-
@data.update(route, updated_route, updated_target)
|
|
38
|
+
@config.update(route, updated_route, updated_target)
|
|
98
39
|
end
|
|
99
40
|
|
|
100
41
|
def remove_route(route)
|
|
101
|
-
@
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def validate
|
|
105
|
-
@data.routes.each do |route|
|
|
106
|
-
route.target.validate(@manifest)
|
|
107
|
-
end
|
|
42
|
+
@config.remove(route)
|
|
108
43
|
end
|
|
109
44
|
|
|
110
45
|
def to_json(*_args)
|
|
111
|
-
@
|
|
46
|
+
@config.to_json
|
|
112
47
|
end
|
|
113
48
|
|
|
114
49
|
def save_to_file(output_path = @path)
|
|
115
|
-
File.
|
|
116
|
-
file.write(to_json)
|
|
117
|
-
end
|
|
50
|
+
File.write(output_path, to_json)
|
|
118
51
|
end
|
|
119
52
|
|
|
120
|
-
|
|
53
|
+
# Auto-generation (ARCHITECTURE.md section 4): every manifest
|
|
54
|
+
# resource gets a route at its path relative to content/; HTML files
|
|
55
|
+
# get two routes (basename without extension and full filename); the
|
|
56
|
+
# index HTML additionally gets "/"; every dataset in storage gets
|
|
57
|
+
# /api/v1/data/<id>. Output is deterministic (sorted by path).
|
|
58
|
+
def generate_routes
|
|
59
|
+
routes = resource_routes + dataset_routes
|
|
60
|
+
RoutesConfig.new(index: index_resource, routes: routes.sort_by(&:path))
|
|
61
|
+
end
|
|
121
62
|
|
|
122
|
-
|
|
123
|
-
routes = RoutesData.new
|
|
124
|
-
@manifest.data.content.each do |data_item|
|
|
125
|
-
file_path = data_item.file
|
|
126
|
-
# mime_type = data_item.mime
|
|
127
|
-
content_path = @manifest.path_to_content_file(file_path).to_s
|
|
63
|
+
private
|
|
128
64
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
65
|
+
def index_resource
|
|
66
|
+
INDEX_RESOURCE if @manifest.resources.key?(INDEX_RESOURCE)
|
|
67
|
+
end
|
|
132
68
|
|
|
133
|
-
|
|
69
|
+
def resource_routes
|
|
70
|
+
@manifest.resources.keys.flat_map do |resource_path|
|
|
71
|
+
routes_for_resource(resource_path)
|
|
72
|
+
end.uniq(&:path)
|
|
73
|
+
end
|
|
134
74
|
|
|
135
|
-
|
|
136
|
-
|
|
75
|
+
def routes_for_resource(resource_path)
|
|
76
|
+
url_path = resource_path.sub(%r{\A#{Package::CONTENT_DIR}/}o, "")
|
|
77
|
+
routes = [Route.new(path: "/#{url_path}", resource: resource_path)]
|
|
78
|
+
return routes unless File.extname(resource_path) == ".html"
|
|
137
79
|
|
|
80
|
+
basename = File.basename(url_path, ".html")
|
|
81
|
+
routes << Route.new(path: "/#{basename}", resource: resource_path)
|
|
82
|
+
routes << Route.new(path: INDEX_ROUTE, resource: resource_path) if index?(resource_path)
|
|
138
83
|
routes
|
|
139
84
|
end
|
|
140
85
|
|
|
141
|
-
def
|
|
142
|
-
|
|
86
|
+
def index?(resource_path)
|
|
87
|
+
resource_path == INDEX_RESOURCE
|
|
143
88
|
end
|
|
144
89
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
raise "Index file does not exist: #{target_path}" unless File.exist?(target_path)
|
|
150
|
-
|
|
151
|
-
return if File.extname(target_path).downcase == ".html"
|
|
152
|
-
|
|
153
|
-
raise "Index file is not an HTML file: #{target_path}"
|
|
90
|
+
def dataset_routes
|
|
91
|
+
@storage.dataset_names.map do |name|
|
|
92
|
+
Route.new(path: "#{DATASET_ROUTE_PREFIX}#{name}", dataset: name)
|
|
93
|
+
end
|
|
154
94
|
end
|
|
155
95
|
end
|
|
156
96
|
end
|