cure 0.1.2 → 0.4.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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +13 -3
  3. data/.tool-versions +1 -0
  4. data/Dockerfile +1 -1
  5. data/Gemfile +1 -0
  6. data/Gemfile.lock +25 -6
  7. data/README.md +61 -93
  8. data/docs/README.md +33 -0
  9. data/docs/about.md +219 -0
  10. data/docs/builder/add.md +52 -0
  11. data/docs/builder/black_white_list.md +83 -0
  12. data/docs/builder/copy.md +48 -0
  13. data/docs/builder/explode.md +70 -0
  14. data/docs/builder/main.md +43 -0
  15. data/docs/builder/remove.md +46 -0
  16. data/docs/examples/examples.md +164 -0
  17. data/docs/export/main.md +37 -0
  18. data/docs/extract/main.md +89 -0
  19. data/docs/metadata/main.md +29 -0
  20. data/docs/query/main.md +45 -0
  21. data/docs/sources/main.md +36 -0
  22. data/docs/transform/main.md +53 -0
  23. data/docs/validate/main.md +42 -0
  24. data/exe/cure +12 -41
  25. data/exe/cure.old +59 -0
  26. data/lib/cure/builder/base_builder.rb +151 -0
  27. data/lib/cure/builder/candidate.rb +56 -0
  28. data/lib/cure/cli/command.rb +105 -0
  29. data/lib/cure/cli/generate_command.rb +54 -0
  30. data/lib/cure/cli/new_command.rb +52 -0
  31. data/lib/cure/cli/run_command.rb +19 -0
  32. data/lib/cure/cli/templates/README.md.erb +1 -0
  33. data/lib/cure/cli/templates/gemfile.erb +5 -0
  34. data/lib/cure/cli/templates/gitignore.erb +181 -0
  35. data/lib/cure/cli/templates/new_template.rb.erb +31 -0
  36. data/lib/cure/cli/templates/tool-versions.erb +1 -0
  37. data/lib/cure/config.rb +142 -18
  38. data/lib/cure/coordinator.rb +61 -25
  39. data/lib/cure/database.rb +191 -0
  40. data/lib/cure/dsl/builder.rb +26 -0
  41. data/lib/cure/dsl/exporters.rb +45 -0
  42. data/lib/cure/dsl/extraction.rb +60 -0
  43. data/lib/cure/dsl/metadata.rb +33 -0
  44. data/lib/cure/dsl/queries.rb +36 -0
  45. data/lib/cure/dsl/source_files.rb +36 -0
  46. data/lib/cure/dsl/template.rb +131 -0
  47. data/lib/cure/dsl/transformations.rb +95 -0
  48. data/lib/cure/dsl/validator.rb +22 -0
  49. data/lib/cure/export/base_processor.rb +194 -0
  50. data/lib/cure/export/manager.rb +24 -0
  51. data/lib/cure/extract/base_processor.rb +47 -0
  52. data/lib/cure/extract/csv_lookup.rb +14 -3
  53. data/lib/cure/extract/extractor.rb +41 -84
  54. data/lib/cure/extract/filter.rb +118 -0
  55. data/lib/cure/extract/named_range.rb +94 -0
  56. data/lib/cure/extract/named_range_processor.rb +128 -0
  57. data/lib/cure/extract/variable.rb +25 -0
  58. data/lib/cure/extract/variable_processor.rb +57 -0
  59. data/lib/cure/generator/base_generator.rb +14 -4
  60. data/lib/cure/generator/case_generator.rb +10 -3
  61. data/lib/cure/generator/character_generator.rb +9 -3
  62. data/lib/cure/generator/erb_generator.rb +21 -0
  63. data/lib/cure/generator/eval_generator.rb +34 -0
  64. data/lib/cure/generator/faker_generator.rb +7 -1
  65. data/lib/cure/generator/guid_generator.rb +7 -2
  66. data/lib/cure/generator/hex_generator.rb +6 -1
  67. data/lib/cure/generator/imports.rb +4 -0
  68. data/lib/cure/generator/number_generator.rb +6 -1
  69. data/lib/cure/generator/placeholder_generator.rb +7 -1
  70. data/lib/cure/generator/proc_generator.rb +21 -0
  71. data/lib/cure/generator/redact_generator.rb +9 -3
  72. data/lib/cure/generator/static_generator.rb +21 -0
  73. data/lib/cure/generator/variable_generator.rb +11 -5
  74. data/lib/cure/helpers/file_helpers.rb +12 -2
  75. data/lib/cure/helpers/object_helpers.rb +5 -17
  76. data/lib/cure/helpers/perf_helpers.rb +30 -0
  77. data/lib/cure/helpers/string.rb +54 -0
  78. data/lib/cure/launcher.rb +125 -0
  79. data/lib/cure/log.rb +7 -0
  80. data/lib/cure/planner.rb +136 -0
  81. data/lib/cure/strategy/append_strategy.rb +4 -0
  82. data/lib/cure/strategy/base_strategy.rb +19 -44
  83. data/lib/cure/strategy/contain_strategy.rb +51 -0
  84. data/lib/cure/strategy/end_with_strategy.rb +7 -1
  85. data/lib/cure/strategy/full_strategy.rb +4 -0
  86. data/lib/cure/strategy/history/history_cache.rb +82 -0
  87. data/lib/cure/strategy/imports.rb +2 -0
  88. data/lib/cure/strategy/match_strategy.rb +7 -2
  89. data/lib/cure/strategy/prepend_strategy.rb +28 -0
  90. data/lib/cure/strategy/regex_strategy.rb +7 -1
  91. data/lib/cure/strategy/split_strategy.rb +8 -3
  92. data/lib/cure/strategy/start_with_strategy.rb +7 -1
  93. data/lib/cure/transformation/candidate.rb +32 -35
  94. data/lib/cure/transformation/transform.rb +22 -56
  95. data/lib/cure/validator/base_rule.rb +78 -0
  96. data/lib/cure/validator/candidate.rb +54 -0
  97. data/lib/cure/validator/manager.rb +21 -0
  98. data/lib/cure/validators.rb +3 -3
  99. data/lib/cure/version.rb +1 -1
  100. data/lib/cure.rb +19 -11
  101. data/templates/dsl_example.rb +48 -0
  102. data/templates/empty_template.rb +31 -0
  103. metadata +132 -21
  104. data/lib/cure/export/exporter.rb +0 -74
  105. data/lib/cure/extract/builder.rb +0 -27
  106. data/lib/cure/main.rb +0 -72
  107. data/lib/cure/template/dispatch.rb +0 -30
  108. data/lib/cure/template/extraction.rb +0 -38
  109. data/lib/cure/template/template.rb +0 -28
  110. data/lib/cure/template/transformations.rb +0 -26
  111. data/templates/aws_cur_template.json +0 -145
  112. data/templates/example_template.json +0 -54
