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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rake.yml +1 -4
  3. data/.github/workflows/release.yml +1 -2
  4. data/.gitignore +1 -0
  5. data/.rubocop.yml +34 -3
  6. data/CHANGELOG.md +58 -0
  7. data/Gemfile +4 -4
  8. data/LICENSE +21 -0
  9. data/README.adoc +177 -135
  10. data/Rakefile +8 -1
  11. data/bin/console +1 -1
  12. data/capsium.gemspec +16 -23
  13. data/exe/capsium +1 -1
  14. data/lib/capsium/cli/convert.rb +18 -0
  15. data/lib/capsium/cli/package.rb +86 -0
  16. data/lib/capsium/cli/reactor.rb +25 -0
  17. data/lib/capsium/cli.rb +5 -79
  18. data/lib/capsium/converters/jekyll.rb +62 -31
  19. data/lib/capsium/converters.rb +7 -0
  20. data/lib/capsium/package/dataset.rb +76 -56
  21. data/lib/capsium/package/manifest.rb +41 -50
  22. data/lib/capsium/package/manifest_config.rb +64 -0
  23. data/lib/capsium/package/metadata.rb +11 -29
  24. data/lib/capsium/package/metadata_config.rb +105 -0
  25. data/lib/capsium/package/routes.rb +51 -111
  26. data/lib/capsium/package/routes_config.rb +150 -0
  27. data/lib/capsium/package/security.rb +123 -0
  28. data/lib/capsium/package/security_config.rb +60 -0
  29. data/lib/capsium/package/storage.rb +26 -26
  30. data/lib/capsium/package/storage_config.rb +106 -0
  31. data/lib/capsium/package/validator.rb +149 -0
  32. data/lib/capsium/package.rb +84 -70
  33. data/lib/capsium/packager.rb +60 -62
  34. data/lib/capsium/reactor/introspection.rb +88 -0
  35. data/lib/capsium/reactor.rb +108 -36
  36. data/lib/capsium/version.rb +1 -1
  37. data/lib/capsium.rb +7 -9
  38. data/sig/capsium/package/dataset.rbs +28 -0
  39. data/sig/capsium/package/manifest.rbs +34 -0
  40. data/sig/capsium/package/manifest_config.rbs +38 -0
  41. data/sig/capsium/package/metadata.rbs +26 -0
  42. data/sig/capsium/package/metadata_config.rbs +49 -0
  43. data/sig/capsium/package/routes.rbs +37 -0
  44. data/sig/capsium/package/routes_config.rbs +74 -0
  45. data/sig/capsium/package/security.rbs +65 -0
  46. data/sig/capsium/package/security_config.rbs +57 -0
  47. data/sig/capsium/package/storage.rbs +28 -0
  48. data/sig/capsium/package/storage_config.rbs +61 -0
  49. data/sig/capsium/package/validator.rbs +29 -0
  50. data/sig/capsium/package.rbs +53 -0
  51. data/sig/capsium/packager.rbs +23 -0
  52. data/sig/capsium/reactor/introspection.rbs +31 -0
  53. data/sig/capsium/reactor.rbs +30 -0
  54. data/sig/capsium.rbs +3 -1
  55. metadata +63 -146
  56. data/images/005df7415a331c466ad2d9a42efdd212b6f42fe50b5fd3b3174c86c706f58244.png +0 -0
  57. data/images/0374025b3af99b8a473282c8cbbf9fcd29573cf41e586982f328f86c0690f43d.png +0 -0
  58. data/images/0bb4da785be40ef58e219470ebccb979325928b75453dc46bac23c6ee8a7a7cb.png +0 -0
  59. data/images/6aa294dccc81af594aacbe804e7ddffdc17eacc28357338108aea5d021d831ff.png +0 -0
  60. data/images/72dd3fbf3f4b475e27a0e7fb8137c475c32c41f8d222bcf62d6a9ccf102d9532.png +0 -0
  61. data/images/8772b6961d169738d7b0fa0b669b06fc2f40632d4c62586c7634fc17b93182a3.png +0 -0
  62. data/images/a998d842405933d45723606ff3f70162ec95b4ef30db25464a366184fd08fb9b.png +0 -0
  63. data/images/aa8980547e8c003d33273ab4d80e62da7f317bd7581b293c06d67f5331f24f31.png +0 -0
  64. data/images/bb78a872b539e0e9b2d80dee58acbb688f3f2727b324a5bf8bf417a69d94a166.png +0 -0
  65. data/images/c48fc83b17725d85fbb64d971196ebfccd8c5c757fe6aa5845303f6e315879b6.png +0 -0
  66. data/images/f08ef07308d08119ac2124bb7428c8bef17ef1ca70045696604d6e83015a9b91.png +0 -0
  67. data/images/f7514206111b695647eae9adfcf498ba3e0ff83ecfe25f3fc3ed8e9f04c5c726-1.png +0 -0
  68. data/images/f7514206111b695647eae9adfcf498ba3e0ff83ecfe25f3fc3ed8e9f04c5c726.png +0 -0
  69. data/lib/capsium/protector.rb +0 -95
