strata-cli 0.1.8 → 0.1.10
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -0
- data/README.md +26 -2
- data/lib/strata/cli/agent_mode.rb +26 -0
- data/lib/strata/cli/agent_output.rb +21 -0
- data/lib/strata/cli/ai/services/table_generator.rb +35 -20
- data/lib/strata/cli/api/client.rb +23 -63
- data/lib/strata/cli/api/response_error_handler.rb +115 -0
- data/lib/strata/cli/error_reporter.rb +4 -1
- data/lib/strata/cli/generators/datasource.rb +4 -3
- data/lib/strata/cli/generators/group.rb +37 -0
- data/lib/strata/cli/generators/migration.rb +2 -1
- data/lib/strata/cli/generators/project.rb +18 -11
- data/lib/strata/cli/generators/relation.rb +2 -1
- data/lib/strata/cli/generators/table.rb +5 -8
- data/lib/strata/cli/generators/templates/AGENTS.md +457 -88
- data/lib/strata/cli/generators/templates/table.table_name.yml +8 -3
- data/lib/strata/cli/generators/test.rb +2 -1
- data/lib/strata/cli/guard.rb +4 -1
- data/lib/strata/cli/helpers/command_context.rb +8 -9
- data/lib/strata/cli/helpers/datasource_helper.rb +27 -1
- data/lib/strata/cli/helpers/description_helper.rb +2 -1
- data/lib/strata/cli/main.rb +12 -3
- data/lib/strata/cli/output.rb +103 -0
- data/lib/strata/cli/sub_commands/audit.rb +136 -16
- data/lib/strata/cli/sub_commands/branch.rb +165 -0
- data/lib/strata/cli/sub_commands/create.rb +13 -2
- data/lib/strata/cli/sub_commands/datasource.rb +21 -3
- data/lib/strata/cli/sub_commands/deploy.rb +16 -13
- data/lib/strata/cli/sub_commands/project.rb +6 -3
- data/lib/strata/cli/sub_commands/table.rb +11 -8
- data/lib/strata/cli/terminal.rb +7 -4
- data/lib/strata/cli/ui/field_editor.rb +21 -27
- data/lib/strata/cli/utils/deployment_monitor.rb +15 -34
- data/lib/strata/cli/utils/git.rb +78 -0
- data/lib/strata/cli/utils/import_manager.rb +4 -1
- data/lib/strata/cli/utils/test_reporter.rb +4 -32
- data/lib/strata/cli/utils/version_checker.rb +4 -8
- data/lib/strata/cli/utils.rb +3 -1
- data/lib/strata/cli/version.rb +1 -1
- data/lib/strata/cli.rb +4 -3
- metadata +6 -2
- data/lib/strata/cli/helpers/color_helper.rb +0 -103
data/lib/strata/cli/guard.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "utils"
|
|
4
|
+
require_relative "output"
|
|
5
|
+
|
|
4
6
|
module Strata
|
|
5
7
|
module CLI
|
|
6
8
|
module Guard
|
|
@@ -10,13 +12,14 @@ module Strata
|
|
|
10
12
|
adapters
|
|
11
13
|
version
|
|
12
14
|
deploy
|
|
15
|
+
branch
|
|
13
16
|
].freeze
|
|
14
17
|
|
|
15
18
|
def invoke_command(command, *args)
|
|
16
19
|
Utils.exit_error_if_not_strata! unless ALLOWED_COMMANDS.include?(command.name)
|
|
17
20
|
super
|
|
18
21
|
rescue Strata::CommandError => e
|
|
19
|
-
|
|
22
|
+
Output.print_error("ERROR: #{e.message}", context: self)
|
|
20
23
|
exit 1
|
|
21
24
|
end
|
|
22
25
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "tty-prompt"
|
|
4
4
|
require_relative "datasource_helper"
|
|
5
|
-
require_relative "
|
|
5
|
+
require_relative "../output"
|
|
6
6
|
require_relative "prompts"
|
|
7
7
|
require_relative "../error_reporter"
|
|
8
8
|
|
|
@@ -10,6 +10,7 @@ module Strata
|
|
|
10
10
|
module CLI
|
|
11
11
|
module Helpers
|
|
12
12
|
module CommandContext
|
|
13
|
+
include Output
|
|
13
14
|
include DatasourceHelper
|
|
14
15
|
|
|
15
16
|
def adapter
|
|
@@ -28,8 +29,7 @@ module Strata
|
|
|
28
29
|
@table_fetch_result ||= begin
|
|
29
30
|
tables = with_spinner("Fetching tables from #{datasource_key}...") { adapter.tables }
|
|
30
31
|
if tables.empty?
|
|
31
|
-
|
|
32
|
-
say Prompts::MSG_NO_TABLES_FOUND % datasource_key, ColorHelper.warning
|
|
32
|
+
say(Output.format(:warning, Prompts::MSG_NO_TABLES_FOUND % datasource_key), nil) if respond_to?(:say)
|
|
33
33
|
{tables: [], failed: false}
|
|
34
34
|
else
|
|
35
35
|
{tables: tables, failed: false}
|
|
@@ -38,15 +38,14 @@ module Strata
|
|
|
38
38
|
ErrorReporter.log_error(e, context: "create table: failed fetching tables for '#{datasource_key}'")
|
|
39
39
|
|
|
40
40
|
if ErrorReporter.connection_error?(e)
|
|
41
|
-
say "Could not connect to datasource '#{datasource_key}'.",
|
|
41
|
+
say(Output.format(:warning, "Could not connect to datasource '#{datasource_key}'."), nil) if respond_to?(:say)
|
|
42
42
|
else
|
|
43
|
-
say "Could not fetch tables from datasource '#{datasource_key}'.",
|
|
44
|
-
say "Reason: #{ErrorReporter.user_message_for(e)}",
|
|
43
|
+
say(Output.format(:warning, "Could not fetch tables from datasource '#{datasource_key}'."), nil) if respond_to?(:say)
|
|
44
|
+
say(Output.format(:warning, "Reason: #{ErrorReporter.user_message_for(e)}"), nil) if respond_to?(:say)
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
say "Hint: verify credentials/settings and run 'strata datasource test #{datasource_key}'.",
|
|
48
|
-
|
|
49
|
-
say "Details logged to '#{ErrorReporter.log_relative_path}'.", ColorHelper.info
|
|
47
|
+
say(Output.format(:dim, "Hint: verify credentials/settings and run 'strata datasource test #{datasource_key}'."), nil) if respond_to?(:say)
|
|
48
|
+
say(Output.format(:info, "Details logged to '#{ErrorReporter.log_relative_path}'."), nil) if respond_to?(:say)
|
|
50
49
|
{tables: [], failed: true}
|
|
51
50
|
end
|
|
52
51
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../agent_output"
|
|
4
|
+
|
|
3
5
|
module Strata
|
|
4
6
|
module CLI
|
|
5
7
|
module DatasourceHelper
|
|
@@ -8,7 +10,7 @@ module Strata
|
|
|
8
10
|
redshift: %w[pg],
|
|
9
11
|
mysql: %w[mysql2],
|
|
10
12
|
sqlserver: %w[tiny_tds],
|
|
11
|
-
athena: %w[aws-sdk-athena aws-sdk-s3],
|
|
13
|
+
athena: %w[aws-sdk-athena aws-sdk-s3 rexml],
|
|
12
14
|
trino: %w[trino-client],
|
|
13
15
|
sqlite: %w[sqlite3],
|
|
14
16
|
duckdb: %w[duckdb]
|
|
@@ -24,6 +26,19 @@ module Strata
|
|
|
24
26
|
# 3. Check available datasources
|
|
25
27
|
ds_keys = datasources.keys
|
|
26
28
|
|
|
29
|
+
if agent_mode?
|
|
30
|
+
if ds_keys.empty?
|
|
31
|
+
agent_emit_no_datasources!
|
|
32
|
+
elsif ds_keys.length == 1
|
|
33
|
+
return ds_keys.first
|
|
34
|
+
else
|
|
35
|
+
AgentOutput.emit_error(
|
|
36
|
+
"Datasource key required. Available: #{ds_keys.join(", ")}",
|
|
37
|
+
code: "datasource_key_required"
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
27
42
|
if ds_keys.empty?
|
|
28
43
|
say "No datasources configured. Run 'strata datasource add' first.", :red
|
|
29
44
|
nil
|
|
@@ -134,6 +149,17 @@ module Strata
|
|
|
134
149
|
|
|
135
150
|
private
|
|
136
151
|
|
|
152
|
+
def agent_emit_no_datasources!
|
|
153
|
+
AgentOutput.emit_error(
|
|
154
|
+
"No datasources configured in datasources.yml. Ask the user to add one interactively — " \
|
|
155
|
+
"do not create datasources or write credentials yourself (secrets belong in .strata via CLI prompts). " \
|
|
156
|
+
"User command: strata datasource add [ADAPTER] (e.g. strata datasource add postgres, strata datasource add duckdb).",
|
|
157
|
+
code: "no_datasources",
|
|
158
|
+
user_action_required: true,
|
|
159
|
+
suggested_command: "strata datasource add [ADAPTER]"
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
|
|
137
163
|
def validate_datasource(ds_key)
|
|
138
164
|
unless datasources[ds_key]
|
|
139
165
|
say "Error: Datasource '#{ds_key}' not found in datasources.yml", :red
|
|
@@ -11,7 +11,8 @@ module Strata
|
|
|
11
11
|
if File.exist?(file_path)
|
|
12
12
|
long_desc File.read(file_path)
|
|
13
13
|
else
|
|
14
|
-
|
|
14
|
+
require_relative "../output"
|
|
15
|
+
Output.print_warning("Warning: Description file not found at #{file_path}")
|
|
15
16
|
end
|
|
16
17
|
end
|
|
17
18
|
end
|
data/lib/strata/cli/main.rb
CHANGED
|
@@ -3,16 +3,21 @@
|
|
|
3
3
|
require_relative "generators/project"
|
|
4
4
|
require_relative "sub_commands/datasource"
|
|
5
5
|
require_relative "sub_commands/deploy"
|
|
6
|
+
require_relative "sub_commands/branch"
|
|
6
7
|
require_relative "sub_commands/project"
|
|
7
8
|
require_relative "sub_commands/table"
|
|
8
9
|
require_relative "sub_commands/create"
|
|
9
10
|
require_relative "sub_commands/audit"
|
|
10
11
|
require_relative "helpers/description_helper"
|
|
12
|
+
require_relative "agent_mode"
|
|
13
|
+
require_relative "output"
|
|
11
14
|
|
|
12
15
|
module Strata
|
|
13
16
|
module CLI
|
|
14
17
|
class Main < Thor
|
|
15
18
|
include Guard
|
|
19
|
+
include Output
|
|
20
|
+
include AgentMode
|
|
16
21
|
extend Helpers::DescriptionHelper
|
|
17
22
|
|
|
18
23
|
def self.exit_on_failure?
|
|
@@ -30,21 +35,22 @@ module Strata
|
|
|
30
35
|
repeatable: true
|
|
31
36
|
option :source, aliases: ["s"], type: :string, desc: "URL of existing project"
|
|
32
37
|
option :api_key, aliases: ["a"], type: :string, desc: "Api Key. Required if initializing existing project."
|
|
38
|
+
option :verbose, aliases: ["v"], type: :boolean, default: false,
|
|
39
|
+
desc: "Show detailed init output (file-by-file actions)."
|
|
33
40
|
|
|
34
41
|
def init(project_name = nil)
|
|
35
42
|
unless project_name || options[:source]
|
|
36
43
|
raise Strata::CommandError, "PROJECT_NAME is required when not using --source option."
|
|
37
44
|
end
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
ColorHelper.info
|
|
46
|
+
print_status(:created, (project_name || "project from source").to_s, type: :info)
|
|
41
47
|
invoke Generators::Project, [project_name], options
|
|
42
48
|
end
|
|
43
49
|
|
|
44
50
|
desc "adapters", "Lists supported data warehouse adapters"
|
|
45
51
|
def adapters
|
|
46
52
|
out = " SUPPORTED ADAPTERS: \n\t#{DWH.adapters.keys.join("\n\t")}"
|
|
47
|
-
|
|
53
|
+
print_info(out)
|
|
48
54
|
end
|
|
49
55
|
|
|
50
56
|
desc "datasource", "Manage project datasources"
|
|
@@ -62,6 +68,9 @@ module Strata
|
|
|
62
68
|
desc "deploy", "Deploy project to Strata server"
|
|
63
69
|
subcommand "deploy", SubCommands::Deploy
|
|
64
70
|
|
|
71
|
+
desc "branch", "Manage Strata server branches"
|
|
72
|
+
subcommand "branch", SubCommands::Branch
|
|
73
|
+
|
|
65
74
|
desc "project", "Manage project configuration"
|
|
66
75
|
subcommand "project", SubCommands::Project
|
|
67
76
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require "pastel"
|
|
5
|
+
|
|
6
|
+
module Strata
|
|
7
|
+
module CLI
|
|
8
|
+
module Output
|
|
9
|
+
THEME = {
|
|
10
|
+
success: :green,
|
|
11
|
+
error: %i[red bold],
|
|
12
|
+
warning: :yellow,
|
|
13
|
+
info: :cyan,
|
|
14
|
+
title: %i[cyan bold],
|
|
15
|
+
highlight: %i[cyan bold],
|
|
16
|
+
dim: :bright_black,
|
|
17
|
+
primary: :blue,
|
|
18
|
+
secondary: :magenta,
|
|
19
|
+
border: %i[cyan dim],
|
|
20
|
+
selected: %i[green bold],
|
|
21
|
+
disabled: :bright_black
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
def pastel
|
|
26
|
+
@pastel ||= Pastel.new(enabled: $stdout.tty?)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def shell_for(context = nil)
|
|
30
|
+
return context.shell if context&.respond_to?(:shell)
|
|
31
|
+
return context if context.is_a?(Thor::Shell)
|
|
32
|
+
|
|
33
|
+
Thor::Shell::Color.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def print_info(message, context: nil)
|
|
37
|
+
shell_for(context).say(format(:info, message))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def print_success(message, context: nil)
|
|
41
|
+
shell_for(context).say(format(:success, message))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def print_warning(message, context: nil)
|
|
45
|
+
shell_for(context).say(format(:warning, message))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def print_error(message, context: nil)
|
|
49
|
+
shell_for(context).say_error(format(:error, message))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def print_hint(message, context: nil, stderr: false)
|
|
53
|
+
shell = shell_for(context)
|
|
54
|
+
formatted = format(:dim, message)
|
|
55
|
+
stderr ? shell.say_error(formatted) : shell.say(formatted)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Standard status output with Thor's prefix formatting.
|
|
59
|
+
# We keep the label color via Thor, but decorate the message via Output theme.
|
|
60
|
+
def print_status(label, message, type: :info, context: nil)
|
|
61
|
+
shell = shell_for(context)
|
|
62
|
+
shell.say_status(label, format(type, message), thor_color(type))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def format(type, message)
|
|
66
|
+
return "" if message.nil?
|
|
67
|
+
|
|
68
|
+
styles = THEME[type]
|
|
69
|
+
return message.to_s unless styles
|
|
70
|
+
|
|
71
|
+
if styles.is_a?(Array)
|
|
72
|
+
pastel.decorate(message.to_s, *styles)
|
|
73
|
+
else
|
|
74
|
+
pastel.send(styles, message.to_s)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Thor only understands a limited palette; keep it simple/stable.
|
|
79
|
+
def thor_color(type)
|
|
80
|
+
case type
|
|
81
|
+
when :success then :green
|
|
82
|
+
when :error then :red
|
|
83
|
+
when :warning then :yellow
|
|
84
|
+
when :info, :title, :border then :cyan
|
|
85
|
+
when :primary then :blue
|
|
86
|
+
when :secondary then :magenta
|
|
87
|
+
when :dim then :white
|
|
88
|
+
else
|
|
89
|
+
:white
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Convenience instance methods for Thor classes.
|
|
95
|
+
def print_info(message) = Output.print_info(message, context: self)
|
|
96
|
+
def print_success(message) = Output.print_success(message, context: self)
|
|
97
|
+
def print_warning(message) = Output.print_warning(message, context: self)
|
|
98
|
+
def print_error(message) = Output.print_error(message, context: self)
|
|
99
|
+
def print_hint(message, stderr: false) = Output.print_hint(message, context: self, stderr: stderr)
|
|
100
|
+
def print_status(label, message, type: :info) = Output.print_status(label, message, type: type, context: self)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
require_relative "../guard"
|
|
4
4
|
require_relative "../terminal"
|
|
5
5
|
require_relative "../credentials"
|
|
6
|
-
require_relative "../
|
|
6
|
+
require_relative "../output"
|
|
7
7
|
require_relative "../helpers/datasource_helper"
|
|
8
|
+
require_relative "../agent_mode"
|
|
8
9
|
require_relative "../utils/yaml_import_resolver"
|
|
9
10
|
require_relative "../utils/import_manager"
|
|
10
11
|
require "yaml"
|
|
@@ -17,12 +18,22 @@ module Strata
|
|
|
17
18
|
include Thor::Actions
|
|
18
19
|
include Guard
|
|
19
20
|
include Terminal
|
|
21
|
+
include Output
|
|
20
22
|
include DatasourceHelper
|
|
23
|
+
include AgentMode
|
|
21
24
|
|
|
22
25
|
REQUIRED_KEYS_FOR_TABLE_MODEL = %w[name physical_name fields datasource].freeze
|
|
23
26
|
REQUIRED_KEYS_FOR_RELATIONSHIP_MODEL = ["datasource"].freeze
|
|
24
27
|
REQUIRED_KEYS_FOR_RELATIONSHIP_DEFINITION = %w[left right sql cardinality].freeze
|
|
25
28
|
RELATIONSHIP_CARDINALITIES = %w[one_to_one one_to_many many_to_one many_to_many].freeze
|
|
29
|
+
VALID_FORMAT_TYPES = %w[raw number currency percent date datetime html javascript].freeze
|
|
30
|
+
ALLOWED_FIELD_KEYS = %w[
|
|
31
|
+
type name description hidden grains data_type display_type format
|
|
32
|
+
secure disable_listing value_list_size snapshot exclusion_type
|
|
33
|
+
exclusions inclusions extended_blend_group synonyms expression
|
|
34
|
+
].freeze
|
|
35
|
+
ALLOWED_EXPRESSION_KEYS = %w[sql array lookup primary_key].freeze
|
|
36
|
+
EXPRESSION_BOOLEAN_KEYS = %w[array lookup primary_key].freeze
|
|
26
37
|
|
|
27
38
|
# Set default command so `strata audit` still works as `strata audit all`
|
|
28
39
|
default_command :all
|
|
@@ -61,7 +72,7 @@ module Strata
|
|
|
61
72
|
if results.values.all?
|
|
62
73
|
# All checks passed - no need for additional message, spinners already show success
|
|
63
74
|
else
|
|
64
|
-
|
|
75
|
+
print_error("\n Some checks failed.")
|
|
65
76
|
exit(1)
|
|
66
77
|
end
|
|
67
78
|
end
|
|
@@ -76,7 +87,7 @@ module Strata
|
|
|
76
87
|
rescue
|
|
77
88
|
failures.each do |f|
|
|
78
89
|
msg = f.is_a?(Hash) ? "#{f[:file]}: #{f[:message]}" : f.to_s
|
|
79
|
-
|
|
90
|
+
print_error(" ✖ #{msg}")
|
|
80
91
|
end
|
|
81
92
|
false
|
|
82
93
|
end
|
|
@@ -98,8 +109,8 @@ module Strata
|
|
|
98
109
|
def audit_models
|
|
99
110
|
failures = []
|
|
100
111
|
Dir.glob("models/**/*.yml").each do |file|
|
|
101
|
-
audit_model_file(file, failures)
|
|
102
112
|
audit_imports(file, failures)
|
|
113
|
+
audit_model_file(file, failures)
|
|
103
114
|
end
|
|
104
115
|
failures
|
|
105
116
|
end
|
|
@@ -108,17 +119,21 @@ module Strata
|
|
|
108
119
|
content = YAML.safe_load_file(file, permitted_classes: [Date, Time], aliases: true) || {}
|
|
109
120
|
return unless content.is_a?(Hash)
|
|
110
121
|
|
|
111
|
-
validate_structure(content, file, failures)
|
|
112
|
-
|
|
113
122
|
case model_type(file)
|
|
114
123
|
when :table
|
|
115
|
-
|
|
124
|
+
resolved = Utils::YamlImportResolver.resolve(file, Dir.pwd)
|
|
125
|
+
return unless resolved.is_a?(Hash)
|
|
126
|
+
|
|
127
|
+
validate_structure(resolved, file, failures)
|
|
128
|
+
validate_table_model(resolved, file, failures)
|
|
116
129
|
when :relationship
|
|
130
|
+
validate_structure(content, file, failures)
|
|
117
131
|
validate_relationship_model(content, file, failures)
|
|
118
132
|
end
|
|
119
|
-
rescue
|
|
120
|
-
|
|
121
|
-
|
|
133
|
+
rescue Strata::ImportError => e
|
|
134
|
+
failures << {file: file, message: e.message}
|
|
135
|
+
rescue Psych::SyntaxError
|
|
136
|
+
# Handled by audit_yaml_syntax when run
|
|
122
137
|
end
|
|
123
138
|
|
|
124
139
|
def model_type(file)
|
|
@@ -139,7 +154,7 @@ module Strata
|
|
|
139
154
|
def validate_table_model(content, file, failures)
|
|
140
155
|
validate_required_keys(content, file, REQUIRED_KEYS_FOR_TABLE_MODEL, failures)
|
|
141
156
|
validate_datasource_reference(content, file, failures)
|
|
142
|
-
validate_table_fields(content["fields"], file, failures)
|
|
157
|
+
validate_table_fields(content["fields"], file, failures)
|
|
143
158
|
end
|
|
144
159
|
|
|
145
160
|
def validate_relationship_model(content, file, failures)
|
|
@@ -171,10 +186,119 @@ module Strata
|
|
|
171
186
|
return
|
|
172
187
|
end
|
|
173
188
|
|
|
189
|
+
if fields.empty?
|
|
190
|
+
failures << {file: file, message: "'fields' must not be empty"}
|
|
191
|
+
return
|
|
192
|
+
end
|
|
193
|
+
|
|
174
194
|
fields.each_with_index do |field, idx|
|
|
175
195
|
unless field.is_a?(Hash) && field.key?("name")
|
|
176
196
|
failures << {file: file, message: "Field at index #{idx} missing 'name'"}
|
|
197
|
+
next
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
validate_field_definition(field, file, idx, failures)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def validate_field_definition(field, file, idx, failures)
|
|
205
|
+
field_name = field["name"] || "index #{idx}"
|
|
206
|
+
|
|
207
|
+
unknown_keys = field.keys.map(&:to_s) - ALLOWED_FIELD_KEYS
|
|
208
|
+
if unknown_keys.any?
|
|
209
|
+
failures << {
|
|
210
|
+
file: file,
|
|
211
|
+
message: "Field '#{field_name}' has unknown #{(unknown_keys.size == 1) ? "key" : "keys"}: #{unknown_keys.sort.join(", ")}"
|
|
212
|
+
}
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
validate_field_expression(field_name, field["expression"], file, failures)
|
|
216
|
+
|
|
217
|
+
return unless field.key?("format")
|
|
218
|
+
|
|
219
|
+
validate_field_format(field_name, field["format"], file, failures)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def validate_field_expression(field_name, expression, file, failures)
|
|
223
|
+
if expression.nil?
|
|
224
|
+
failures << {file: file, message: "Field '#{field_name}' missing required 'expression'"}
|
|
225
|
+
return
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
if expression.is_a?(String)
|
|
229
|
+
if expression.strip.empty?
|
|
230
|
+
failures << {file: file, message: "Field '#{field_name}' expression must not be empty"}
|
|
231
|
+
end
|
|
232
|
+
return
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
unless expression.is_a?(Hash)
|
|
236
|
+
type_name = expression.class.name
|
|
237
|
+
failures << {
|
|
238
|
+
file: file,
|
|
239
|
+
message: "Field '#{field_name}' expression must be a string shortcut or a mapping (got #{type_name})"
|
|
240
|
+
}
|
|
241
|
+
return
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
unknown_keys = expression.keys.map(&:to_s) - ALLOWED_EXPRESSION_KEYS
|
|
245
|
+
if unknown_keys.any?
|
|
246
|
+
failures << {
|
|
247
|
+
file: file,
|
|
248
|
+
message: "Field '#{field_name}' expression has unknown #{(unknown_keys.size == 1) ? "key" : "keys"}: " \
|
|
249
|
+
"#{unknown_keys.sort.join(", ")}"
|
|
250
|
+
}
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
sql = expression["sql"] || expression[:sql]
|
|
254
|
+
if sql.nil? || sql.to_s.strip.empty?
|
|
255
|
+
failures << {file: file, message: "Field '#{field_name}' expression must include non-empty 'sql'"}
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
EXPRESSION_BOOLEAN_KEYS.each do |key|
|
|
259
|
+
next unless expression.key?(key) || expression.key?(key.to_sym)
|
|
260
|
+
|
|
261
|
+
value = expression[key] || expression[key.to_sym]
|
|
262
|
+
next if value == true || value == false
|
|
263
|
+
|
|
264
|
+
failures << {
|
|
265
|
+
file: file,
|
|
266
|
+
message: "Field '#{field_name}' expression.#{key} must be true or false"
|
|
267
|
+
}
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def validate_field_format(field_name, format_value, file, failures)
|
|
272
|
+
case format_value
|
|
273
|
+
when String
|
|
274
|
+
shortcut = format_value.strip
|
|
275
|
+
if shortcut.empty?
|
|
276
|
+
failures << {file: file, message: "Field '#{field_name}' format must not be empty"}
|
|
277
|
+
return
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
type = shortcut.split(":", 2).first
|
|
281
|
+
unless VALID_FORMAT_TYPES.include?(type)
|
|
282
|
+
failures << {
|
|
283
|
+
file: file,
|
|
284
|
+
message: "Field '#{field_name}' format shortcut has invalid type '#{type}' (expected one of: #{VALID_FORMAT_TYPES.join(", ")})"
|
|
285
|
+
}
|
|
286
|
+
end
|
|
287
|
+
when Hash
|
|
288
|
+
type = format_value["type"] || format_value[:type]
|
|
289
|
+
if type.nil? || type.to_s.strip.empty?
|
|
290
|
+
failures << {file: file, message: "Field '#{field_name}' format hash must include type"}
|
|
291
|
+
elsif !VALID_FORMAT_TYPES.include?(type.to_s)
|
|
292
|
+
failures << {
|
|
293
|
+
file: file,
|
|
294
|
+
message: "Field '#{field_name}' format hash has invalid type '#{type}' (expected one of: #{VALID_FORMAT_TYPES.join(", ")})"
|
|
295
|
+
}
|
|
177
296
|
end
|
|
297
|
+
else
|
|
298
|
+
failures << {
|
|
299
|
+
file: file,
|
|
300
|
+
message: "Field '#{field_name}' format must be a string shortcut or a hash with type"
|
|
301
|
+
}
|
|
178
302
|
end
|
|
179
303
|
end
|
|
180
304
|
|
|
@@ -253,13 +377,9 @@ module Strata
|
|
|
253
377
|
imported_content = YAML.safe_load_file(resolved_path, permitted_classes: [Date, Time], aliases: true) || {}
|
|
254
378
|
|
|
255
379
|
unless imported_content.is_a?(Hash)
|
|
256
|
-
failures << {file: file, message: "Imported file '#{import_path}' does not contain valid YAML
|
|
380
|
+
failures << {file: file, message: "Imported file '#{import_path}' does not contain valid YAML"}
|
|
257
381
|
next
|
|
258
382
|
end
|
|
259
|
-
|
|
260
|
-
if model_type(file) == :table && imported_content["fields"]
|
|
261
|
-
validate_table_fields(imported_content["fields"], file, failures)
|
|
262
|
-
end
|
|
263
383
|
rescue Strata::InvalidImportPathError => e
|
|
264
384
|
failures << {file: file, message: e.message}
|
|
265
385
|
rescue Strata::MissingImportError
|