@@ -33,6 +33,11 @@ module Cure
33
33
  result_arr[@params.index] = generated_value if value?(result_arr[@params.index])
34
34
  result_arr.join(split_token)
35
35
  end
36
+
37
+ def _describe
38
+ "Splitting on '#{@params.token}', at index #{@params.index}) " \
39
+ "[Note: If the value does not include '#{@params.token}', no substitution is made.]"
40
+ end
36
41
  end
37
42
 
38
43
  class SplitStrategyParams < BaseStrategyParams
@@ -42,9 +47,9 @@ module Cure
42
47
  validates :index, validator: :presence
43
48
 
44
49
  def initialize(options=nil)
45
- @token = options["token"]
46
- @index = options["index"]
47
- valid?
50
+ @token = options[:token]
51
+ @index = options[:index]
52
+ # valid?
48
53
 
49
54
  super(options)
50
55
  end
@@ -31,6 +31,12 @@ module Cure
31
31
 
32
32
  @params.match + generated_value
33
33
  end
34
+
35
+ def _describe
36
+ "Start with replacement will look for '#{@params.match}'. " \
37
+ "It will do a #{replace_partial_record ? "partial" : "full"} replacement. " \
38
+ "[Note: If the value does not include '#{@params.match}', no substitution is made.]"
39
+ end
34
40
  end
35
41
 
