chronicle-etl 0.3.1 → 0.4.2

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 (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