chronicle-etl 0.3.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +35 -0
  3. data/.rubocop.yml +31 -1
  4. data/Guardfile +7 -0
  5. data/README.md +157 -82
  6. data/Rakefile +4 -2
  7. data/chronicle-etl.gemspec +11 -3
  8. data/exe/chronicle-etl +1 -1
  9. data/lib/chronicle/etl/cli/connectors.rb +34 -5
  10. data/lib/chronicle/etl/cli/jobs.rb +90 -24
  11. data/lib/chronicle/etl/cli/main.rb +41 -19
  12. data/lib/chronicle/etl/cli/plugins.rb +62 -0
  13. data/lib/chronicle/etl/cli/subcommand_base.rb +2 -2
  14. data/lib/chronicle/etl/cli.rb +9 -0
  15. data/lib/chronicle/etl/config.rb +7 -4
  16. data/lib/chronicle/etl/configurable.rb +163 -0
  17. data/lib/chronicle/etl/exceptions.rb +29 -1
  18. data/lib/chronicle/etl/extractors/csv_extractor.rb +24 -23
  19. data/lib/chronicle/etl/extractors/extractor.rb +16 -15
  20. data/lib/chronicle/etl/extractors/file_extractor.rb +34 -11
  21. data/lib/chronicle/etl/extractors/helpers/input_reader.rb +76 -0
  22. data/lib/chronicle/etl/extractors/json_extractor.rb +19 -18
  23. data/lib/chronicle/etl/job.rb +8 -2
  24. data/lib/chronicle/etl/job_definition.rb +20 -5
  25. data/lib/chronicle/etl/loaders/csv_loader.rb +36 -9
  26. data/lib/chronicle/etl/loaders/helpers/encoding_helper.rb +18 -0
  27. data/lib/chronicle/etl/loaders/json_loader.rb +44 -0
  28. data/lib/chronicle/etl/loaders/loader.rb +28 -2
  29. data/lib/chronicle/etl/loaders/rest_loader.rb +5 -5
  30. data/lib/chronicle/etl/loaders/table_loader.rb +18 -37
  31. data/lib/chronicle/etl/logger.rb +6 -2
  32. data/lib/chronicle/etl/models/base.rb +3 -0
  33. data/lib/chronicle/etl/models/entity.rb +8 -2
  34. data/lib/chronicle/etl/models/raw.rb +26 -0
  35. data/lib/chronicle/etl/registry/connector_registration.rb +6 -0
  36. data/lib/chronicle/etl/registry/plugin_registry.rb +70 -0
  37. data/lib/chronicle/etl/registry/registry.rb +27 -14
  38. data/lib/chronicle/etl/runner.rb +35 -17
  39. data/lib/chronicle/etl/serializers/jsonapi_serializer.rb +6 -0
  40. data/lib/chronicle/etl/serializers/raw_serializer.rb +10 -0
  41. data/lib/chronicle/etl/serializers/serializer.rb +2 -1
  42. data/lib/chronicle/etl/transformers/image_file_transformer.rb +22 -28
  43. data/lib/chronicle/etl/transformers/null_transformer.rb +1 -1
  44. data/lib/chronicle/etl/transformers/transformer.rb +3 -2
  45. data/lib/chronicle/etl/version.rb +1 -1
  46. data/lib/chronicle/etl.rb +12 -4
  47. metadata +123 -18
  48. data/.ruby-version +0 -1
  49. data/lib/chronicle/etl/extractors/helpers/filesystem_reader.rb +0 -104
  50. data/lib/chronicle/etl/loaders/stdout_loader.rb +0 -14
  51. data/lib/chronicle/etl/models/generic.rb +0 -23
@@ -1,4 +1,6 @@
1
1
  require 'pp'
2
+ require 'tty-prompt'
3
+
2
4
  module Chronicle
3
5
  module ETL
4
6
  module CLI
@@ -7,18 +9,28 @@ module Chronicle
7
9
  default_task "start"
8
10
  namespace :jobs
9
11
 