36
42
  class StartWithStrategyParams < BaseStrategyParams
@@ -39,7 +45,7 @@ module Cure
39
45
  validates :match
40
46
 
41
47
  def initialize(options=nil)
42
- @match = options["match"]
48
+ @match = options[:match]
43
49
  super(options)
44
50
  end
45
51
  end
@@ -3,6 +3,7 @@
3
3
  require "cure/helpers/object_helpers"
4
4
  require "cure/strategy/imports"
5
5
  require "cure/generator/imports"
6
+ require "cure/extract/extractor"
6
7
  require "cure/log"
7
8
 
8
9
  module Cure
@@ -14,11 +15,11 @@ module Cure
14
15
 
15
16
  # Named range that column exists in
16
17
  # @return [String]
17
- attr_accessor :named_range
18
+ attr_reader :named_range
18
19
 
19
20
  # Lookup column name for CSV.
20
21
  # @return [String]
21
- attr_accessor :column
22
+ attr_reader :column
22
23
 
23
24
  # What sort of data needs to be generated.
24
25
  # @return [List<Translation>]
@@ -27,40 +28,46 @@ module Cure
27
28
  # @return [Translation]
28
29
  attr_reader :no_match_translation
29
30
 
30
- def initialize
31
+ attr_reader :ignore_empty
32
+
33
+ def initialize(column, named_range: Cure::Extraction.default_named_range, options: {})
34
+ @column = column
35
+ @named_range = named_range
36
+ @ignore_empty = options.fetch(:ignore_empty, false)
37
+
31
38
  @translations = []
32
- @named_range = "default"
39
+ @no_match_translation = nil
33
40
  end
34
41
 
35
- # @param [String] source_value
36
- # @return [String]
42
+ # @param [String, Nil] source_value
43
+ # @param [RowCtx] row_ctx
44
+ # @return [String,nil]
37
45
  # Transforms the existing value
38
- def perform(source_value)
39
- # log_debug("Performing substitution for [#{@column}] with [#{@translations.length}] translations")
46
+ def perform(source_value, row_ctx)
40
47
  value = source_value
41
48
 
42
49
  @translations.each do |translation|
43
- temp = translation.extract(value)
50
+ temp = translation.extract(value, row_ctx)
44
51
  value = temp if temp
45
52
  end
46
53
 
47
- if value == source_value
48
- log_debug("No translation made for #{value} [#{source_value}]")
49
- value = @no_match_translation&.extract(source_value)
50
- log_debug("Translated to #{value} from [#{source_value}]")
54
+ if value == source_value && @no_match_translation
55
+ log_trace("No translation made for #{value} [#{source_value}]")
56
+ value = @no_match_translation.extract(source_value, row_ctx)
57
+ log_trace("Translated to #{value} from [#{source_value}]")
51
58
  end
52
59
 
53
60
  value
54
61
  end
55
62
 
56
- # @param [Hash] opts
57
- def translations=(opts)
58
- @translations = opts.map { |o| Translation.new.from_hash(o) }
63
+ def with_translations(translations)
64
+ @translations = translations
65
+ self
59
66
  end
60
67
 
61
- # @param [Hash] opts
62
- def no_match_translation=(opts)
63
- @no_match_translation = Translation.new.from_hash(opts)
68
+ def with_no_match_translation(no_match_translation)
69
+ @no_match_translation = no_match_translation
70
+ self
64
71
  end
65
72
  end
66
73
 
@@ -75,26 +82,16 @@ module Cure
75
82
  # @return [Generator::BaseGenerator]
76
83
  attr_reader :generator
77
84
 
78
- # @param [String] source_value
79
- # @return [String]
80
- def extract(source_value)
81
- @strategy.extract(source_value, @generator)
82
- end
83
-
84
- # @param [Hash] opts
85
- def strategy=(opts)
86
- clazz_name = "Cure::Strategy::#{opts["name"].to_s.capitalize}Strategy"
87
- strategy = Kernel.const_get(clazz_name).new(opts["options"] || {})
88
-
85
+ def initialize(strategy, generator)
89
86
  @strategy = strategy
87
+ @generator = generator
90
88
  end