data/capsium.gemspec CHANGED
@@ -1,25 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "lib/capsium/version"
3
+ require File.expand_path("lib/capsium/version", __dir__)
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "capsium"
7
7
  spec.version = Capsium::VERSION
8
8
  spec.authors = ["Ribose Inc."]
9
- spec.email = ["open.source@ribose.com'"]
10
-
11
- spec.summary = "Capsium"
12
- spec.description = "Capsium"
13
- spec.homepage = "https://github.com/metanorma/capsium"
9
+ spec.email = ["open.source@ribose.com"]
10
+
11
+ spec.summary = "Packager and reactor for Capsium packages (.cap)"
12
+ spec.description = "Capsium facilitates the creation, management and " \
13
+ "deployment of content packages with ease. This gem " \
14
+ "provides a structured way to handle content, data, " \
15
+ "and metadata for various applications: it packages " \
16
+ "them into .cap files and serves them over HTTP."
17
+ spec.homepage = "https://github.com/capsiums/capsium"
14
18
  spec.license = "MIT"
15
19
 
16
20
  spec.metadata["homepage_uri"] = spec.homepage
17
21
  spec.metadata["source_code_uri"] = spec.homepage
18
- spec.metadata["changelog_uri"] = "https://github.com/metanorma/capsium/releases"
22
+ spec.metadata["changelog_uri"] = "https://github.com/capsiums/capsium/releases"
23
+ spec.metadata["rubygems_mfa_required"] = "true"
19
24
 
20
- # Specify which files should be added to the gem when it is released.
21
- # The `git ls-files -z` loads the files in the RubyGem
22
- # that have been added into git.
23
25
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
26
  `git ls-files -z`
25
27
  .split("\x0")
@@ -29,26 +31,17 @@ Gem::Specification.new do |spec|
29
31
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30
32
  spec.require_paths = ["lib"]
31
33
 
32
- spec.required_ruby_version = ">= 2.7.0"
34
+ spec.required_ruby_version = ">= 3.2.0"
33
35
 
34
- spec.add_dependency "cssminify"
35
36
  spec.add_dependency "csv"
36
- spec.add_dependency "htmlcompressor"
37
- spec.add_dependency "html-proofer"
38
37
  spec.add_dependency "json"
38
+ spec.add_dependency "json-schema"
39
39
  spec.add_dependency "listen"
40
+ spec.add_dependency "lutaml-model", ">= 0.8", "< 1.0"
40
41
  spec.add_dependency "marcel"
41
- spec.add_dependency "rubyzip", "~>2.3.2"
42
- spec.add_dependency "scss_lint"
43
- spec.add_dependency "shale"
42
+ spec.add_dependency "rubyzip", ">= 2.3.2", "< 4.0"
44
43
  spec.add_dependency "sqlite3"
45
44
  spec.add_dependency "thor"
46
- spec.add_dependency "uglifier"
47
45
  spec.add_dependency "webrick"
48
46
  spec.add_dependency "yaml"
49
-
50
- spec.add_development_dependency "pry", "~> 0.12.2"
51
- spec.add_development_dependency "rake", "~> 13.0"
52
- spec.add_development_dependency "rspec", "~> 3.11"
53
- spec.add_development_dependency "rubocop", "~> 1.58"
54
47
  end
