capsium 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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