provisioning-core/nulib/tools/nickel/migration_helper.nu

278 lines
9.2 KiB
Text

# | Nickel to Nickel Migration Helper
# | Automates pattern detection and application
# | Follows: .claude/kcl_to_nickel_migration_framework.md
# | Author: Migration Framework
# | Date: 2025-12-15
# ============================================================
# Pattern Detection
# ============================================================
# Detect if Nickel file uses schema inheritance pattern
export def "detect-inheritance" [decl_file: path] -> bool {
let content = open $decl_file | into string
($content | str contains "schema ") and ($content | str contains "(")
}
# Detect if Nickel file exports global instances
export def "detect-exports" [decl_file: path] -> list {
let content = open $decl_file | into string
$content
| lines
| filter { |line| ($line | str contains ": ") and not ($line | str contains "schema") }
| filter { |line| ($line | str contains " = ") }
| map { |line| $line | str trim }
}
# Detect if Nickel file only defines schemas (no exports)
export def "is-schema-only" [decl_file: path] -> bool {
let exports = (detect-exports $decl_file)
($exports | length) == 0
}
# Get migration template type for Nickel file
export def "get-template-type" [decl_file: path] -> string {
let has_inheritance = (detect-inheritance $decl_file)
let is_empty_export = (is-schema-only $decl_file)
let exports = (detect-exports $decl_file)
let export_count = ($exports | length)
if $is_empty_export {
"template-1-schema-only"
} else if $has_inheritance {
"template-4-inheritance"
} else if $export_count == 1 {
"template-2-single-instance"
} else if $export_count > 1 {
"template-5-multiple-schemas"
} else {
"template-3-complex-nesting"
}
}
# ============================================================
# Value Conversion
# ============================================================
# Convert Nickel boolean to Nickel
export def "convert-boolean" [value: string] -> string {
match ($value | str trim) {
"True" => "true",
"False" => "false",
"true" => "true",
"false" => "false",
_ => $value,
}
}
# Convert Nickel None to Nickel null
export def "convert-none" [value: string] -> string {
match ($value | str trim) {
"None" => "null",
"null" => "null",
_ => $value,
}
}
# Convert Nickel value to Nickel value
export def "convert-value" [decl_value: string] -> string {
let trimmed = ($decl_value | str trim)
let bool_converted = (convert-boolean $trimmed)
(convert-none $bool_converted)
}
# ============================================================
# JSON Equivalence Validation
# ============================================================
# Export Nickel file to JSON for comparison
export def "nickel-to-json" [decl_file: path] {
if not ($decl_file | path exists) {
error make {msg: $"Nickel file not found: ($decl_file)"}
}
nickel export $decl_file --format json 2>&1
}
# Export Nickel file to JSON for comparison
export def "nickel-to-json" [nickel_file: path] {
if not ($nickel_file | path exists) {
error make {msg: $"Nickel file not found: ($nickel_file)"}
}
nickel export $nickel_file 2>&1 | from json | to json
}
# Compare Nickel and Nickel JSON outputs for equivalence
export def "compare-equivalence" [decl_file: path, nickel_file: path] -> bool {
let source_json = (nickel-to-json $decl_file | from json)
let nickel_json = (nickel-to-json $nickel_file | from json)
$source_json == $nickel_json
}
# Show detailed comparison between Nickel and Nickel
export def "show-comparison" [decl_file: path, nickel_file: path] {
print $"Comparing: ($decl_file) ⇄ ($nickel_file)\n"
let source_json = (nickel-to-json $decl_file)
let nickel_json = (nickel-to-json $nickel_file)
print "=== Source Output (JSON) ==="
print $source_json
print ""
print "=== Target Output (JSON) ==="
print $nickel_json
print ""
let equivalent = ($source_json == $nickel_json)
if $equivalent {
print "✅ Outputs are EQUIVALENT"
} else {
print "❌ Outputs DIFFER"
print "\nDifferences:"
diff <(print $source_json | jq -S .) <(print $nickel_json | jq -S .)
}
}
# ============================================================
# Migration Workflow
# ============================================================
# Analyze Nickel file and recommend migration approach
export def "analyze-nickel" [decl_file: path] {
if not ($decl_file | path exists) {
error make {msg: $"File not found: ($decl_file)"}
}
let template = (get-template-type $decl_file)
let has_inheritance = (detect-inheritance $decl_file)
let exports = (detect-exports $decl_file)
let is_empty = (is-schema-only $decl_file)
print $"File: ($decl_file)"
print $"Template Type: ($template)"
print $"Has Schema Inheritance: ($has_inheritance)"
print $"Is Schema-Only (no exports): ($is_empty)"
print $"Exported Instances: ($exports | length)"
if ($exports | length) > 0 {
print "\nExported instances:"
$exports | each { |exp| print $" - ($exp)" }
}
}
# Generate skeleton Nickel file from Nickel template
export def "generate-nickel-skeleton" [decl_file: path, output_file: path] {
let template = (get-template-type $decl_file)
let source_name = ($decl_file | path basename | str replace ".ncl" "")
let skeleton = match $template {
"template-1-schema-only" => {
$"# | Schema definitions migrated from ($source_name).ncl\n# | Migrated: 2025-12-15\n\n{{}}"
},
"template-2-single-instance" => {
let exports = (detect-exports $decl_file)
let instance = ($exports | get 0 | str split " " | get 0)
$"# | Configuration migrated from ($source_name).ncl\n\n{\n ($instance) = {\n # TODO: Fill in fields\n },\n}"
},
_ => {
$"# | Migrated from ($source_name).ncl\n# | Template: ($template)\n\n{\n # TODO: Implement\n}"
},
}
print $skeleton
print $"\nTo save: print output to ($output_file)"
}
# ============================================================
# Batch Migration
# ============================================================
# Migrate multiple Nickel files to Nickel using templates
export def "batch-migrate" [
source_dir: path,
nickel_dir: path,
--pattern: string = "*.ncl",
--dry-run: bool = false,
] {
let source_files = (glob $"($source_dir)/($pattern)")
print $"Found ($source_files | length) Nickel files matching pattern: ($pattern)"
print ""
$source_files | each { |source_file|
let relative_path = ($source_file | str replace $"($source_dir)/" "")
let nickel_file = $"($nickel_dir)/($relative_path | str replace ".ncl" ".ncl")"
print $"[$relative_path]"
let template = (get-template-type $source_file)
print $" Template: ($template)"
if not $dry_run {
if ($nickel_file | path exists) {
print $" ⚠️ Already exists: ($nickel_file)"
} else {
print $" → Would migrate to: ($nickel_file)"
}
}
}
}
# ============================================================
# Validation
# ============================================================
# Validate Nickel file syntax
export def "validate-nickel" [nickel_file: path] -> bool {
# Validate Nickel syntax (no try-catch)
let result = (do { nickel export $nickel_file | null } | complete)
($result.exit_code == 0)
}
# Full migration validation for a file pair
export def "validate-migration" [decl_file: path, nickel_file: path] -> record {
let source_exists = ($decl_file | path exists)
let nickel_exists = ($nickel_file | path exists)
let nickel_valid = if $nickel_exists { (validate-nickel $nickel_file) } else { false }
let equivalent = if ($source_exists and $nickel_valid) {
(compare-equivalence $decl_file $nickel_file)
} else {
false
}
{
source_exists: $source_exists,
nickel_exists: $nickel_exists,
nickel_valid: $nickel_valid,
outputs_equivalent: $equivalent,
status: if $equivalent { "✅ PASS" } else { "❌ FAIL" },
}
}
# Validation report for all migrated files
export def "validation-report" [source_dir: path, nickel_dir: path] {
let nickel_files = (glob $"($nickel_dir)/**/*.ncl")
print $"Validation Report: ($nickel_files | length) Nickel files\n"
let results = $nickel_files | map { |nickel_file|
let relative = ($nickel_file | str replace $"($nickel_dir)/" "")
let source_file = $"($source_dir)/($relative | str replace ".ncl" ".ncl")"
let validation = (validate-migration $source_file $nickel_file)
print $"($validation.status) $relative"
if not $validation.nickel_valid {
print " ⚠️ Nickel syntax error"
}
if not $validation.outputs_equivalent {
print " ⚠️ JSON outputs differ"
}
$validation
}
let passed = ($results | where {|r| $r.outputs_equivalent} | length)
let total = ($results | length)
print $"\nSummary: ($passed)/($total) files PASS equivalence check"
}