data/exe/capsium CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "capsium/cli"
4
+ require File.expand_path("../lib/capsium", __dir__)
5
5
 
6
6
  Capsium::Cli.start(ARGV)
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capsium
4
+ class Cli
5
+ class Convert < Thor
6
+ extend ThorExt::Start
7
+
8
+ desc "jekyll PACKAGE_FILE OUTPUT_DIRECTORY",
9
+ "Convert a Capsium package to a Jekyll site"
10
+
11
+ def jekyll(package_file, output_directory)
12
+ converter = Capsium::Converters::Jekyll.new(package_file,
13
+ output_directory)
14
+ converter.convert
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Capsium
6
+ class Cli
7
+ class Package < Thor
8
+ extend ThorExt::Start
9
+
10
+ desc "info PACKAGE_PATH", "Display information about the package"
11
+
12
+ def info(path_to_package)
13
+ package = Capsium::Package.new(path_to_package)
14
+ puts "Package Path: #{package.path}"
15
+ puts "Routes: #{package.routes.to_hash}"
16
+ puts "Manifest: #{package.manifest.to_hash}"
17
+ end
18
+
19
+ desc "manifest PATH_TO_PACKAGE",
20
+ "Show the manifest content of the package"
21
+
22
+ def manifest(path_to_package)
23
+ package = Capsium::Package.new(path_to_package)
24
+ puts JSON.pretty_generate(package.manifest.to_hash)
25
+ end
26
+
27
+ desc "storage PATH_TO_PACKAGE", "Show the storage datasets of the package"
28
+
29
+ def storage(path_to_package)
30
+ package = Capsium::Package.new(path_to_package)
31
+ puts JSON.pretty_generate(package.storage.to_hash)
32
+ end
33
+
34
+ desc "routes PATH_TO_PACKAGE", "Show the routes of the package"
35
+
36
+ def routes(path_to_package)
37
+ package = Capsium::Package.new(path_to_package)
38
+ puts JSON.pretty_generate(package.routes.to_hash)
39
+ end
40
+
41
+ desc "metadata PATH_TO_PACKAGE", "Show the metadata of the package"
42
+
43
+ def metadata(path_to_package)
44
+ package = Capsium::Package.new(path_to_package)
45
+ puts JSON.pretty_generate(package.metadata.to_hash)
46
+ end
47
+
48
+ desc "pack PATH_TO_PACKAGE_FOLDER", "Package the files into the package"
49
+ option :force, type: :boolean, default: false, aliases: "-f"
50
+
51
+ def pack(path_to_package)
52
+ package = Capsium::Package.new(path_to_package)
53
+ packager = Capsium::Packager.new
54
+ packager.pack(package, options)
55
+ end
56
+
57
+ desc "unpack PACKAGE_FILE", "Unpack a .cap package into a directory"
58
+ option :output, type: :string, aliases: "-o", desc: "Output directory"
59
+
60
+ def unpack(path_to_package)
61
+ destination = options[:output] || File.basename(path_to_package, ".cap")
62
+ Capsium::Packager.new.unpack(path_to_package, destination)
63
+ puts "Package unpacked to: #{destination}"
64
+ end
65
+
66
+ desc "validate PACKAGE_PATH", "Validate a package directory or .cap file"
67
+
68
+ def validate(path_to_package)
69
+ results = Capsium::Package::Validator.new(path_to_package).run
70
+ results.each { |result| puts format_result(result) }
71
+ return if results.all?(&:ok?)
72
+
73
+ raise Thor::Error, "Package validation failed"
74
+ end
75
+
76
+ private
77
+
78
+ def format_result(result)
79
+ line = "#{result.ok? ? 'PASS' : 'FAIL'} #{result.name}"
80
+ return line if result.messages.empty?
81
+
82
+ "#{line}: #{result.messages.join('; ')}"
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capsium
4
+ class Cli
5
+ class Reactor < Thor
6
+ extend ThorExt::Start
7
+
8
+ desc "serve PACKAGE_PATH",
9
+ "Start the Capsium reactor to serve the package"
10
+ option :port, type: :numeric, default: Capsium::Reactor::DEFAULT_PORT
11
+ option :do_not_listen, type: :boolean, default: false
12
+
13
+ def serve(path_to_package)
14
+ reactor = Capsium::Reactor.new(
15
+ package: path_to_package,
16
+ port: options[:port],
17
+ do_not_listen: options[:do_not_listen]
18
+ )
19
+ reactor.serve
20
+ ensure
21
+ reactor&.package&.cleanup
22
+ end
23
+ end
24
+ end
25
+ end
data/lib/capsium/cli.rb CHANGED
@@ -1,88 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # lib/capsium/cli.rb
4
3
  require "thor"
