diff --git a/go/internal/lint/lint_test.go b/go/internal/lint/lint_test.go index 8b7219b..9862e9d 100644 --- a/go/internal/lint/lint_test.go +++ b/go/internal/lint/lint_test.go @@ -148,6 +148,93 @@ unknown_prop: true } } +func TestLintFile_AcceptsSodaRunner(t *testing.T) { + dir := t.TempDir() + f := filepath.Join(dir, "contract.yml") + os.WriteFile(f, []byte(` +dataset: my_ds/db/schema/orders +columns: + - name: id +soda_runner: + checks_schedule: + cron: "0 0 * * *" + timezone: UTC +`), 0644) + + result, err := LintFile(f) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !result.Valid { + for _, e := range result.Errors { + t.Logf(" %s: %s", e.Path, e.Message) + } + t.Fatalf("expected valid contract with soda_runner, got %d errors", len(result.Errors)) + } +} + +func TestLintFile_AcceptsSodaAgentLegacyAlias(t *testing.T) { + dir := t.TempDir() + f := filepath.Join(dir, "contract.yml") + os.WriteFile(f, []byte(` +dataset: my_ds/db/schema/orders +columns: + - name: id +soda_agent: + checks_schedule: + cron: "0 0 * * *" + timezone: UTC +`), 0644) + + result, err := LintFile(f) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !result.Valid { + for _, e := range result.Errors { + t.Logf(" %s: %s", e.Path, e.Message) + } + t.Fatalf("expected valid contract with deprecated soda_agent, got %d errors", len(result.Errors)) + } +} + +func TestLintFile_RejectsBothSodaRunnerAndSodaAgent(t *testing.T) { + dir := t.TempDir() + f := filepath.Join(dir, "contract.yml") + os.WriteFile(f, []byte(` +dataset: my_ds/db/schema/orders +columns: + - name: id +soda_runner: + checks_schedule: + cron: "0 0 * * *" +soda_agent: + checks_schedule: + cron: "0 0 * * *" +`), 0644) + + result, err := LintFile(f) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result.Valid { + t.Fatal("expected invalid when both soda_runner and soda_agent are set") + } + if len(result.Errors) == 0 { + t.Fatal("expected at least one validation error") + } + rootError := false + for _, e := range result.Errors { + t.Logf(" %s: %s", e.Path, e.Message) + if e.Path == "$" { + rootError = true + } + } + if !rootError { + t.Fatal("expected a validation error at the root path '$' for the not-both constraint") + } +} + func TestSegmentsToPath(t *testing.T) { tests := []struct { in []string diff --git a/go/internal/lint/schema/soda_data_contract_json_schema_1_0_0.json b/go/internal/lint/schema/soda_data_contract_json_schema_1_0_0.json index 89a360c..d78d3bf 100644 --- a/go/internal/lint/schema/soda_data_contract_json_schema_1_0_0.json +++ b/go/internal/lint/schema/soda_data_contract_json_schema_1_0_0.json @@ -38,7 +38,7 @@ } }, "soda_agent": { - "description": "The agent configuration to be used within the contract file", + "description": "DEPRECATED: use 'soda_runner' instead. The runner configuration to be used within the contract file. Kept for backwards compatibility.", "type": "object", "properties": { "checks_schedule": { @@ -59,8 +59,36 @@ "type": [ "string", "number" - ], - "additionalProperties": false + ] + } + } + } + } + } + }, + "soda_runner": { + "description": "The runner configuration to be used within the contract file. Replaces 'soda_agent'; both keys are accepted for backwards compatibility but not at the same time.", + "type": "object", + "properties": { + "checks_schedule": { + "type": "object", + "properties": { + "cron": { + "type": "string", + "description": "Cron expression in the format '* * * * *' (minute hour day month weekday). Requires string in quotes." + }, + "timezone": { + "type": "string", + "description": "The timezone to be used within the contract file" + }, + "variables": { + "type": "object", + "description": "The variables to be used within the contract file", + "additionalProperties": { + "type": [ + "string", + "number" + ] } } } @@ -837,6 +865,9 @@ "dataset", "columns" ], + "not": { + "required": ["soda_runner", "soda_agent"] + }, "$defs": { "generic_check_properties": { "description": "NOT USED YET, problem with inheritance and additionalProperties",