91
89
 
92
- # @param [Hash] opts
93
- def generator=(opts)
94
- clazz_name = "Cure::Generator::#{opts["name"].to_s.capitalize}Generator"
95
- @generator = Kernel.const_get(clazz_name).new(opts["options"] || {})
90
+ # @param [String] source_value
91
+ # @return [String]
92
+ def extract(source_value, row_ctx)
93
+ @strategy.extract(source_value, row_ctx, @generator)
96
94
  end
97
-
98
95
  end
99
96
  end
100
97
  end
@@ -23,74 +23,40 @@ module Cure
23
23
  @candidates = candidates
24
24
  end
25
25
 
26
- # @param [ParsedCSV] parsed_content
27
- # @return [Hash<String,TransformResult>] # make this transformation results?
28
- def transform_content(parsed_content)
29
- parsed_content.content.each_with_object({}) do |section, hash|
30
- ctx = TransformResult.new
31
- section["rows"].each do |row|
32
- ctx.row_count += 1
26
+ # @param [Hash] row
27
+ # @return [Hash]
28
+ def transform(row)
29
+ original_row = row.dup
33
30
 
34
- if ctx.row_count == 1
35
- ctx.extract_column_headers(row)
36
- next
37
- end
31
+ @candidates.each do |candidate|
32
+ column = candidate.column.to_sym
38
33
 
39
- row = transform(section["name"], ctx.column_headers, row)
40
- ctx.add_transformed_row(row)
41
- end
34
+ next unless row.key?(column)
42
35
 
43
- hash[section["name"]] = ctx
44
- end
45
- end
46
-
47
- private
48
-
49
- # @param [Hash] column_headers
50
- # @param [Array] row
51
- # @return [Array]
52
- def transform(named_range, column_headers, row)
53
- candidates_for_named_range(named_range).each do |candidate|
54
- column_idx = column_headers[candidate.column]
55
- next unless column_idx
36
+ existing_value = row[column]
37
+ next if existing_value.nil? && candidate.ignore_empty
56
38
 
57
- existing_value = row[column_idx]
58
- next unless existing_value
59
-
60
- new_value = candidate.perform(existing_value) # transform value
61
- row[column_idx] = new_value
39
+ new_value = candidate.perform(existing_value, RowCtx.new(row, original_row: original_row)) # transform value
40
+ row[column] = new_value
62
41
  end
63
42
 
64
- row
43
+ remove_system_columns(row)
65
44
  end
66
45
 
67
- # @param [String] named_range
68
- # @return [Array<Cure::Transformation::Candidate>]
69
- def candidates_for_named_range(named_range)
70
- @candidates.select { |c| c.named_range == named_range }
46
+ def remove_system_columns(row)
47
+ row.delete(:_id)
48
+ row
71
49
  end
72
50
  end
73
51
 
74
- class TransformResult
75
- include Helpers::FileHelpers
76
-
77
- attr_accessor :row_count,
78
- :transformed_rows,
79
- :column_headers
80
-
81
- def initialize
82
- @row_count = 0
83
- @transformed_rows = []
84
- @column_headers = {}
85
- end
86
-
87
- # @param [Array<String>] row
88
- def extract_column_headers(row)
89
- row.each_with_index { |column, idx| @column_headers[column] = idx }
90
- end
52
+ # This class looks useless, but it isn't. It exists purely to give a hook to add
53
+ # more stuff to a strategy in the future without the method signature changing
54
+ class RowCtx
55
+ attr_accessor :row, :original_row
91
56
 
92
- def add_transformed_row(row)
93
- @transformed_rows << row
57
+ def initialize(row, original_row: nil)
58
+ @row = row
59
+ @original_row = original_row
94
60
  end
95
61
  end
96
62
  end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cure/log"