5
- require_relative "package"
6
- require_relative "reactor"
7
- require_relative "thor_ext"
8
- require_relative "converters/jekyll"
9
4
 
10
5
  module Capsium
11
6
  class Cli < Thor
12
7
  extend ThorExt::Start
13
8
 
14
- class Package < Thor
15
- extend ThorExt::Start
16
-
17
- desc "info PACKAGE_PATH", "Display information about the package"
18
- def info(path_to_package)
19
- package = Capsium::Package.new(path_to_package)
20
- puts "Package Path: #{package.path}"
21
- puts "Routes: #{package.routes.as_json}"
22
- puts "Manifest: #{package.manifest.as_json}"
23
- end
24
-
25
- desc "manifest PATH_TO_PACKAGE", "Show the manifest content of the package"
26
- def manifest(path_to_package)
27
- package = Capsium::Package.new(path_to_package)
28
- puts JSON.pretty_generate(package.manifest.as_json)
29
- end
30
-
31
- desc "storage PATH_TO_PACKAGE", "Show the storage datasets of the package"
32
- def storage(path_to_package)
33
- package = Capsium::Package.new(path_to_package)
34
- puts JSON.pretty_generate(package.storage.as_json)
35
- end
36
-
37
- desc "routes PATH_TO_PACKAGE", "Show the routes of the package"
38
- def routes(path_to_package)
39
- package = Capsium::Package.new(path_to_package)
40
- puts JSON.pretty_generate(package.routes.as_json)
41
- end
42
-
43
- desc "metadata PATH_TO_PACKAGE", "Show the metadata of the package"
44
- def metadata(path_to_package)
45
- package = Capsium::Package.new(path_to_package)
46
- puts JSON.pretty_generate(package.metadata.as_json)
47
- end
48
-
49
- desc "pack PATH_TO_PACKAGE_FOLDER", "Package the files into the package"
50
- option :force, type: :boolean, default: false, aliases: "-f"
51
- def pack(path_to_package)
52
- package = Capsium::Package.new(path_to_package)
53
- packager = Capsium::Packager.new
54
- packager.pack(package, options)
55
- rescue StandardError => e
56
- puts e
57
- puts e.inspect
58
- puts e.backtrace
59
- end
60
- end
61
-
62
- class Reactor < Thor
63
- extend ThorExt::Start
64
- desc "serve PACKAGE_PATH", "Start the Capsium reactor to serve the package"
65
- def serve(path_to_package)
66
- package = Capsium::Package.new(path_to_package)
67
- reactor = Capsium::Reactor.new(package)
68
- reactor.serve
69
- rescue StandardError => e
70
- puts e
71
- puts e.inspect
72
- puts e.backtrace
73
- ensure
74
- package.cleanup
75
- end
76
- end
77
-
78
- class Convert < Thor
79
- extend ThorExt::Start
80
- desc "jekyll SITE_DIRECTORY OUTPUT_DIRECTORY", "Convert a Jekyll site directory to a Capsium package"
81
- def jekyll(site_directory, output_directory)
82
- converter = Capsium::Converters::Jekyll.new(site_directory, output_directory)
83
- converter.convert
84
- end
85
- end
9
+ autoload :Convert, "capsium/cli/convert"
10
+ autoload :Package, "capsium/cli/package"
11
+ autoload :Reactor, "capsium/cli/reactor"
86
12
 
87
13
  desc "package SUBCOMMAND ...ARGS", "Manage packages"
88
14
  subcommand "package", Capsium::Cli::Package
@@ -90,7 +16,7 @@ module Capsium
90
16
  desc "reactor SUBCOMMAND ...ARGS", "Manage the reactor"
91
17
  subcommand "reactor", Capsium::Cli::Reactor
92
18
 