10
- class_option :extractor, aliases: '-e', desc: "Extractor class. Default: stdin", banner: 'extractor-name'
12
+ class_option :name, aliases: '-j', desc: 'Job configuration name'
13
+
14
+ class_option :extractor, aliases: '-e', desc: "Extractor class. Default: stdin", banner: 'NAME'
11
15
  class_option :'extractor-opts', desc: 'Extractor options', type: :hash, default: {}
12
- class_option :transformer, aliases: '-t', desc: 'Transformer class. Default: null', banner: 'transformer-name'
16
+ class_option :transformer, aliases: '-t', desc: 'Transformer class. Default: null', banner: 'NAME'
13
17
  class_option :'transformer-opts', desc: 'Transformer options', type: :hash, default: {}
14
- class_option :loader, aliases: '-l', desc: 'Loader class. Default: stdout', banner: 'loader-name'
18
+ class_option :loader, aliases: '-l', desc: 'Loader class. Default: table', banner: 'NAME'
15
19
  class_option :'loader-opts', desc: 'Loader options', type: :hash, default: {}
16
- class_option :name, aliases: '-j', desc: 'Job configuration name'
17
20
 
18
- map run: :start # Thor doesn't like `run` as a command name
21
+ # This is an array to deal with shell globbing
22
+ class_option :input, aliases: '-i', desc: 'Input filename or directory', default: [], type: 'array', banner: 'FILENAME'
23
+ class_option :since, desc: "Load records SINCE this date", banner: 'DATE'
24
+ class_option :until, desc: "Load records UNTIL this date", banner: 'DATE'
25
+ class_option :limit, desc: "Only extract the first LIMIT records", banner: 'N'
26
+
27
+ class_option :output, aliases: '-o', desc: 'Output filename', type: 'string'
28
+ class_option :fields, desc: 'Output only these fields', type: 'array', banner: 'field1 field2 ...'
29
+ class_option :header_row, desc: 'Output the header row of tabular output', type: 'boolean'
30
+
31
+ # Thor doesn't like `run` as a command name
32
+ map run: :start
19
33
  desc "run", "Start a job"
20
- option :log_level, desc: 'Log level (debug, info, warn, error, fatal)', default: 'info'
21
- option :verbose, aliases: '-v', desc: 'Set log level to verbose', type: :boolean
22
34
  option :dry_run, desc: 'Only run the extraction and transform steps, not the loading', type: :boolean
23
35
  long_desc <<-LONG_DESC
24
36
  This will run an ETL job. Each job needs three parts:
@@ -33,25 +45,40 @@ module Chronicle
33
45
  LONG_DESC
34
46
  # Run an ETL job
35
47
  def start
36
- setup_log_level
37
- job_definition = build_job_definition(options)
38
- job = Chronicle::ETL::Job.new(job_definition)
39
- runner = Chronicle::ETL::Runner.new(job)
40
- runner.run!
48
+ run_job(options)
49
+ rescue Chronicle::ETL::JobDefinitionError => e
50
+ missing_plugins = e.job_definition.errors
51
+ .select { |error| error.is_a?(Chronicle::ETL::PluginLoadError) }
52
+ .map(&:name)
53
+ .uniq
54
+
55
+ install_missing_plugins(missing_plugins)
56
+ run_job(options)
41
57
  end
42
58
 
43
59
  desc "create", "Create a job"
44
60
  # Create an ETL job
45
61
  def create
46
62
  job_definition = build_job_definition(options)
63
+ job_definition.validate!
64
+
47
65
  path = File.join('chronicle', 'etl', 'jobs', options[:name])
48
66
  Chronicle::ETL::Config.write(path, job_definition.definition)
67
+ rescue Chronicle::ETL::JobDefinitionError => e
68
+ Chronicle::ETL::Logger.debug(e.full_message)
69
+ Chronicle::ETL::Logger.fatal("Job definition error".red)
49
70
  end
50
71
 
51
72
  desc "show", "Show details about a job"
52
73
  # Show an ETL job
53
74
  def show