4
+ require "cure/config"
5
+ require "cure/database"
6
+ require "cure/helpers/file_helpers"
7
+ require "cure/extract/extractor"
8
+
9
+ require "rcsv"
10
+
11
+ module Cure
12
+ module Validator
13
+ class BaseRule
14
+
15
+ def initialize(named_range, column, options)
16
+ @named_range = named_range
17
+ @column = column
18
+ @options = options
19
+ end
20
+
21
+ def process(_value)
22
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
23
+ end
24
+
25
+ def to_s
26
+ "Base Rule"
27
+ end
28
+ end
29
+
30
+ class NotNullRule < BaseRule
31
+ def process(value)
32
+ !value.nil?
33
+ end
34
+
35
+ def to_s
36
+ "Not null"
37
+ end
38
+ end
39
+
40
+ class LengthRule < BaseRule
41
+ def process(value)
42
+ return true if value.nil?
43
+ return true unless value.respond_to? :size
44
+
45
+ length = value.size
46
+ length >= min && length <= max
47
+ end
48
+
49
+ def to_s
50
+ "Length [Min: #{min}, Max: #{max}]"
51
+ end
52
+
53
+ def min
54
+ @min || @options.fetch(:max, 0)
55
+ end
56
+
57
+ def max
58
+ @max || @options.fetch(:max, 99_999)
59
+ end
60
+ end
61
+
62
+ class CustomRule < BaseRule
63
+ def process(value)
64
+ return true if value.nil?
65
+
66
+ custom_proc.call(value)
67
+ end
68
+
69
+ def to_s
70
+ "Custom"
71
+ end
72
+
73
+ def custom_proc
74
+ @options.fetch(:proc, proc { |_x| false })
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cure/helpers/object_helpers"
4
+ require "cure/validator/base_rule"
5
+ require "cure/log"
6
+
7
+ module Cure
8
+ module Validator
9
+ class Candidate
10
+ include Helpers::ObjectHelpers
11
+ include Log
12
+
13
+ # Named range that column exists in
14
+ # @return [String]
15
+ attr_reader :named_range
16
+
17
+ # Lookup column name for CSV.
18
+ # @return [String]
19
+ attr_reader :column
20
+
21
+ # # What sort of data needs to be generated.
22
+ # # @return [Array<Cure::Validator::BaseRule>]
23
+ attr_reader :rules
24
+
25
+ DEFAULT_OPTIONS = {
26
+ fail_on_error: false
27
+ }.freeze
28
+
29
+ def initialize(column, named_range, options={})
30
+ @column = column
31
+ @named_range = named_range || "_default"
32
+ @options = DEFAULT_OPTIONS.merge(options)
33
+ @rules = []
34
+ end
35
+
36
+ def perform(value)
37
+ result = @rules.filter_map do |rule|
38
+ rule.process(value) ? nil : "#{rule} failed -> [#{@column}][#{value}]"
39
+ end
40
+
41
+ raise "Validation failed:\n#{result.join("\n")}" if @options[:fail_on_error] && !result.empty?
42
+
43
+ result
44
+ end
45
+
46
+ def with_rule(method_name, options={})
47
+ klass_name = "Cure::Validator::#{method_name.to_s.split("_").map(&:capitalize).join}Rule"
48
+ raise "#{method_name} is not valid" unless class_exists?(klass_name)
49
+
50
+ @rules << Kernel.const_get(klass_name).new(@named_range, @column, options)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cure
4
+ module Export
5
+ class Manager
6
+
7
+ # @param [Array<Cure::Validator::BaseRule>] candidates
8
+ attr_reader :validators
9
+
10
+ def initialize(named_range, validators)
11
+ @named_range = named_range
12
+ @validators = validators
13
+ end
14
+
15
+ # @param [Hash] row
16
+ def process_row(row)
17
+
18
+ end
19
+ end
20
+ end
21
+ end
@@ -12,7 +12,7 @@ module Cure
12
12
  # @param [Object] options
13
13
  def register_validator(caller, prop, options)
14
14
  @validators[caller] = [] unless @validators.has_key? caller
15
- @validators[caller] << {prop: "@#{prop}".to_sym, options: options}
15
+ @validators[caller] << {prop: "@#{prop}".to_sym, options:}
16
16
  end
17
17
 
18
18
  # @return [TrueClass, FalseClass]
@@ -28,8 +28,8 @@ module Cure
28
28
  proc = case validator_prop
29
29
  when Symbol
30
30
  common_validators.fetch(validator_prop, proc { |_x| false })