93
- desc "converter SUBCOMMAND ...ARGS", "Convert from another format"
94
- subcommand "converter", Capsium::Cli::Convert
19
+ desc "convert SUBCOMMAND ...ARGS", "Convert from another format"
20
+ subcommand "convert", Capsium::Cli::Convert
95
21
  end
96
22
  end
@@ -1,58 +1,89 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # lib/capsium/converters/jekyll_to_capsium.rb
4
3
  require "fileutils"
5
- require "json"
6
- require "capsium/packager"
4
+ require "pathname"
5
+ require "yaml"
7
6
 
8
7
  module Capsium
9
8
  module Converters
10
9
  class Jekyll
11
- def initialize(site_directory, output_directory)
12
- @site_directory = site_directory
10
+ def initialize(package_file, output_directory)
11
+ @package_file = package_file
13
12
  @output_directory = output_directory
14
13
  end
15
14
 
16
15
  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
16
+ package = Capsium::Package.new(@package_file)
23
17
 
24
- private
18
+ prepare_output_directory
25
19
 
26
- def validate_site_directory
27
- return if Dir.exist?(@site_directory) && File.exist?(File.join(@site_directory, "index.html"))
20
+ write_config_yml(package)
21
+ copy_content_files(package)
22
+ generate_index_html(package)
28
23
 
29
- raise "Invalid Jekyll site directory: #{@site_directory}"
24
+ puts "Capsium package converted to Jekyll site at #{@output_directory}"
30
25
  end
31
26
 
32
- def prepare_package_directory
33
- package_directory = File.join(@output_directory, "capsium_package")
34
- FileUtils.mkdir_p(package_directory)
27
+ private
35
28
 
36
- FileUtils.cp_r(Dir.glob("#{@site_directory}/*"), package_directory)
29
+ def prepare_output_directory
30
+ FileUtils.mkdir_p(@output_directory)
31
+ end
37
32
 
38
- create_manifest(package_directory)
33
+ def write_config_yml(package)
34
+ config = {
35
+ "title" => package.metadata.name || "Capsium Jekyll Site",
36
+ "description" => package.metadata.description || "Generated from Capsium package",
37
+ "baseurl" => "",
38
+ "url" => "",
39
+ "markdown" => "kramdown",
40
+ "theme" => "minima"
41
+ }
42
+ write_file("_config.yml", config.to_yaml)
43
+ end
39
44
 
40
- package_directory
45
+ def write_file(relative_path, content)
46
+ output_path = File.join(@output_directory, relative_path)
47
+ FileUtils.mkdir_p(File.dirname(output_path))
48
+ File.write(output_path, content)
41
49
  end
42
50
 
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
- }
51
+ def copy_content_files(package)
52
+ package.content_files.each do |file_path|
53
+ relative_path = Pathname.new(file_path).relative_path_from(Pathname.new(package.path))
54
+ output_path = File.join(@output_directory, relative_path)
55
+ FileUtils.mkdir_p(File.dirname(output_path))
56
+ FileUtils.cp(file_path, output_path)
57
+ end
58
+ end
50
59
 
51
- File.write(File.join(package_directory, "manifest.json"), JSON.pretty_generate(manifest))
60
+ def generate_index_html(package)
61
+ root_route = package.routes.config.routes.find do |route|
62
+ route.path == "/"
63
+ end
64
+ if root_route
65
+ index_path = File.join(package.path, root_route.resource)
66
+ index_content = File.read(index_path)
67
+ write_file("index.html", index_content)
68
+ else
69
+ write_file("index.html", default_index_html(package))
70
+ end
52
71
  end
53
72
 
54
- def cleanup_package_directory(package_directory)
55
- FileUtils.rm_rf(package_directory)
73
+ def default_index_html(package)
74
+ <<~HTML
75
+ <!DOCTYPE html>
76
+ <html>
77
+ <head>
78
+ <meta charset="utf-8">
79
+ <title>#{package.metadata.name || 'Capsium Jekyll Site'}</title>
80
+ </head>
81
+ <body>
82
+ <h1>Welcome to #{package.metadata.name || 'Capsium Jekyll Site'}</h1>
83
+ <p>This site was generated from a Capsium package.</p>
84
+ </body>
85
+ </html>
86
+ HTML
56
87
  end