54
- puts Chronicle::ETL::Job.new(build_job_definition(options))
75
+ job_definition = build_job_definition(options)
76
+ job_definition.validate!
77
+ puts Chronicle::ETL::Job.new(job_definition)
78
+ rescue Chronicle::ETL::JobDefinitionError => e
79
+ Chronicle::ETL::Logger.debug(e.full_message)
80
+ Chronicle::ETL::Logger.fatal("Job definition error".red)
81
+ exit 1
55
82
  end
56
83
 
57
84
  desc "list", "List all available jobs"
@@ -69,28 +96,52 @@ LONG_DESC
69
96
  [job, extractor, transformer, loader]
70
97
  end
71
98
 
72
- headers = ['name', 'extractor', 'transformer', 'loader'].map{|h| h.upcase.bold }
99
+ headers = ['name', 'extractor', 'transformer', 'loader'].map { |h| h.upcase.bold }
73
100
 
101
+ puts "Available jobs:"
74
102
  table = TTY::Table.new(headers, job_details)
75
103
  puts table.render(indent: 0, padding: [0, 2])
104
+ rescue Chronicle::ETL::ConfigError => e
105
+ Chronicle::ETL::Logger.debug(e.full_message)
106
+ Chronicle::ETL::Logger.fatal("Error reading config. #{e.message}".red)
107
+ exit 1
76
108
  end
77
109
 
78
110
  private
79
111
 
80
- def setup_log_level
81
- if options[:verbose]
82
- Chronicle::ETL::Logger.log_level = Chronicle::ETL::Logger::DEBUG
83
- elsif options[:log_level]
84
- level = Chronicle::ETL::Logger.const_get(options[:log_level].upcase)
85
- Chronicle::ETL::Logger.log_level = level
112
+ def run_job(options)
113
+ job_definition = build_job_definition(options)
114
+ job = Chronicle::ETL::Job.new(job_definition)
115
+ runner = Chronicle::ETL::Runner.new(job)
116
+ runner.run!
117
+ end
118
+
119
+ # TODO: probably could merge this with something in cli/plugin
120
+ def install_missing_plugins(missing_plugins)
121
+ prompt = TTY::Prompt.new
122
+ message = "Plugin#{'s' if missing_plugins.count > 1} specified by job not installed.\n"
123
+ message += "Do you want to install "
124
+ message += missing_plugins.map { |name| "chronicle-#{name}".bold}.join(", ")
125
+ message += " and start the job?"
126
+ install = prompt.yes?(message)
127
+ return unless install
128
+
129
+ spinner = TTY::Spinner.new("[:spinner] Installing plugins...", format: :dots_2)
130
+ spinner.auto_spin
131
+ missing_plugins.each do |plugin|
132
+ Chronicle::ETL::Registry::PluginRegistry.install(plugin)
86
133
  end
134
+ spinner.success("(#{'successful'.green})")
135
+ rescue Chronicle::ETL::PluginNotAvailableError => e
136
+ spinner.error("Error".red)
137
+ Chronicle::ETL::Logger.fatal("Plugin '#{e.name}' could not be installed".red)
87
138
  end
88
139
 
89
140
  # Create job definition by reading config file and then overwriting with flag options
90
141
  def build_job_definition(options)
91
142
  definition = Chronicle::ETL::JobDefinition.new
92
143
  definition.add_config(load_job_config(options[:name]))
93
- definition.add_config(process_flag_options(options))
144
+ definition.add_config(process_flag_options(options).transform_keys(&:to_sym))
94
145
  definition
95
146
  end
96
147
 
@@ -100,19 +151,34 @@ LONG_DESC
100
151
 
101
152
  # Takes flag options and turns them into a runner config
102
153
  def process_flag_options options
154
+ extractor_options = options[:'extractor-opts'].merge({
155
+ input: (options[:input] if options[:input].any?),
156
+ since: options[:since],
157
+ until: options[:until],
158
+ limit: options[:limit],
159
+ }.compact)
160
+
161
+ transformer_options = options[:'transformer-opts']
162
+
163
+ loader_options = options[:'loader-opts'].merge({
164
+ output: options[:output],
165
+ header_row: options[:header_row],
166
+ fields: options[:fields]
167
+ }.compact)
168
+
103
169
  {
104
170
  dry_run: options[:dry_run],
105
171
  extractor: {
106
172
  name: options[:extractor],
107
- options: options[:'extractor-opts']
173
+ options: extractor_options
108
174
  }.compact,
109
175
  transformer: {
110
176
  name: options[:transformer],
111
- options: options[:'transformer-opts']
177
+ options: transformer_options
112
178
  }.compact,
113
179
  loader: {
114
180
  name: options[:loader],
115
- options: options[:'loader-opts']
181
+ options: loader_options
116
182
  }.compact
117
183
  }
