capsium 0.1.2 → 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/.gitignore +1 -0
- data/.rubocop.yml +34 -7
- data/CHANGELOG.md +58 -0
- data/Gemfile +6 -0
- data/LICENSE +21 -0
- data/README.adoc +177 -135
- data/Rakefile +8 -1
- data/bin/console +1 -1
- data/capsium.gemspec +11 -19
- 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 +3 -96
- data/lib/capsium/converters/jekyll.rb +5 -7
- data/lib/capsium/converters.rb +7 -0
- data/lib/capsium/package/dataset.rb +67 -34
- data/lib/capsium/package/manifest.rb +33 -22
- data/lib/capsium/package/manifest_config.rb +52 -7
- data/lib/capsium/package/metadata.rb +4 -8
- data/lib/capsium/package/metadata_config.rb +97 -9
- data/lib/capsium/package/routes.rb +39 -41
- data/lib/capsium/package/routes_config.rb +120 -44
- data/lib/capsium/package/security.rb +123 -0
- data/lib/capsium/package/security_config.rb +60 -0
- data/lib/capsium/package/storage.rb +20 -45
- data/lib/capsium/package/storage_config.rb +100 -3
- data/lib/capsium/package/validator.rb +149 -0
- data/lib/capsium/package.rb +68 -66
- data/lib/capsium/packager.rb +42 -6
- data/lib/capsium/reactor/introspection.rb +88 -0
- data/lib/capsium/reactor.rb +89 -41
- 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 +52 -183
- data/lib/capsium/package/dataset_config.rb +0 -28
- data/lib/capsium/protector.rb +0 -103
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
require "time"
|
|
6
|
+
|
|
7
|
+
module Capsium
|
|
8
|
+
class Reactor
|
|
9
|
+
# Monitoring HTTP API reports for the package this reactor serves
|
|
10
|
+
# (ARCHITECTURE.md section 7). Each report wraps its single entry in
|
|
11
|
+
# the list shape all reactors converge on.
|
|
12
|
+
class Introspection
|
|
13
|
+
METADATA_PATH = "/api/v1/introspect/metadata"
|
|
14
|
+
ROUTES_PATH = "/api/v1/introspect/routes"
|
|
15
|
+
CONTENT_HASHES_PATH = "/api/v1/introspect/content-hashes"
|
|
16
|
+
CONTENT_VALIDITY_PATH = "/api/v1/introspect/content-validity"
|
|
17
|
+
PATHS = [METADATA_PATH, ROUTES_PATH, CONTENT_HASHES_PATH,
|
|
18
|
+
CONTENT_VALIDITY_PATH].freeze
|
|
19
|
+
|
|
20
|
+
attr_reader :package
|
|
21
|
+
|
|
22
|
+
def initialize(package)
|
|
23
|
+
@package = package
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def endpoint?(path)
|
|
27
|
+
PATHS.include?(path)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The report body for an introspection endpoint, or nil when the
|
|
31
|
+
# path is not an introspection endpoint.
|
|
32
|
+
def report_for(path)
|
|
33
|
+
case path
|
|
34
|
+
when METADATA_PATH then metadata_report
|
|
35
|
+
when ROUTES_PATH then routes_report
|
|
36
|
+
when CONTENT_HASHES_PATH then content_hashes_report
|
|
37
|
+
when CONTENT_VALIDITY_PATH then content_validity_report
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def metadata_report
|
|
42
|
+
metadata = package.metadata
|
|
43
|
+
{ packages: [{
|
|
44
|
+
name: metadata.name,
|
|
45
|
+
version: metadata.version,
|
|
46
|
+
author: metadata.author,
|
|
47
|
+
description: metadata.description
|
|
48
|
+
}] }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def routes_report
|
|
52
|
+
entries = package.routes.config.routes.map do |route|
|
|
53
|
+
{ method: route.http_method || "GET", path: route.path }
|
|
54
|
+
end
|
|
55
|
+
{ routes: [{ package: package.name, routes: entries }] }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def content_hashes_report
|
|
59
|
+
{ contentHashes: [{ package: package.name, hash: content_hash }] }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def content_validity_report
|
|
63
|
+
errors = package.verify_integrity
|
|
64
|
+
entry = {
|
|
65
|
+
package: package.name,
|
|
66
|
+
valid: errors.empty?,
|
|
67
|
+
lastChecked: Time.now.utc.iso8601
|
|
68
|
+
}
|
|
69
|
+
entry[:reason] = errors.map(&:message).join("; ") unless errors.empty?
|
|
70
|
+
{ contentValidity: [entry] }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# SHA-256 of the .cap blob when the package was loaded from one.
|
|
76
|
+
# For directory sources there is no blob, so the hash covers a
|
|
77
|
+
# canonical (sorted-key) JSON serialization of the package content
|
|
78
|
+
# checksums — the same data security.json integrityChecks carry.
|
|
79
|
+
def content_hash
|
|
80
|
+
cap_file = package.cap_file_path
|
|
81
|
+
return Digest::SHA256.file(cap_file).hexdigest if cap_file
|
|
82
|
+
|
|
83
|
+
checksums = Package::Security.checksums_for(package.path)
|
|
84
|
+
Digest::SHA256.hexdigest(JSON.generate(checksums))
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
data/lib/capsium/reactor.rb
CHANGED
|
@@ -1,52 +1,59 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "webrick"
|
|
4
3
|
require "json"
|
|
5
4
|
require "listen"
|
|
6
|
-
require "
|
|
5
|
+
require "webrick"
|
|
7
6
|
|
|
8
7
|
module Capsium
|
|
9
8
|
class Reactor
|
|
9
|
+
autoload :Introspection, "capsium/reactor/introspection"
|
|
10
|
+
|
|
10
11
|
DEFAULT_PORT = 8864
|
|
12
|
+
DEFAULT_CACHE_CONTROL = "public, max-age=31536000"
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
attr_reader :package, :package_path, :routes, :port, :cache_control,
|
|
15
|
+
:server, :server_thread, :introspection
|
|
14
16
|
|
|
15
|
-
def initialize(package:, port: DEFAULT_PORT,
|
|
17
|
+
def initialize(package:, port: DEFAULT_PORT,
|
|
18
|
+
cache_control: DEFAULT_CACHE_CONTROL, do_not_listen: false)
|
|
16
19
|
@package = package.is_a?(String) ? Package.new(package) : package
|
|
17
20
|
@package_path = @package.path
|
|
18
21
|
@port = port
|
|
22
|
+
@cache_control = cache_control
|
|
19
23
|
setup_server(do_not_listen)
|
|
20
|
-
|
|
24
|
+
load_state
|
|
21
25
|
mount_routes
|
|
22
26
|
end
|
|
23
27
|
|
|
24
28
|
def serve
|
|
29
|
+
trap("INT") { shutdown_server }
|
|
25
30
|
@server_thread = start_server
|
|
26
31
|
start_listener
|
|
27
32
|
end
|
|
28
33
|
|
|
29
34
|
def handle_request(request, response)
|
|
35
|
+
return serve_introspection(request, response) if @introspection.endpoint?(request.path)
|
|
36
|
+
|
|
30
37
|
route = @routes.resolve(request.path)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
else
|
|
39
|
-
response.status = 404
|
|
40
|
-
response["Content-Type"] = "text/plain"
|
|
41
|
-
response.body = "Not Found"
|
|
42
|
-
end
|
|
43
|
-
else
|
|
44
|
-
response.status = 404
|
|
45
|
-
response["Content-Type"] = "text/plain"
|
|
46
|
-
response.body = "Not Found"
|
|
38
|
+
route ? serve_route(route, response) : respond_not_found(response)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def mount_routes
|
|
42
|
+
paths = @routes.config.routes.map(&:path) + Introspection::PATHS
|
|
43
|
+
paths.each do |path|
|
|
44
|
+
@server.mount_proc(path.to_s) { |req, res| handle_request(req, res) }
|
|
47
45
|
end
|
|
48
46
|
end
|
|
49
47
|
|
|
48
|
+
def restart_server
|
|
49
|
+
@server.shutdown
|
|
50
|
+
@server_thread&.join
|
|
51
|
+
load_package
|
|
52
|
+
setup_server(false)
|
|
53
|
+
mount_routes
|
|
54
|
+
@server_thread = start_server
|
|
55
|
+
end
|
|
56
|
+
|
|
50
57
|
private
|
|
51
58
|
|
|
52
59
|
def setup_server(do_not_listen)
|
|
@@ -55,18 +62,8 @@ module Capsium
|
|
|
55
62
|
@server = WEBrick::HTTPServer.new(server_options)
|
|
56
63
|
end
|
|
57
64
|
|
|
58
|
-
def mount_routes
|
|
59
|
-
@routes.config.routes.each do |route|
|
|
60
|
-
path = route.path
|
|
61
|
-
@server.mount_proc(path.to_s) do |req, res|
|
|
62
|
-
handle_request(req, res)
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
65
|
def start_server
|
|
68
66
|
Thread.new do
|
|
69
|
-
trap("INT") { shutdown_server }
|
|
70
67
|
puts "Starting server on http://localhost:#{@port}"
|
|
71
68
|
@server.start
|
|
72
69
|
end
|
|
@@ -78,15 +75,6 @@ module Capsium
|
|
|
78
75
|
exit
|
|
79
76
|
end
|
|
80
77
|
|
|
81
|
-
def restart_server
|
|
82
|
-
@server.shutdown
|
|
83
|
-
@server_thread&.join
|
|
84
|
-
load_package
|
|
85
|
-
setup_server(false)
|
|
86
|
-
mount_routes
|
|
87
|
-
@server_thread = start_server
|
|
88
|
-
end
|
|
89
|
-
|
|
90
78
|
def start_listener
|
|
91
79
|
listener = Listen.to(@package_path) do |_modified, _added, _removed|
|
|
92
80
|
puts "Changes detected, reloading..."
|
|
@@ -100,7 +88,67 @@ module Capsium
|
|
|
100
88
|
|
|
101
89
|
def load_package
|
|
102
90
|
@package = Package.new(@package_path)
|
|
91
|
+
load_state
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def load_state
|
|
103
95
|
@routes = @package.routes
|
|
96
|
+
@introspection = Introspection.new(@package)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def serve_introspection(request, response)
|
|
100
|
+
return respond_method_not_allowed(response) unless request.request_method == "GET"
|
|
101
|
+
|
|
102
|
+
response.status = 200
|
|
103
|
+
response["Content-Type"] = "application/json"
|
|
104
|
+
response.body = JSON.generate(@introspection.report_for(request.path))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def serve_route(route, response)
|
|
108
|
+
case route.kind
|
|
109
|
+
when :dataset then serve_dataset(route.dataset, response)
|
|
110
|
+
when :resource then serve_file(route, response)
|
|
111
|
+
else respond_not_implemented(response)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def serve_file(route, response)
|
|
116
|
+
content_path = route.fs_path(@package.path)
|
|
117
|
+
if content_path && File.exist?(content_path)
|
|
118
|
+
response.status = 200
|
|
119
|
+
response["Content-Type"] = route.mime(@package.manifest) || "application/octet-stream"
|
|
120
|
+
headers_for(route).each { |name, value| response[name] = value }
|
|
121
|
+
response.body = File.read(content_path)
|
|
122
|
+
else
|
|
123
|
+
respond_not_found(response)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def headers_for(route)
|
|
128
|
+
route.headers || (@cache_control ? { "Cache-Control" => @cache_control } : {})
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def serve_dataset(dataset_name, response)
|
|
132
|
+
dataset = @package.storage.dataset(dataset_name)
|
|
133
|
+
if dataset
|
|
134
|
+
response.status = 200
|
|
135
|
+
response["Content-Type"] = "application/json"
|
|
136
|
+
response.body = JSON.generate(dataset.data)
|
|
137
|
+
else
|
|
138
|
+
respond_not_found(response)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def respond_not_found(response) = respond_text(response, 404, "Not Found")
|
|
143
|
+
|
|
144
|
+
def respond_not_implemented(response) = respond_text(response, 501, "Not Implemented")
|
|
145
|
+
|
|
146
|
+
def respond_method_not_allowed(response) = respond_text(response, 405, "Method Not Allowed")
|
|
147
|
+
|
|
148
|
+
def respond_text(response, status, body)
|
|
149
|
+
response.status = status
|
|
150
|
+
response["Content-Type"] = "text/plain"
|
|
151
|
+
response.body = body
|
|
104
152
|
end
|
|
105
153
|
end
|
|
106
154
|
end
|
data/lib/capsium/version.rb
CHANGED
data/lib/capsium.rb
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "capsium/version"
|
|
4
|
-
require "shale"
|
|
5
|
-
require "shale/adapter/nokogiri"
|
|
6
|
-
Shale.xml_adapter = Shale::Adapter::Nokogiri
|
|
7
|
-
|
|
8
3
|
module Capsium
|
|
9
4
|
class Error < StandardError; end
|
|
10
5
|
|
|
11
|
-
|
|
6
|
+
autoload :VERSION, "capsium/version"
|
|
7
|
+
autoload :Cli, "capsium/cli"
|
|
8
|
+
autoload :Converters, "capsium/converters"
|
|
9
|
+
autoload :Package, "capsium/package"
|
|
10
|
+
autoload :Packager, "capsium/packager"
|
|
11
|
+
autoload :Reactor, "capsium/reactor"
|
|
12
|
+
autoload :ThorExt, "capsium/thor_ext"
|
|
12
13
|
end
|
|
13
|
-
|
|
14
|
-
require_relative "capsium/package"
|
|
15
|
-
require_relative "capsium/packager"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
# A loaded dataset. "source"/"schemaFile"/"databaseFile" are
|
|
4
|
+
# package-relative paths resolved against the package directory.
|
|
5
|
+
class Dataset
|
|
6
|
+
attr_reader name: String
|
|
7
|
+
attr_reader config: DatasetConfig
|
|
8
|
+
attr_reader data: untyped
|
|
9
|
+
attr_reader package_path: String
|
|
10
|
+
|
|
11
|
+
def initialize: (name: String name, config: DatasetConfig config,
|
|
12
|
+
package_path: String package_path) -> void
|
|
13
|
+
|
|
14
|
+
def source_path: () -> String
|
|
15
|
+
|
|
16
|
+
def schema_path: () -> String?
|
|
17
|
+
|
|
18
|
+
def load_data: () -> untyped
|
|
19
|
+
|
|
20
|
+
def validate: () -> bool
|
|
21
|
+
|
|
22
|
+
# File-existence and schema validations; empty when valid.
|
|
23
|
+
def validation_errors: () -> Array[String]
|
|
24
|
+
|
|
25
|
+
def to_json: (*untyped args) -> String
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
# Loads, generates and writes manifest.json (ARCHITECTURE.md
|
|
4
|
+
# section 3).
|
|
5
|
+
class Manifest
|
|
6
|
+
attr_reader path: String
|
|
7
|
+
attr_reader content_path: String
|
|
8
|
+
attr_reader config: ManifestConfig
|
|
9
|
+
|
|
10
|
+
def initialize: (String path) -> void
|
|
11
|
+
|
|
12
|
+
# Auto-generation: scan content/ recursively, detect MIME types,
|
|
13
|
+
# default visibility "exported", deterministic (sorted) output.
|
|
14
|
+
def generate_manifest: () -> Hash[String, Resource]
|
|
15
|
+
|
|
16
|
+
def resources: () -> Hash[String, Resource]
|
|
17
|
+
|
|
18
|
+
def lookup: (String path) -> Resource?
|
|
19
|
+
|
|
20
|
+
def type_for: (String path) -> String?
|
|
21
|
+
|
|
22
|
+
def to_json: (*untyped args) -> String
|
|
23
|
+
def to_hash: () -> Hash[String, untyped]
|
|
24
|
+
|
|
25
|
+
def save_to_file: (?String output_path) -> void
|
|
26
|
+
|
|
27
|
+
def path_to_content_file: (String path) -> Pathname
|
|
28
|
+
|
|
29
|
+
def content_file_exists?: (String path) -> bool
|
|
30
|
+
|
|
31
|
+
def relative_path: (String path) -> String
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
# A single manifest resource entry (ARCHITECTURE.md section 3).
|
|
4
|
+
class Resource
|
|
5
|
+
VISIBILITIES: Array[String]
|
|
6
|
+
|
|
7
|
+
attr_accessor type: String?
|
|
8
|
+
attr_accessor visibility: String
|
|
9
|
+
attr_accessor version: String?
|
|
10
|
+
|
|
11
|
+
def initialize: (?type: String? type, ?visibility: String? visibility,
|
|
12
|
+
?version: String? version) -> void
|
|
13
|
+
|
|
14
|
+
def to_json: (*untyped args) -> String
|
|
15
|
+
def to_hash: () -> Hash[String, untyped]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Canonical manifest.json model: an object keyed by package-relative
|
|
19
|
+
# resource path. The legacy array form is normalized on read;
|
|
20
|
+
# writers emit only the object form.
|
|
21
|
+
class ManifestConfig
|
|
22
|
+
attr_accessor resources: Hash[String, Resource]
|
|
23
|
+
|
|
24
|
+
def initialize: (?resources: Hash[String, Resource] resources) -> void
|
|
25
|
+
|
|
26
|
+
def self.from_json: (String json) -> instance
|
|
27
|
+
|
|
28
|
+
def resources_from_json: (ManifestConfig model, untyped value) -> void
|
|
29
|
+
|
|
30
|
+
def resources_to_json: (ManifestConfig model, Hash[String, untyped] doc) -> void
|
|
31
|
+
|
|
32
|
+
def sort!: () -> ManifestConfig
|
|
33
|
+
|
|
34
|
+
def to_json: (*untyped args) -> String
|
|
35
|
+
def to_hash: () -> Hash[String, untyped]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
# Loads and writes metadata.json; field access is delegated to the
|
|
4
|
+
# wrapped MetadataData config (ARCHITECTURE.md section 2).
|
|
5
|
+
class Metadata
|
|
6
|
+
attr_reader path: String
|
|
7
|
+
attr_reader config: MetadataData
|
|
8
|
+
|
|
9
|
+
def initialize: (String path) -> void
|
|
10
|
+
|
|
11
|
+
def save_to_file: (?String output_path) -> void
|
|
12
|
+
|
|
13
|
+
def to_json: (*untyped args) -> String
|
|
14
|
+
def to_hash: () -> Hash[String, untyped]
|
|
15
|
+
def name: () -> String?
|
|
16
|
+
def version: () -> String?
|
|
17
|
+
def description: () -> String?
|
|
18
|
+
def guid: () -> String?
|
|
19
|
+
def uuid: () -> String?
|
|
20
|
+
def author: () -> String?
|
|
21
|
+
def license: () -> String?
|
|
22
|
+
def repository: () -> Repository?
|
|
23
|
+
def dependencies: () -> Hash[String, String]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
# The repository object of metadata.json.
|
|
4
|
+
class Repository
|
|
5
|
+
attr_accessor type: String?
|
|
6
|
+
attr_accessor url: String?
|
|
7
|
+
|
|
8
|
+
def initialize: (?type: String? type, ?url: String? url) -> void
|
|
9
|
+
|
|
10
|
+
def to_json: (*untyped args) -> String
|
|
11
|
+
def to_hash: () -> Hash[String, untyped]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Canonical metadata.json model (ARCHITECTURE.md section 2). The
|
|
15
|
+
# legacy array form of "dependencies" is normalized on read; writers
|
|
16
|
+
# emit only the object form.
|
|
17
|
+
class MetadataData
|
|
18
|
+
KEBAB_CASE_PATTERN: Regexp
|
|
19
|
+
SEMVER_PATTERN: Regexp
|
|
20
|
+
UUID_PATTERN: Regexp
|
|
21
|
+
|
|
22
|
+
attr_accessor name: String?
|
|
23
|
+
attr_accessor version: String?
|
|
24
|
+
attr_accessor description: String?
|
|
25
|
+
attr_accessor guid: String?
|
|
26
|
+
attr_accessor uuid: String?
|
|
27
|
+
attr_accessor author: String?
|
|
28
|
+
attr_accessor license: String?
|
|
29
|
+
attr_accessor repository: Repository?
|
|
30
|
+
attr_accessor dependencies: Hash[String, String]
|
|
31
|
+
attr_accessor read_only: bool?
|
|
32
|
+
|
|
33
|
+
def initialize: (?name: String? name, ?version: String? version,
|
|
34
|
+
?description: String? description, ?guid: String? guid,
|
|
35
|
+
?uuid: String? uuid, ?author: String? author,
|
|
36
|
+
?license: String? license, ?repository: Repository? repository,
|
|
37
|
+
?dependencies: Hash[String, String] dependencies,
|
|
38
|
+
?read_only: bool? read_only) -> void
|
|
39
|
+
|
|
40
|
+
def self.from_json: (String json) -> instance
|
|
41
|
+
|
|
42
|
+
# Field-level format validations; empty when valid.
|
|
43
|
+
def format_errors: () -> Array[String]
|
|
44
|
+
|
|
45
|
+
def to_json: (*untyped args) -> String
|
|
46
|
+
def to_hash: () -> Hash[String, untyped]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
# Loads, generates and writes routes.json (ARCHITECTURE.md
|
|
4
|
+
# section 4).
|
|
5
|
+
class Routes
|
|
6
|
+
INDEX_ROUTE: String
|
|
7
|
+
INDEX_RESOURCE: String
|
|
8
|
+
DATASET_ROUTE_PREFIX: String
|
|
9
|
+
|
|
10
|
+
attr_reader path: String
|
|
11
|
+
attr_reader config: RoutesConfig
|
|
12
|
+
attr_reader manifest: Manifest
|
|
13
|
+
attr_reader storage: Storage
|
|
14
|
+
|
|
15
|
+
def initialize: (String path, Manifest manifest, Storage storage) -> void
|
|
16
|
+
|
|
17
|
+
def resolve: (String url_path) -> Route?
|
|
18
|
+
|
|
19
|
+
def add_route: (String route, String target) -> Route
|
|
20
|
+
|
|
21
|
+
def update_route: (String route, String updated_route, String updated_target) -> Route
|
|
22
|
+
|
|
23
|
+
def remove_route: (String route) -> Array[Route]
|
|
24
|
+
|
|
25
|
+
def to_json: (*untyped args) -> String
|
|
26
|
+
def to_hash: () -> Hash[String, untyped]
|
|
27
|
+
|
|
28
|
+
def save_to_file: (?String output_path) -> void
|
|
29
|
+
|
|
30
|
+
# Auto-generation (ARCHITECTURE.md section 4): every manifest
|
|
31
|
+
# resource gets a route at its path relative to content/; HTML
|
|
32
|
+
# files get two routes; the index HTML additionally gets "/";
|
|
33
|
+
# every dataset gets /api/v1/data/<id>. Deterministic output.
|
|
34
|
+
def generate_routes: () -> RoutesConfig
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
# A single route entry (ARCHITECTURE.md section 4). Kinds are
|
|
4
|
+
# discriminated by key, MECE: static file, dataset route, or dynamic
|
|
5
|
+
# handler (accepted-and-ignored; reactors respond 501).
|
|
6
|
+
class Route
|
|
7
|
+
DATASET_PATH_PREFIX: String
|
|
8
|
+
|
|
9
|
+
attr_accessor path: String?
|
|
10
|
+
attr_accessor resource: String?
|
|
11
|
+
attr_accessor headers: Hash[String, String]?
|
|
12
|
+
attr_accessor headers_file: String?
|
|
13
|
+
attr_accessor visibility: String
|
|
14
|
+
attr_accessor dataset: String?
|
|
15
|
+
attr_accessor access_control: Hash[String, untyped]?
|
|
16
|
+
attr_accessor http_method: String?
|
|
17
|
+
attr_accessor handler: String?
|
|
18
|
+
|
|
19
|
+
def initialize: (?path: String? path, ?resource: String? resource,
|
|
20
|
+
?headers: Hash[String, String]? headers,
|
|
21
|
+
?headers_file: String? headers_file,
|
|
22
|
+
?visibility: String? visibility, ?dataset: String? dataset,
|
|
23
|
+
?access_control: Hash[String, untyped]? access_control,
|
|
24
|
+
?http_method: String? http_method,
|
|
25
|
+
?handler: String? handler) -> void
|
|
26
|
+
|
|
27
|
+
def self.from_json: (String json) -> instance
|
|
28
|
+
|
|
29
|
+
def kind: () -> (:resource | :dataset | :handler)
|
|
30
|
+
|
|
31
|
+
def dataset_route?: () -> bool
|
|
32
|
+
|
|
33
|
+
def handler_route?: () -> bool
|
|
34
|
+
|
|
35
|
+
def fs_path: (String package_path) -> String?
|
|
36
|
+
|
|
37
|
+
def mime: (Manifest manifest) -> String?
|
|
38
|
+
|
|
39
|
+
def validate_target: (String package_path, Storage storage) -> void
|
|
40
|
+
|
|
41
|
+
def to_json: (*untyped args) -> String
|
|
42
|
+
def to_hash: () -> Hash[String, untyped]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Canonical routes.json model: optional top-level "index" plus a
|
|
46
|
+
# "routes" array. The legacy object/target form is normalized on
|
|
47
|
+
# read; writers emit only the canonical form.
|
|
48
|
+
class RoutesConfig
|
|
49
|
+
attr_accessor index: String?
|
|
50
|
+
attr_accessor routes: Array[Route]
|
|
51
|
+
|
|
52
|
+
def initialize: (?index: String? index, ?routes: Array[Route] routes) -> void
|
|
53
|
+
|
|
54
|
+
def self.from_json: (String json) -> instance
|
|
55
|
+
|
|
56
|
+
def routes_from_json: (RoutesConfig model, untyped value) -> void
|
|
57
|
+
|
|
58
|
+
def routes_to_json: (RoutesConfig model, Hash[String, untyped] doc) -> void
|
|
59
|
+
|
|
60
|
+
def resolve: (String path) -> Route?
|
|
61
|
+
|
|
62
|
+
def add: (String path, String target) -> Route
|
|
63
|
+
|
|
64
|
+
def update: (String path, String updated_path, String updated_target) -> Route
|
|
65
|
+
|
|
66
|
+
def remove: (String path) -> Array[Route]
|
|
67
|
+
|
|
68
|
+
def sort!: () -> RoutesConfig
|
|
69
|
+
|
|
70
|
+
def to_json: (*untyped args) -> String
|
|
71
|
+
def to_hash: () -> Hash[String, untyped]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module Capsium
|
|
2
|
+
class Package
|
|
3
|
+
# Loads, generates and verifies security.json (ARCHITECTURE.md
|
|
4
|
+
# section 6). Checksums cover every file in the package except
|
|
5
|
+
# security.json itself.
|
|
6
|
+
class Security
|
|
7
|
+
class IntegrityError < Capsium::Error
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# A file whose SHA-256 does not match the recorded checksum.
|
|
11
|
+
class ChecksumMismatch
|
|
12
|
+
attr_reader path: String
|
|
13
|
+
attr_reader expected: String
|
|
14
|
+
attr_reader actual: String
|
|
15
|
+
|
|
16
|
+
def initialize: (path: String path, expected: String expected, actual: String actual) -> void
|
|
17
|
+
|
|
18
|
+
def message: () -> String
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# A checksum listed for a file that is missing on disk.
|
|
22
|
+
class MissingFile
|
|
23
|
+
attr_reader path: String
|
|
24
|
+
attr_reader expected: String
|
|
25
|
+
|
|
26
|
+
def initialize: (path: String path, expected: String expected) -> void
|
|
27
|
+
|
|
28
|
+
def message: () -> String
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# A file on disk not covered by any checksum.
|
|
32
|
+
class UncheckedFile
|
|
33
|
+
attr_reader path: String
|
|
34
|
+
|
|
35
|
+
def initialize: (path: String path) -> void
|
|
36
|
+
|
|
37
|
+
def message: () -> String
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
attr_reader path: String
|
|
41
|
+
attr_reader config: SecurityConfig?
|
|
42
|
+
|
|
43
|
+
def initialize: (String path, ?SecurityConfig? config) -> void
|
|
44
|
+
|
|
45
|
+
def self.generate: (String package_path) -> Security
|
|
46
|
+
|
|
47
|
+
def self.checksums_for: (String package_path) -> Hash[String, String]
|
|
48
|
+
|
|
49
|
+
def self.package_files: (String package_path) -> Array[String]
|
|
50
|
+
|
|
51
|
+
def present?: () -> bool
|
|
52
|
+
|
|
53
|
+
def checksums: () -> Hash[String, String]
|
|
54
|
+
|
|
55
|
+
def verify: (String package_path) -> Array[ChecksumMismatch | MissingFile | UncheckedFile]
|
|
56
|
+
|
|
57
|
+
def verify!: (String package_path) -> void
|
|
58
|
+
|
|
59
|
+
def save_to_file: (?String output_path) -> void
|
|
60
|
+
|
|
61
|
+
def to_json: (*untyped args) -> String
|
|
62
|
+
def to_hash: () -> Hash[String, untyped]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|