57
88
  end
58
89
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capsium
4
+ module Converters
5
+ autoload :Jekyll, "capsium/converters/jekyll"
6
+ end
7
+ end
@@ -1,85 +1,105 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # lib/capsium/package/dataset.rb
4
- require "json"
5
- require "yaml"
6
3
  require "csv"
4
+ require "forwardable"
5
+ require "json"
6
+ require "json-schema"
7
7
  require "sqlite3"
8
+ require "yaml"
8
9
 
9
10
  module Capsium
10
11
  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
12
+ # A loaded dataset. "source"/"schemaFile"/"databaseFile" are
13
+ # package-relative paths (ARCHITECTURE.md section 5), resolved against
14
+ # the package directory.
15
+ class Dataset
16
+ attr_reader :name, :config, :data, :package_path
17
+
18
+ extend Forwardable
19
+
20
+ def_delegators :@config, :to_json
21
+
22
+ def initialize(name:, config:, package_path:)
23
+ @name = name
24
+ @config = config
25
+ @package_path = package_path
29
26
  @data = load_data
30
27
  end
31
28
 
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
29
+ def source_path
30
+ File.join(@package_path, @config.backing_file)
31
+ end
32
+
33
+ def schema_path
34
+ return unless @config.schema_file
35
+
36
+ File.join(@package_path, @config.schema_file)
42
37
  end
43
38
 
44
39
  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
40
+ return nil unless File.file?(source_path)
41
+
42
+ case @config.format
43
+ when "yaml" then YAML.load_file(source_path)
44
+ when "json" then JSON.parse(File.read(source_path))
45
+ when "csv" then CSV.read(source_path, headers: true)
46
+ when "tsv" then CSV.read(source_path, col_sep: "\t", headers: true)
47
+ when "sqlite" then load_sqlite_data
51
48
  else
52
- raise "Unsupported data file type: #{@type}"
49
+ raise Error, "Unsupported data file type: #{@config.format}"
53
50
  end
54
51
  end
55
52
 
56
- def as_json
57
- {
58
- name: name,
59
- source: relative_path(path),
60
- format: type.to_s
61
- }
62
- end
53
+ def validate
54
+ return true unless @config.schema_file && @config.schema_type == "json-schema"
63
55
 
64
- def to_json(*_args)
65
- JSON.pretty_generate(as_json)
56
+ JSON::Validator.validate!(load_schema, @data.to_json)
66
57
  end
67
58
 
68
- def relative_path(path)
69
- Pathname.new(path).relative_path_from(@data_path).to_s
59
+ # File-existence and schema validations for this dataset. Returns a
60
+ # list of human-readable problems; empty when valid.
61
+ def validation_errors
62
+ problems = []
63
+ unless File.file?(source_path)
64
+ problems << "dataset source missing on disk: #{@config.backing_file}"
65
+ end
66
+ problems.concat(schema_validation_errors)
67
+ problems
70
68
  end
71
69
 
72
70
  private
73
71
 
72
+ def schema_validation_errors
73
+ return [] unless @config.schema_file
74
+ unless File.file?(schema_path)
75
+ return ["dataset schema missing on disk: #{@config.schema_file}"]
76
+ end
77
+
78
+ validate
79
+ []
80
+ rescue JSON::Schema::ValidationError => e
81
+ ["dataset #{@name} fails schema validation: #{e.message}"]
82
+ end
83
+
84
+ def load_schema
85
+ case File.extname(schema_path).downcase
86
+ when ".yaml", ".yml" then YAML.load_file(schema_path)
87
+ else JSON.parse(File.read(schema_path))
88
+ end
89
+ end
90
+
74
91
  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};")
92
+ db = SQLite3::Database.new(source_path)
93
+ tables = @config.table ? [@config.table] : sqlite_tables(db)
94
+ tables.to_h do |table_name|
95
+ [table_name, db.execute("SELECT * FROM #{table_name};")]
81
96
  end
82
- data
97
+ ensure
98
+ db&.close
99
+ end
100
+
101
+ def sqlite_tables(db)
102
+ db.execute("SELECT name FROM sqlite_master WHERE type='table';").map(&:first)
83
103
  end
84
104
  end
85
105
  end