118
184
  end
@@ -1,17 +1,18 @@
1
- require 'thor'
2
- require 'chronicle/etl'
3
1
  require 'colorize'
4
2
 
5
- require 'chronicle/etl/cli/subcommand_base'
6
- require 'chronicle/etl/cli/connectors'
7
- require 'chronicle/etl/cli/jobs'
8
-
9
3
  module Chronicle
10
4
  module ETL
11
5
  module CLI
12
6
  # Main entrypoint for CLI app
13
- class Main < Thor
14
- class_option "verbose", type: :boolean, default: false
7
+ class Main < ::Thor
8
+ class_before :set_log_level
9
+ class_before :set_color_output
10
+
11
+ class_option :log_level, desc: 'Log level (debug, info, warn, error, fatal, silent)', default: 'info'
12
+ class_option :verbose, aliases: '-v', desc: 'Set log level to verbose', type: :boolean
13
+ class_option :silent, desc: 'Silence all output', type: :boolean
14
+ class_option :'no-color', desc: 'Disable colour output', type: :boolean
15
+
15
16
  default_task "jobs"
16
17
 
17
18
  desc 'connectors:COMMAND', 'Connectors available for ETL jobs', hide: true
@@ -20,17 +21,11 @@ module Chronicle
20
21
  desc 'jobs:COMMAND', 'Configure and run jobs', hide: true
21
22
  subcommand 'jobs', Jobs
22
23
 
24
+ desc 'plugins:COMMAND', 'Configure plugins', hide: true
25
+ subcommand 'plugins', Plugins
26
+
23
27
  # Entrypoint for the CLI
24
28
  def self.start(given_args = ARGV, config = {})
25
- if given_args[0] == "--version"
26
- puts "#{Chronicle::ETL::VERSION}"
27
- exit
28
- end
29
-
30
- if given_args.none?
31
- abort "No command entered or job specified. To see commands, run `chronicle-etl help`".red
32
- end
33
-
34
29
  # take a subcommand:command and splits them so Thor knows how to hand off to the subcommand class
35
30
  if given_args.any? && given_args[0].include?(':')
36
31
  commands = given_args.shift.split(':')
@@ -40,10 +35,20 @@ module Chronicle
40
35
  super(given_args, config)
41
36
  end
42
37
 
38
+ def self.exit_on_failure?
39
+ true
40
+ end
41
+
42
+ desc "version", "Show version"
43
+ map %w(--version -v) => :version
44
+ def version
45
+ shell.say "chronicle-etl #{Chronicle::ETL::VERSION}"
46
+ end
47
+
43
48
  # Displays help options for chronicle-etl
44
49
  def help(meth = nil, subcommand = false)
45
50
  if meth && !respond_to?(meth)
46
- klass, task = Thor::Util.find_class_and_task_by_namespace("#{meth}:#{meth}")
51
+ klass, task = ::Thor::Util.find_class_and_task_by_namespace("#{meth}:#{meth}")
47
52
  klass.start(['-h', task].compact, shell: shell)
48
53
  else
49
54
  shell.say "ABOUT".bold
@@ -64,7 +69,7 @@ module Chronicle
64
69
 
65
70
  list = []
66
71
 
67
- Thor::Util.thor_classes_in(Chronicle::ETL::CLI).each do |thor_class|
72
+ ::Thor::Util.thor_classes_in(Chronicle::ETL::CLI).each do |thor_class|
68
73
  list += thor_class.printable_tasks(false)
69
74
  end
70
75
  list.sort! { |a, b| a[0] <=> b[0] }
@@ -85,6 +90,23 @@ module Chronicle
85
90
  shell.say
86
91
  end
87
92
  end
