capsium 0.1.1

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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/rake.yml +18 -0
  3. data/.github/workflows/release.yml +24 -0
  4. data/.gitignore +11 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +8 -0
  7. data/CODE_OF_CONDUCT.md +84 -0
  8. data/Gemfile +12 -0
  9. data/README.adoc +258 -0
  10. data/Rakefile +12 -0
  11. data/bin/console +11 -0
  12. data/bin/setup +8 -0
  13. data/capsium.gemspec +54 -0
  14. data/exe/capsium +6 -0
  15. data/images/005df7415a331c466ad2d9a42efdd212b6f42fe50b5fd3b3174c86c706f58244.png +0 -0
  16. data/images/0374025b3af99b8a473282c8cbbf9fcd29573cf41e586982f328f86c0690f43d.png +0 -0
  17. data/images/0bb4da785be40ef58e219470ebccb979325928b75453dc46bac23c6ee8a7a7cb.png +0 -0
  18. data/images/6aa294dccc81af594aacbe804e7ddffdc17eacc28357338108aea5d021d831ff.png +0 -0
  19. data/images/72dd3fbf3f4b475e27a0e7fb8137c475c32c41f8d222bcf62d6a9ccf102d9532.png +0 -0
  20. data/images/8772b6961d169738d7b0fa0b669b06fc2f40632d4c62586c7634fc17b93182a3.png +0 -0
  21. data/images/a998d842405933d45723606ff3f70162ec95b4ef30db25464a366184fd08fb9b.png +0 -0
  22. data/images/aa8980547e8c003d33273ab4d80e62da7f317bd7581b293c06d67f5331f24f31.png +0 -0
  23. data/images/bb78a872b539e0e9b2d80dee58acbb688f3f2727b324a5bf8bf417a69d94a166.png +0 -0
  24. data/images/c48fc83b17725d85fbb64d971196ebfccd8c5c757fe6aa5845303f6e315879b6.png +0 -0
  25. data/images/f08ef07308d08119ac2124bb7428c8bef17ef1ca70045696604d6e83015a9b91.png +0 -0
  26. data/images/f7514206111b695647eae9adfcf498ba3e0ff83ecfe25f3fc3ed8e9f04c5c726-1.png +0 -0
  27. data/images/f7514206111b695647eae9adfcf498ba3e0ff83ecfe25f3fc3ed8e9f04c5c726.png +0 -0
  28. data/lib/capsium/cli.rb +96 -0
  29. data/lib/capsium/converters/jekyll.rb +59 -0
  30. data/lib/capsium/package/dataset.rb +86 -0
  31. data/lib/capsium/package/manifest.rb +97 -0
  32. data/lib/capsium/package/metadata.rb +48 -0
  33. data/lib/capsium/package/routes.rb +157 -0
  34. data/lib/capsium/package/storage.rb +52 -0
  35. data/lib/capsium/package.rb +142 -0
  36. data/lib/capsium/packager.rb +100 -0
  37. data/lib/capsium/protector.rb +95 -0
  38. data/lib/capsium/reactor.rb +82 -0
  39. data/lib/capsium/thor_ext.rb +71 -0
  40. data/lib/capsium/version.rb +5 -0
  41. data/lib/capsium.rb +15 -0
  42. data/sig/capsium.rbs +4 -0
  43. metadata +354 -0
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/capsium/converters/jekyll_to_capsium.rb
4
+ require "fileutils"
5
+ require "json"
6
+ require "capsium/packager"
7
+
8
+ module Capsium
9
+ module Converters
10
+ class Jekyll
11
+ def initialize(site_directory, output_directory)
12
+ @site_directory = site_directory
13
+ @output_directory = output_directory
14
+ end
15
+
16
+ def convert
17
+ validate_site_directory
18
+ package_directory = prepare_package_directory
19
+ packager = Capsium::Packager.new(package_directory)
20
+ packager.pack
21
+ cleanup_package_directory(package_directory)
22
+ end
23
+
24
+ private
25
+
26
+ def validate_site_directory
27
+ return if Dir.exist?(@site_directory) && File.exist?(File.join(@site_directory, "index.html"))
28
+
29
+ raise "Invalid Jekyll site directory: #{@site_directory}"
30
+ end
31
+
32
+ def prepare_package_directory
33
+ package_directory = File.join(@output_directory, "capsium_package")
34
+ FileUtils.mkdir_p(package_directory)
35
+
36
+ FileUtils.cp_r(Dir.glob("#{@site_directory}/*"), package_directory)
37
+
38
+ create_manifest(package_directory)
39
+
40
+ package_directory
41
+ end
42
+
43
+ def create_manifest(package_directory)
44
+ manifest = {
45
+ "name" => "jekyll_site",
46
+ "version" => "1.0.0",
47
+ "description" => "A Jekyll site converted to a Capsium package",
48
+ "files" => Dir.glob("#{package_directory}/**/*").map { |file| file.sub("#{package_directory}/", "") }
49
+ }
50
+
51
+ File.write(File.join(package_directory, "manifest.json"), JSON.pretty_generate(manifest))
52
+ end
53
+
54
+ def cleanup_package_directory(package_directory)
55
+ FileUtils.rm_rf(package_directory)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/capsium/package/dataset.rb
4
+ require "json"
5
+ require "yaml"
6
+ require "csv"
7
+ require "sqlite3"
8
+
9
+ module Capsium
10
+ class Package
11
+ class Dataset < Shale::Mapper
12
+ # {
13
+ # "datasets": [
14
+ # {
15
+ # "name": "animals",
16
+ # "source": "data/animals.yaml",
17
+ # "format": "yaml",
18
+ # "schema": "data/animals_schema.yaml"
19
+ # }
20
+ # ]
21
+ # }
22
+ attr_reader :name, :path, :type, :data
23
+
24
+ def initialize(path, data_path)
25
+ @path = path
26
+ @name = File.basename(@path, ".*")
27
+ @type = detect_type
28
+ @data_path = data_path
29
+ @data = load_data
30
+ end
31
+
32
+ def detect_type
33
+ case File.extname(@path).downcase
34
+ when /.ya?ml/ then :yaml
35
+ when ".json" then :json
36
+ when ".csv" then :csv
37
+ when ".tsv" then :tsv
38
+ when ".sqlite", ".db" then :sqlite
39
+ else
40
+ raise "Unsupported data file type: #{File.extname(@path)}"
41
+ end
42
+ end
43
+
44
+ def load_data
45
+ case @type
46
+ when :yaml then YAML.load_file(@path)
47
+ when :json then JSON.parse(File.read(@path))
48
+ when :csv then CSV.read(@path, headers: true)
49
+ when :tsv then CSV.read(@path, col_sep: "\t", headers: true)
50
+ when :sqlite then load_sqlite_data
51
+ else
52
+ raise "Unsupported data file type: #{@type}"
53
+ end
54
+ end
55
+
56
+ def as_json
57
+ {
58
+ name: name,
59
+ source: relative_path(path),
60
+ format: type.to_s
61
+ }
62
+ end
63
+
64
+ def to_json(*_args)
65
+ JSON.pretty_generate(as_json)
66
+ end
67
+
68
+ def relative_path(path)
69
+ Pathname.new(path).relative_path_from(@data_path).to_s
70
+ end
71
+
72
+ private
73
+
74
+ def load_sqlite_data
75
+ db = SQLite3::Database.new(@path)
76
+ tables = db.execute("SELECT name FROM sqlite_master WHERE type='table';")
77
+ data = {}
78
+ tables.each do |table|
79
+ table_name = table.first
80
+ data[table_name] = db.execute("SELECT * FROM #{table_name};")
81
+ end
82
+ data
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/capsium/package/manifest.rb
4
+ require "json"
5
+ require "marcel"
6
+ require "shale"
7
+
8
+ module Capsium
9
+ class Package
10
+ class Manifest
11
+ attr_accessor :path, :content_path, :data
12
+
13
+ def initialize(path)
14
+ @path = path
15
+ # This is {package-name}/content
16
+ # or
17
+ # /tmp/../content
18
+ @content_path = File.join(File.dirname(@path), "content")
19
+
20
+ @data = if File.exist?(path)
21
+ ManifestData.from_json(File.read(path))
22
+ else
23
+ ManifestData.new(content: generate_manifest)
24
+ end
25
+ end
26
+
27
+ def generate_manifest
28
+ files = Dir[File.join(@content_path, "**", "*")].reject do |f|
29
+ File.directory?(f)
30
+ end
31
+
32
+ files.sort.map do |file_path|
33
+ ManifestDataItem.new(
34
+ file: relative_path(file_path),
35
+ mime: mime_from_path(file_path)
36
+ )
37
+ end
38
+ end
39
+
40
+ def lookup(filename)
41
+ @data.content.detect do |data_item|
42
+ data_item.file == filename
43
+ end
44
+ end
45
+
46
+ def to_json(*_args)
47
+ @data.to_json
48
+ end
49
+
50
+ def save_to_file(output_path = @path)
51
+ File.open(output_path, "w") do |file|
52
+ file.write(to_json)
53
+ end
54
+ end
55
+
56
+ def path_to_content_file(path)
57
+ Pathname.new(@content_path).join(path)
58
+ end
59
+
60
+ def content_file_exists?(path)
61
+ File.exist?(path_to_content_file(path))
62
+ end
63
+
64
+ def relative_path(path)
65
+ Pathname.new(path).relative_path_from(@content_path).to_s
66
+ end
67
+
68
+ private
69
+
70
+ def mime_from_path(path)
71
+ Marcel::MimeType.for(
72
+ Pathname.new(path),
73
+ name: File.basename(path),
74
+ extension: File.extname(path)
75
+ )
76
+ end
77
+ 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
+ end
97
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/capsium/package/metadata.rb
4
+ require "shale"
5
+ require "forwardable"
6
+
7
+ module Capsium
8
+ 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
+ class Metadata
21
+ attr_reader :path, :data
22
+
23
+ extend Forwardable
24
+ def_delegator :@data, :name
25
+ def_delegator :@data, :version
26
+ def_delegator :@data, :dependencies
27
+
28
+ def initialize(path)
29
+ @path = path
30
+ @data = if File.exist?(path)
31
+ MetadataData.from_json(File.read(path))
32
+ else
33
+ MetadataData.new
34
+ end
35
+ end
36
+
37
+ def to_json(*_args)
38
+ @data.to_json
39
+ end
40
+
41
+ def save_to_file(output_path = @path)
42
+ File.open(output_path, "w") do |file|
43
+ file.write(to_json)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/capsium/package/routes.rb
4
+ require "json"
5
+ require "fileutils"
6
+
7
+ module Capsium
8
+ class Package
9
+ class RouteTarget < Shale::Mapper
10
+ attribute :file, Shale::Type::String
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
59
+
60
+ def sort!
61
+ @routes.sort_by!(&:path)
62
+ self
63
+ end
64
+ end
65
+
66
+ class Routes
67
+ attr_reader :path, :data, :index, :manifest
68
+
69
+ ROUTES_FILE = "routes.json"
70
+ DEFAULT_INDEX_TARGET = "index.html"
71
+ INDEX_ROUTE = "/"
72
+
73
+ def initialize(path, manifest)
74
+ @path = path
75
+ @dir = File.dirname(path)
76
+ @manifest = manifest
77
+ @data = if File.exist?(path)
78
+ RoutesData.from_json(File.read(path))
79
+ else
80
+ generate_routes_from_manifest
81
+ end
82
+ validate_index_path(@data.resolve(INDEX_ROUTE).target.file)
83
+ validate
84
+ end
85
+
86
+ def resolve(url_path)
87
+ @data.resolve(url_path)
88
+ end
89
+
90
+ def add_route(route, target)
91
+ validate_route_target(route, target)
92
+ @data.add(route, target)
93
+ end
94
+
95
+ def update_route(route, updated_route, updated_target)
96
+ validate_route_target(updated_route, updated_target)
97
+ @data.update(route, updated_route, updated_target)
98
+ end
99
+
100
+ def remove_route(route)
101
+ @data._removed(route)
102
+ end
103
+
104
+ def validate
105
+ @data.routes.each do |route|
106
+ route.target.validate(@manifest)
107
+ end
108
+ end
109
+
110
+ def to_json(*_args)
111
+ @data.sort!.to_json
112
+ end
113
+
114
+ def save_to_file(output_path = @path)
115
+ File.open(output_path, "w") do |file|
116
+ file.write(to_json)
117
+ end
118
+ end
119
+
120
+ private
121
+
122
+ def generate_routes_from_manifest
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
128
+
129
+ if file_path == DEFAULT_INDEX_TARGET
130
+ routes.add(INDEX_ROUTE, DEFAULT_INDEX_TARGET)
131
+ end
132
+
133
+ routes.add("/#{clean_target_html_path(file_path)}", file_path) if file_path =~ /\.html$/
134
+
135
+ routes.add("/#{file_path}", file_path)
136
+ end
137
+
138
+ routes
139
+ end
140
+
141
+ def clean_target_html_path(path)
142
+ File.dirname(path) + File.basename(path, ".html")
143
+ end
144
+
145
+
146
+ def validate_index_path(index_path)
147
+ target_path = @manifest.path_to_content_file(index_path)
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}"
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/capsium/package/storage.rb
4
+ require "json"
5
+
6
+ module Capsium
7
+ class Package
8
+ class Storage
9
+ attr_reader :datasets
10
+
11
+ DATA_DIR = "data"
12
+
13
+ def initialize(path)
14
+ @path = path
15
+ @dir = File.dirname(path)
16
+ @datasets_path = File.join(@dir, DATA_DIR)
17
+ @datasets = load_datasets || generate_datasets
18
+ end
19
+
20
+ def load_datasets
21
+ return unless File.exist?(@path)
22
+
23
+ storage_data = JSON.parse(File.read(@path))
24
+ @datasets = storage_data["storage"]
25
+ end
26
+
27
+ def as_json
28
+ { datasets: datasets.map(&:as_json) }
29
+ end
30
+
31
+ def to_json(*_args)
32
+ JSON.pretty_generate(as_json)
33
+ end
34
+
35
+ def save_to_file(output_path = @path)
36
+ File.open(output_path, "w") do |file|
37
+ file.write(to_json)
38
+ end
39
+ end
40
+
41
+ def generate_datasets
42
+ datasets = []
43
+ paths = File.join(@datasets_path, "*.{yaml,yml,json,csv,tsv,sqlite,db}")
44
+ Dir.glob(paths).each do |file_path|
45
+ datasets << Dataset.new(file_path, @datasets_path)
46
+ # dataset_info[:table] = dataset.table_name if dataset.type == :sqlite
47
+ end
48
+ datasets
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/capsium/package.rb
4
+ require "fileutils"
5
+ require "json"
6
+ require "yaml"
7
+ require "csv"
8
+ require "sqlite3"
9
+ require "zip"
10
+ require_relative "package/manifest"
11
+ require_relative "package/metadata"
12
+ require_relative "package/routes"
13
+ require_relative "package/dataset"
14
+ require_relative "package/storage"
15
+ require_relative "packager"
16
+
17
+ module Capsium
18
+ class Package
19
+ attr_reader :name, :path, :manifest, :metadata, :routes, :datasets, :storage
20
+
21
+ MANIFEST_FILE = "manifest.json"
22
+ METADATA_FILE = "metadata.json"
23
+ PACKAGING_FILE = "packaging.json"
24
+ SIGNATURE_FILE = "signature.json"
25
+ STORAGE_FILE = "storage.json"
26
+ ROUTES_FILE = "routes.json"
27
+ CONTENT_DIR = "content"
28
+ DATA_DIR = "data"
29
+ ENCRYPTED_PACKAGING_FILE = "package.enc"
30
+
31
+ def initialize(path)
32
+ @original_path = Pathname.new(path).expand_path
33
+ @path = prepare_package(@original_path)
34
+ create_package_structure
35
+ load_package
36
+ @name = metadata.name
37
+ end
38
+
39
+ def prepare_package(path)
40
+ return path if File.directory?(path)
41
+
42
+ if File.file?(path)
43
+ return decompress_cap_file(path) if File.extname(path) == ".cap"
44
+
45
+ raise "Error: The package must have a .cap extension"
46
+
47
+ end
48
+
49
+ raise "Invalid package path: #{path}"
50
+ end
51
+
52
+ def solidify
53
+ @manifest.save_to_file
54
+ @metadata.save_to_file
55
+ @routes.save_to_file
56
+ @storage.save_to_file
57
+ end
58
+
59
+ def decompress_cap_file(file_path)
60
+ temp_dir = Dir.mktmpdir
61
+ metadata_path = File.join(temp_dir, METADATA_FILE)
62
+
63
+ # Extract metadata.json first
64
+ Zip::File.open(file_path) do |zip_file|
65
+ if entry = zip_file.find_entry(METADATA_FILE)
66
+ entry.extract(metadata_path)
67
+ end
68
+ end
69
+
70
+ metadata = Metadata.new(metadata_path)
71
+ package_name = metadata.name
72
+ package_version = metadata.version
73
+ package_dependencies = metadata.dependencies
74
+
75
+ package_path = File.join(temp_dir, "#{package_name}-#{package_version}")
76
+ FileUtils.mkdir_p(package_path)
77
+
78
+ Zip::File.open(file_path) do |zip_file|
79
+ zip_file.each do |entry|
80
+ entry_path = File.join(package_path, entry.name)
81
+ FileUtils.mkdir_p(File.dirname(entry_path))
82
+ entry.extract(entry_path)
83
+ end
84
+ end
85
+
86
+ package_path
87
+ end
88
+
89
+ def load_package
90
+ # Mandatory
91
+ @metadata = Metadata.new(metadata_path)
92
+
93
+ # Optional
94
+ @manifest = Manifest.new(manifest_path)
95
+ @routes = Routes.new(routes_path, @manifest)
96
+ @storage = Storage.new(storage_path)
97
+ # @datasets = load_datasets
98
+ end
99
+
100
+ def cleanup
101
+ return unless @path != @original_path && File.directory?(@path)
102
+
103
+ FileUtils.remove_entry(@path)
104
+ end
105
+
106
+ def package_files
107
+ @packager.package_files
108
+ end
109
+
110
+ private
111
+
112
+ def create_package_structure
113
+ FileUtils.mkdir_p(@path)
114
+ FileUtils.mkdir_p(content_path)
115
+ FileUtils.mkdir_p(data_path)
116
+ end
117
+
118
+ def content_path
119
+ File.join(@path, CONTENT_DIR)
120
+ end
121
+
122
+ def data_path
123
+ File.join(@path, DATA_DIR)
124
+ end
125
+
126
+ def routes_path
127
+ File.join(@path, ROUTES_FILE)
128
+ end
129
+
130
+ def storage_path
131
+ File.join(@path, STORAGE_FILE)
132
+ end
133
+
134
+ def metadata_path
135
+ File.join(@path, METADATA_FILE)
136
+ end
137
+
138
+ def manifest_path
139
+ File.join(@path, MANIFEST_FILE)
140
+ end
141
+ end
142
+ end