31
- when Proc
32
- validator_prop
31
+ # when Proc
32
+ # validator_prop
33
33
  else
34
34
  proc { |_x| false }
35
35
  end
data/lib/cure/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cure
4
- VERSION = "0.1.2"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/cure.rb CHANGED
@@ -1,31 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "logger"
3
4
  require "cure/log"
4
- require "cure/main"
5
+ require "cure/launcher"
5
6
  require "cure/config"
6
7
  require "cure/version"
7
- require "cure/helpers/file_helpers"
8
-
9
- require "cure/template/template"
8
+ require "cure/dsl/template"
9
+ require "cure/strategy/imports"
10
+ require "cure/generator/imports"
10
11
  require "cure/transformation/transform"
11
-
12
- require "logger"
12
+ require "cure/helpers/file_helpers"
13
13
 
14
14
  module Cure
15
15
  class << self
16
16
  attr_writer :logger
17
17
 
18
+ attr_reader :config
19
+
18
20
  def logger
19
21
  @logger ||= Logger.new($stdout).tap do |log|
20
22
  log.progname = name
23
+ log.formatter = proc do |severity, _datetime, _progname, msg|
24
+ "[#{severity}] #{msg}\n"
25
+ end
21
26
  end
22
27
  end
23
28
 
24
- # @param [String] csv_file_location
25
- # @param [Hash] template
26
- # @return [File] output_file
27
- def process(template, csv_file_location, output_dir)
28
- # Main.init_from_hash(template, csv_file_location, output_dir)
29
+ def init(&block)
30
+ launcher = Cure::Launcher.new
31
+ launcher.with_config(&block)
32
+ end
33
+
34
+ def init_from_file(file_path)
35
+ launcher = Cure::Launcher.new
36
+ launcher.with_config_file(file_path)
29
37
  end
30
38
  end
31
39
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ sources do
4
+ csv :pathname, Pathname.new("spec/cure/e2e/input/simple_names.csv"), ref_name: "names"
5
+ csv :pathname, Pathname.new("spec/cure/e2e/input/simple_ages.csv"), ref_name: "ages"
6
+ end
7
+
8
+ extract do
9
+ named_range name: "section_1", at: "B2:G6"
10
+ named_range name: "section_2", at: "B18:G20", headers: "B2:G2"
11
+
12
+ variable name: "new_field", location: "A16"
13
+ variable name: "new_field_2", location: "B16"
14
+ end
15
+
16
+ query do
17
+ with named_range: "section_1", query: <<-SQL
18
+ SELECT * FROM section_1
19
+ SQL
20
+
21
+ with named_range: "section_2", query: <<-SQL
22
+ SELECT * FROM section_2
23
+ SQL
24
+ end
25
+
26
+ build do
27
+ candidate column: "new_column", named_range: "section_1" do
28
+ add options: {}
29
+ end
30
+ end
31
+
32
+ transform do
33
+ candidate column: "new_column", named_range: "section_1" do
34
+ with_translation { replace("full").with("variable", name: "new_field") }
35
+ end
36
+
37
+ candidate column: "new_column", named_range: "section_2" do
38
+ with_translation { replace("full").with("variable", name: "new_field") }
39
+ end
40
+
41
+ placeholders({key: "value", key2: "value2"})
42
+ end
43
+
44
+ export do
45
+ terminal named_range: "section_1", title: "Exported", limit_rows: 5
46
+ csv named_range: "section_1", file: "/tmp/cure/section_1.csv"
47
+ csv named_range: "section_2", file: "/tmp/cure/section_2.csv"
48
+ end
@@ -0,0 +1,31 @@
1
+ metadata do
2
+ # ...
3
+ end
4
+
5
+ sources do
6
+ # ...
7
+ end
8
+
9
+ extract do
10
+ # ...
11
+ end
12
+
13
+ validate do
14
+ # ...
15
+ end
16
+
17
+ build do
18
+ # ...
19
+ end
20
+
21
+ query do
22
+ # ...
23
+ end
24
+
25
+ transform do
26
+ # ...
27
+ end
28
+
29
+ export do
30
+ # ...
31
+ end