93
+
94
+ no_commands do
95
+ def set_color_output
96
+ String.disable_colorization true if options[:'no-color'] || ENV['NO_COLOR']
97
+ end
98
+
99
+ def set_log_level
100
+ if options[:silent]
101
+ Chronicle::ETL::Logger.log_level = Chronicle::ETL::Logger::SILENT
102
+ elsif options[:verbose]
103
+ Chronicle::ETL::Logger.log_level = Chronicle::ETL::Logger::DEBUG
104
+ elsif options[:log_level]
105
+ level = Chronicle::ETL::Logger.const_get(options[:log_level].upcase)
106
+ Chronicle::ETL::Logger.log_level = level
107
+ end
108
+ end
109
+ end
88
110
  end
89
111
  end
90
112
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-prompt"
4
+ require "tty-spinner"
5
+
6
+
7
+ module Chronicle
8
+ module ETL
9
+ module CLI
10
+ # CLI commands for working with ETL plugins
11
+ class Plugins < SubcommandBase
12
+ default_task 'list'
13
+ namespace :plugins
14
+
15
+ desc "install", "Install a plugin"
16
+ def install(name)
17
+ spinner = TTY::Spinner.new("[:spinner] Installing plugin #{name}...", format: :dots_2)
18
+ spinner.auto_spin
19
+ Chronicle::ETL::Registry::PluginRegistry.install(name)
20
+ spinner.success("(#{'successful'.green})")
21
+ rescue Chronicle::ETL::PluginError => e
22
+ spinner.error("Error".red)
23
+ Chronicle::ETL::Logger.debug(e.full_message)
24
+ Chronicle::ETL::Logger.fatal("Plugin '#{name}' could not be installed".red)
25
+ exit 1
26
+ end
27
+
28
+ desc "uninstall", "Unintall a plugin"
29
+ def uninstall(name)
30
+ spinner = TTY::Spinner.new("[:spinner] Uninstalling plugin #{name}...", format: :dots_2)
31
+ spinner.auto_spin
32
+ Chronicle::ETL::Registry::PluginRegistry.uninstall(name)
33
+ spinner.success("(#{'successful'.green})")
34
+ rescue Chronicle::ETL::PluginError => e
35
+ spinner.error("Error".red)
36
+ Chronicle::ETL::Logger.debug(e.full_message)
37
+ Chronicle::ETL::Logger.fatal("Plugin '#{name}' could not be uninstalled (was it installed?)".red)
38
+ exit 1
39
+ end
40
+
41
+ desc "list", "Lists available plugins"
42
+ # Display all available plugins that chronicle-etl has access to
43
+ def list
44
+ plugins = Chronicle::ETL::Registry::PluginRegistry.all_installed_latest
45
+
46
+ info = plugins.map do |plugin|
47
+ {
48
+ name: plugin.name.sub("chronicle-", ""),
49
+ description: plugin.description,
50
+ version: plugin.version
51
+ }
52
+ end
53
+
54
+ headers = ['name', 'description', 'latest version'].map{ |h| h.to_s.upcase.bold }
55
+ table = TTY::Table.new(headers, info.map(&:values))
56
+ puts "Installed plugins:"
57
+ puts table.render(indent: 2, padding: [0, 0])
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -2,11 +2,11 @@ module Chronicle
2
2
  module ETL
3
3
  module CLI
4
4
  # Base class for CLI subcommands. Overrides Thor methods so we can use command:subcommand syntax
5
- class SubcommandBase < Thor
5
+ class SubcommandBase < ::Thor
6
6
  # Print usage instructions for a subcommand
7
7
  def self.help(shell, subcommand = false)
8
8
  list = printable_commands(true, subcommand)
9
- Thor::Util.thor_classes_in(self).each do |klass|
9
+ ::Thor::Util.thor_classes_in(self).each do |klass|
10
10
  list += klass.printable_commands(false)
11
11
  end
12
12
  list.sort! { |a, b| a[0] <=> b[0] }
@@ -0,0 +1,9 @@
1
+ require 'thor'
2
+ require 'thor/hollaback'
3
+ require 'chronicle/etl'
4
+
5
+ require 'chronicle/etl/cli/subcommand_base'
6
+ require 'chronicle/etl/cli/connectors'
7
+ require 'chronicle/etl/cli/jobs'
8
+ require 'chronicle/etl/cli/plugins'
9
+ require 'chronicle/etl/cli/main'
@@ -24,16 +24,14 @@ module Chronicle
24
24
 
25
25
  # Returns all jobs available in ~/.config/chronicle/etl/jobs/*.yml
26
26
  def available_jobs
27
- job_directory = Runcom::Config.new('chronicle/etl/jobs').current
28
- Dir.glob(File.join(job_directory, "*.yml")).map do |filename|
27
+ Dir.glob(File.join(config_directory("jobs"), "*.yml")).map do |filename|
29
28
  File.basename(filename, ".*")
30
29
  end
31
30
  end
32
31
 
33
32
  # Returns all available credentials available in ~/.config/chronicle/etl/credentials/*.yml
34
33
  def available_credentials
35
- job_directory = Runcom::Config.new('chronicle/etl/credentials').current
36
- Dir.glob(File.join(job_directory, "*.yml")).map do |filename|
34
+ Dir.glob(File.join(config_directory("credentials"), "*.yml")).map do |filename|
37
35
  File.basename(filename, ".*")
38
36
  end
39
37
  end
@@ -48,6 +46,11 @@ module Chronicle
48
46
  def load_credentials(name)
49
47
  config = self.load("chronicle/etl/credentials/#{name}.yml")
50
48
  end
49
+
50
+ def config_directory(type)
51
+ path = "chronicle/etl/#{type}"
52
+ Runcom::Config.new(path).current || raise(Chronicle::ETL::ConfigError, "Could not access config directory (#{path})")
53
+ end
51
54
  end
52
55
  end
53
56
  end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ostruct"
4
+
5
+ module Chronicle
6
+ module ETL
7
+ # A mixin that gives a class
8
+ # a {Chronicle::ETL::Configurable::ClassMethods#setting} macro to define
9
+ # settings and their properties (require, type, etc)
10
+ #
11
+ # @example Basic usage
12
+ # class Test < Chronicle::ETL::Extractor
13
+ # include Chronicle::ETL::Configurable
14
+ # setting :when, type: :date, required: true
15
+ # end
16
+ #
17
+ # t = Test.new(when: '2022-02-24')
18
+ # t.config.when
19
+ module Configurable
20
+ # An individual setting for this Configurable
21
+ Setting = Struct.new(:default, :required, :type)
22
+ private_constant :Setting
23
+
24
+ # Collection of user-supplied options for this Configurable
25
+ class Config < OpenStruct
26
+ # Config values that aren't nil, as a hash
27
+ def compacted_h
28
+ to_h.compact
29
+ end
30
+ end
31
+
32
+ # @private
33
+ def self.included(klass)
34
+ klass.extend(ClassMethods)
35
+ klass.include(InstanceMethods)
36
+ klass.prepend(Initializer)
37
+ end
38
+
39
+ # Initializer method for classes that have Configurable mixed in
40
+ module Initializer
41
+ # Make sure this class has a default @config ready to use
42
+ def initialize(*args)
43
+ @config = initialize_default_config
44
+ super
45
+ end
46
+ end
47
+
48
+ # Instance methods for classes that have Configurable mixed in
49
+ module InstanceMethods
50
+ attr_reader :config
51
+
52
+ # Take given options and apply them to this class's settings
53
+ # and make them available in @config and validates that they
54
+ # conform to setting rules
55
+ def apply_options(options)
56
+ options.transform_keys!(&:to_sym)
57
+
58
+ options.each do |name, value|
59
+ setting = self.class.all_settings[name]
60
+ raise(Chronicle::ETL::ConnectorConfigurationError, "Unrecognized setting: #{name}") unless setting
61
+
62
+ @config[name] = coerced_value(setting, value)
63
+ end
64
+ validate_config
65
+ options
66
+ end
67
+
68
+ # Name of all settings available to this class
69
+ def self.settings
70
+ self.class.all_settings.keys
71
+ end
72
+
73
+ private
74
+
75
+ def initialize_default_config
76
+ self.class.config_with_defaults
77
+ end
78
+
79
+ def validate_config
80
+ missing = (self.class.all_required_settings.keys - @config.compacted_h.keys)
81
+ raise Chronicle::ETL::ConnectorConfigurationError, "Missing options: #{missing}" if missing.count.positive?
82
+ end
83
+
84
+ def coerced_value(setting, value)
85
+ setting.type ? __send__("coerce_#{setting.type}", value) : value
86
+ end
87
+
88
+ def coerce_string(value)
89
+ value.to_s
90
+ end
91
+
92
+ # TODO: think about whether to split up float, integer
93
+ def coerce_numeric(value)
94
+ value.to_f
95
+ end
96
+
97
+ def coerce_boolean(value)
98
+ if value.is_a?(String)
99
+ value.downcase == "true"
100
+ else
101
+ value
102
+ end
103
+ end
104
+
105
+ def coerce_time(value)
106
+ # TODO: handle durations like '3h'
107
+ if value.is_a?(String)
108
+ Time.parse(value)
109
+ else
110
+ value
111
+ end
112
+ end
113
+ end
114
+
115
+ # Class methods for classes that have Configurable mixed in
116
+ module ClassMethods
117
+ # Macro for creating a setting on a class {::Chronicle::ETL::Configurable}
118
+ #
119
+ # @param [String] name Name of the setting
120
+ # @param [Boolean] required whether setting is required
121
+ # @param [Object] default Default value
122
+ # @param [Symbol] type Type
123
+ #
124
+ # @example Basic usage
125
+ # setting :when, type: :date, required: true
126
+ #
127
+ # @see ::Chronicle::ETL::Configurable
128
+ def setting(name, default: nil, required: false, type: nil)
129
+ s = Setting.new(default, required, type)
130
+ settings[name] = s
131
+ end
132
+
133
+ # Collect all settings defined on this class and its ancestors (that
134
+ # have Configurable mixin included)
135
+ def all_settings
136
+ if superclass.include?(Chronicle::ETL::Configurable)
137
+ superclass.all_settings.merge(settings)
138
+ else
139
+ settings
140
+ end
141
+ end
142
+
143
+ # Filters settings to those that are required.
144
+ def all_required_settings
145
+ all_settings.select { |_name, setting| setting.required } || {}
146
+ end
147
+
148
+ def settings
149
+ @settings ||= {}
150
+ end
151
+
152
+ def setting_exists?(name)
153
+ all_settings.keys.include? name
154
+ end
155
+
156
+ def config_with_defaults
157
+ s = all_settings.transform_values(&:default)
158
+ Config.new(s)
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -1,9 +1,33 @@
1
1
  module Chronicle
2
2
  module ETL
3
- class Error < StandardError; end;
3
+ class Error < StandardError; end
4
+
5
+ class ConfigError < Error; end
4
6
 
5
7
  class RunnerTypeError < Error; end
6
8
 
9
+ class JobDefinitionError < Error
10
+ attr_reader :job_definition
11
+
12
+ def initialize(job_definition)
13
+ @job_definition = job_definition
14
+ super
15
+ end
16
+ end
17
+
18
+ class PluginError < Error
19
+ attr_reader :name
20
+
21
+ def initialize(name)
22
+ @name = name
23
+ end
24
+ end
25
+
26
+ class PluginNotAvailableError < PluginError; end
27
+ class PluginLoadError < PluginError; end
28
+
29
+ class ConnectorConfigurationError < Error; end
30
+
7
31
  class ConnectorNotAvailableError < Error
8
32
  def initialize(message, provider: nil, name: nil)
9
33
  super(message)
@@ -16,6 +40,10 @@ module Chronicle
16
40
  class ProviderNotAvailableError < ConnectorNotAvailableError; end
17
41
  class ProviderConnectorNotAvailableError < ConnectorNotAvailableError; end
18
42
 
43
+ class ExtractionError < Error; end
44
+
45
+ class SerializationError < Error; end
46
+
19
47
  class TransformationError < Error
20
48
  attr_reader :transformation
21
49