Class: Synthra::CLI

Inherits:
Object
  • Object
show all
Defined in:
lib/synthra/cli.rb,
lib/synthra/cli/commands/base.rb,
lib/synthra/cli/commands/diff.rb,
lib/synthra/cli/commands/docs.rb,
lib/synthra/cli/commands/lint.rb,
lib/synthra/cli/commands/live.rb,
lib/synthra/cli/commands/seed.rb,
lib/synthra/cli/commands/export.rb,
lib/synthra/cli/commands/import.rb,
lib/synthra/cli/commands/generate.rb,
lib/synthra/cli/commands/validate.rb

Overview

Command-line interface for Synthra

The CLI provides commands for schema validation and data generation. It supports multiple output formats (JSON, NDJSON) and generation modes (random, edge, invalid, mixed).

Commands:

  • validate <path>: Check if a DSL file is syntactically valid
  • lint <path|dir>: Lint schemas for path errors, missing fields, etc.
  • generate <SchemaName>: Generate fake data for a schema

Examples:

Run the CLI

cli = CLI.new(ARGV)
exit_code = cli.run

Defined Under Namespace

Modules: Commands

Constant Summary collapse

EXIT_SUCCESS =

Exit code for successful execution

Returns:

  • (Integer)

    0

0
EXIT_PARSE_ERROR =

Exit code for parse/validation errors

Returns:

  • (Integer)

    1

1
EXIT_RUNTIME_ERROR =

Exit code for runtime errors

Returns:

  • (Integer)

    2

2

Instance Method Summary collapse

Constructor Details

#initialize(args) ⇒ CLI

Create a new CLI instance

Examples:

cli = CLI.new(["generate", "User", "--count", "10"])

Parameters:

  • args (Array<String>)

    command-line arguments (typically ARGV)



67
68
69
70
71
72
73
74
75
76
# File 'lib/synthra/cli.rb', line 67

def initialize(args)
  @args = args
  @options = {
    count: 1,           # Number of records to generate
    mode: :random,      # Generation mode
    format: :json,      # Output format
    pretty: false,      # Pretty-print JSON
    dir: "."            # Directory containing .dsl files
  }
end

Instance Method Details

#build_export_options(registry) ⇒ Object (private)



806
807
808
809
810
811
812
813
# File 'lib/synthra/cli.rb', line 806

def build_export_options(registry)
  {
    registry: registry,
    count: @options[:count],
    seed: @options[:seed],
    mode: @options[:mode] || :random
  }.compact
end

#compare_schemas(s1, s2) ⇒ Array<String> (private)

Compare two schemas and return differences

Parameters:

  • s1 (Schema)

    first schema

  • s2 (Schema)

    second schema

Returns:

  • (Array<String>)

    list of differences



1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
# File 'lib/synthra/cli.rb', line 1330

def compare_schemas(s1, s2)
  diff = []

  # Compare versions
  # :nocov:
  if s1.version != s2.version
    diff << "version: #{s1.version || 'nil'}#{s2.version || 'nil'}"
  end
  # :nocov:

  # Compare deprecation
  # :nocov:
  if s1.deprecated? != s2.deprecated?
    diff << "deprecated: #{s1.deprecated?}#{s2.deprecated?}"
  end
  # :nocov:

  # Compare fields
  f1_names = s1.fields.map(&:name)
  f2_names = s2.fields.map(&:name)

  (f2_names - f1_names).each { |f| diff << "+ field: #{f}" }
  (f1_names - f2_names).each { |f| diff << "- field: #{f}" }

  # Compare field types
  (f1_names & f2_names).each do |name|
    field1 = s1.field(name)
    field2 = s2.field(name)
    if field1.type_name != field2.type_name
      diff << "~ #{name}: #{field1.type_name}#{field2.type_name}"
    end
  end

  diff
end

#contracts_commandInteger (private)

Execute the contracts command

Manages data contracts registry.

Returns:

  • (Integer)

    exit code



2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
# File 'lib/synthra/cli.rb', line 2073

def contracts_command
  parse_contracts_options
  subcommand = @args.shift

  case subcommand
  when "publish"
    contracts_publish
  when "deprecate"
    contracts_deprecate
  when "list"
    contracts_list
  when "history"
    contracts_history
  when "diff"
    contracts_diff
  else
    puts "Usage: synthra contracts <subcommand> [options]"
    puts ""
    puts "Subcommands:"
    puts "  publish <schema> -v VERSION   Publish a schema version"
    puts "  deprecate <schema> -v VERSION Deprecate a schema version"
    puts "  list                          List all contracts"
    puts "  history <schema>              Show schema history"
    puts "  diff <schema> <v1> <v2>       Compare versions"
    EXIT_SUCCESS
  end
end

#contracts_deprecateInteger (private)

Contracts subcommand: deprecate

Returns:

  • (Integer)

    exit code



2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
# File 'lib/synthra/cli.rb', line 2315

def contracts_deprecate
  schema_name = @args.shift
  version = @options[:version]

  unless schema_name && version
    $stderr.puts "Usage: synthra contracts deprecate <SchemaName> -v <version>"
    return EXIT_PARSE_ERROR
  end

  contracts = ContractsRegistry.new(@options[:contracts_dir])
  contracts.deprecate(
    schema_name,
    version: version,
    sunset_date: @options[:sunset],
    message: @options[:changelog]
  )

  puts "Deprecated #{schema_name}@#{version}"
  puts "  Sunset: #{@options[:sunset]}" if @options[:sunset]
  EXIT_SUCCESS
end

#contracts_diffInteger (private)

Contracts subcommand: diff

Returns:

  • (Integer)

    exit code



2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
# File 'lib/synthra/cli.rb', line 2405

def contracts_diff
  schema_name = @args.shift
  v1 = @args.shift
  v2 = @args.shift

  unless schema_name && v1 && v2
    $stderr.puts "Usage: synthra contracts diff <SchemaName> <v1> <v2>"
    return EXIT_PARSE_ERROR
  end

  contracts = ContractsRegistry.new(@options[:contracts_dir])
  result = contracts.compatible?(schema_name, v1, v2)

  puts "Comparing #{schema_name} v#{v1} → v#{v2}"
  puts ""

  if result[:compatible]
    puts "✅ Compatible (no breaking changes)"
  else
    puts "❌ Breaking changes detected!"
  end

  puts ""
  puts "Changes:"
  changes = result[:changes]

  if changes[:removed_fields].any?
    puts "  Removed: #{changes[:removed_fields].join(', ')}"
  end

  if changes[:added_fields].any?
    puts "  Added: #{changes[:added_fields].join(', ')}"
  end

  changes[:type_changes].each do |c|
    puts "  Type changed: #{c[:field]} (#{c[:from]}#{c[:to]})"
  end

  changes[:required_changes].each do |c|
    puts "  Required changed: #{c[:field]} (#{c[:change]})"
  end

  result[:compatible] ? EXIT_SUCCESS : EXIT_PARSE_ERROR
end

#contracts_historyInteger (private)

Contracts subcommand: history

Returns:

  • (Integer)

    exit code



2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
# File 'lib/synthra/cli.rb', line 2374

def contracts_history
  schema_name = @args.shift

  unless schema_name
    $stderr.puts "Usage: synthra contracts history <SchemaName>"
    return EXIT_PARSE_ERROR
  end

  contracts = ContractsRegistry.new(@options[:contracts_dir])
  history = contracts.history(schema_name)

  if history.empty?
    puts "No history found for #{schema_name}"
    return EXIT_SUCCESS
  end

  puts "History for #{schema_name}:"
  puts ""

  history.reverse.each do |entry|
    puts "  #{entry[:timestamp]} - v#{entry[:version]} #{entry[:action]}"
    puts "    #{entry[:message]}" if entry[:message]
  end

  EXIT_SUCCESS
end

#contracts_listInteger (private)

Contracts subcommand: list

Returns:

  • (Integer)

    exit code



2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
# File 'lib/synthra/cli.rb', line 2341

def contracts_list
  contracts = ContractsRegistry.new(@options[:contracts_dir])
  all = contracts.list

  if all.empty?
    puts "No contracts found."
    return EXIT_SUCCESS
  end

  puts "Data Contracts:"
  puts ""

  contracts.schema_names.each do |name|
    puts "  #{name}:"
    contracts.versions_for(name).each do |v|
      contract = contracts.get(name, version: v)
      state_icon = case contract[:state]
                   when :published then ""
                   when :deprecated then "⚠️"
                   when :retired then ""
                   else "📝"
                   end
      puts "    #{state_icon} v#{v} (#{contract[:fields].count} fields)"
    end
  end

  EXIT_SUCCESS
end

#contracts_publishInteger (private)

Contracts subcommand: publish

Returns:

  • (Integer)

    exit code



2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
# File 'lib/synthra/cli.rb', line 2282

def contracts_publish
  schema_name = @args.shift
  version = @options[:version]

  unless schema_name && version
    $stderr.puts "Usage: synthra contracts publish <SchemaName> -v <version>"
    return EXIT_PARSE_ERROR
  end

  registry = load_registry(@options[:schema_dir] || "schemas")
  unless registry.schema?(schema_name)
    $stderr.puts "Schema not found: #{schema_name}"
    return EXIT_PARSE_ERROR
  end

  contracts = ContractsRegistry.new(@options[:contracts_dir])
  contract = contracts.publish(
    schema_name,
    version: version,
    schema: registry.schema(schema_name),
    changelog: @options[:changelog]
  )

  puts "Published #{schema_name}@#{version}"
  puts "  Hash: #{contract[:schema_hash]}"
  puts "  Fields: #{contract[:fields].count}"
  EXIT_SUCCESS
end

#debug_generation(schema, registry, mode, seed) ⇒ Object (private)

Debug generation with step-through

Parameters:

  • schema (Schema)

    schema to generate from

  • registry (Registry)

    schema registry

  • mode (Symbol)

    generation mode

  • seed (Integer, nil)

    seed for determinism



601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
# File 'lib/synthra/cli.rb', line 601

def debug_generation(schema, registry, mode, seed)
  puts "🐛 Debug Mode: Generating #{schema.name}"
  puts "=" * 60
  puts "Press Enter to continue after each field, 'q' to quit, 'c' to continue without pausing"
  puts

  field_index = 0
  context_snapshot = nil

  # Set up callback to intercept field generation
  original_callback = Synthra.configuration.on_field_generated
  
  Synthra.configuration.on_field_generated = lambda do |schema_name, field_name, value, duration|
    field_index += 1
    
    puts "\n[Field #{field_index}] #{field_name}"
    puts "" * 60
    puts "Value: #{REPL::Formatter.format_value(value, 80)}"
    puts "Duration: #{duration}ms" if duration
    puts "Schema: #{schema_name}"
    puts
    
    loop do
      print "Press Enter to continue, 'q' to quit, 'c' to continue without pausing: "
      response = $stdin.gets&.strip&.downcase
      
      case response
      when "q", "quit"
        Synthra.configuration.on_field_generated = original_callback
        puts "Debug cancelled."
        return
      when "c", "continue"
        Synthra.configuration.on_field_generated = original_callback
        puts "Continuing without pausing..."
        return
      when "", nil
        break
      else
        puts "Invalid input. Press Enter, 'q', or 'c'."
      end
    end
  end

  begin
    opts = { registry: registry, mode: mode }
    opts[:seed] = seed if seed
    
    result = schema.generate(**opts)
    
    puts
    puts "=" * 60
    puts "✅ Generation Complete!"
    puts "=" * 60
    puts
    puts "Final Record:"
    puts REPL::Formatter.table(result)
  ensure
    Synthra.configuration.on_field_generated = original_callback
  end
end

#diff_commandInteger (private)

Execute the diff command

Compares two schema files/directories and shows differences.

Returns:

  • (Integer)

    exit code



345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
# File 'lib/synthra/cli.rb', line 345

def diff_command
  parse_diff_options
  path1 = @args.shift
  path2 = @args.shift

  unless path1 && path2
    $stderr.puts "Usage: synthra diff <path1> <path2>"
    return EXIT_PARSE_ERROR
  end

  registry1 = Registry.new
  registry2 = Registry.new

  load_path(registry1, path1)
  load_path(registry2, path2)

  schemas1 = registry1.schemas
  schemas2 = registry2.schemas

  all_names = (schemas1.keys + schemas2.keys).uniq.sort

  puts "=" * 60
  puts "Schema Diff"
  puts "=" * 60
  puts

  has_diff = false

  all_names.each do |name|
    s1 = schemas1[name]
    s2 = schemas2[name]

    if s1.nil?
      puts "#{name} (added in #{path2})"
      has_diff = true
    elsif s2.nil?
      puts "#{name} (removed in #{path2})"
      has_diff = true
    else
      diff = compare_schemas(s1, s2)
      if diff.any?
        puts "📝 #{name} (modified)"
        diff.each { |d| puts "   #{d}" }
        has_diff = true
      end
    end
  end

  puts "✅ No differences found." unless has_diff
  EXIT_SUCCESS
end

#docs_commandInteger (private)

Execute the docs command

Generates HTML documentation from schemas.

Returns:

  • (Integer)

    exit code



1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
# File 'lib/synthra/cli.rb', line 1899

def docs_command
  parse_docs_options
  path = @args.shift || @options[:dir]

  unless path && File.directory?(path)
    $stderr.puts "Usage: synthra docs <schema-directory> [options]"
    return EXIT_PARSE_ERROR
  end

  cmd = CLI::Commands::Docs.new
  cmd.call(path, @options)
end

#export_commandInteger (private)

Execute the export command

Exports schema in various formats (JSON Schema, etc.)

Returns:

  • (Integer)

    exit code



670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
# File 'lib/synthra/cli.rb', line 670

def export_command
  parse_export_options
  schema_name = @args.shift

  registry = Registry.new
  load_schemas_from_dir_no_validate(registry, @options[:dir])

  format = @options[:export_format]

  # Handle "export all" mode for bulk exports
  if @options[:all]
    output = case format
             when "typescript", "ts"
               Export::Typescript.export_all(registry)
             when "javascript", "js"
               Export::Javascript.export_all(registry)
             when "python", "py", "pydantic"
               Export::Python.export_all(registry, style: @options[:python_style])
             when "dataclass"
               Export::Python.export_all(registry, style: :dataclass)
             when "sql", "sql-ddl", "postgresql", "mysql", "sqlite"
               Export::Sql.export_all(registry, dialect: @options[:sql_dialect])
             when "graphviz", "dot"
               Export::Graphviz.new(nil, registry: registry).export
             when "protobuf", "proto"
               Export::Protobuf.export_all(registry, package: @options[:package] || "generated")
             when "openapi", "swagger"
               Export::OpenAPI.new(registry,
                 title: @options[:title] || "Synthra API",
                 format: @options[:output]&.end_with?(".json") ? :json : :yaml
               ).export
             else
               $stderr.puts "Bulk export not supported for format: #{format}"
               $stderr.puts "Supported: typescript, javascript, python, sql, graphviz, protobuf, openapi"
               return EXIT_PARSE_ERROR
             end
  else
    unless schema_name
      $stderr.puts "Usage: synthra export <SchemaName> [options]"
      $stderr.puts "       synthra export --all [options]"
      $stderr.puts ""
      $stderr.puts "Schema formats: json-schema, typescript, javascript, sql, graphviz"
      $stderr.puts "Data formats:   json, csv, sql-insert, yaml, xml"
      return EXIT_PARSE_ERROR
    end

    schema = registry.schema(schema_name)
    export_opts = build_export_options(registry)

    output = case format
             # Schema export formats
             when "json-schema"
               Export::JsonSchema.new(schema, **export_opts).export
             when "typescript", "ts"
               Export::Typescript.new(schema, **export_opts).export
             when "javascript", "js"
               Export::Javascript.new(schema, **export_opts).export
             when "python", "py", "pydantic"
               Export::Python.new(schema, **export_opts.merge(style: @options[:python_style])).export
             when "dataclass"
               Export::Python.new(schema, **export_opts.merge(style: :dataclass)).export
             when "sql", "sql-ddl", "postgresql", "mysql", "sqlite"
               dialect = %w[postgresql mysql sqlite].include?(format) ? format.to_sym : @options[:sql_dialect]
               Export::Sql.new(schema, registry: registry, dialect: dialect).export
             when "graphviz", "dot"
               Export::Graphviz.new(schema, **export_opts).export
             when "protobuf", "proto"
               Export::Protobuf.new(schema, **export_opts.merge(
                 package: @options[:package] || "generated"
               )).export
             when "openapi", "swagger"
               mini_registry = Registry.new
               mini_registry.register_schema(schema.name, schema)
               Export::OpenAPI.new(mini_registry,
                 title: @options[:title] || "#{schema.name} API",
                 format: @options[:output]&.end_with?(".json") ? :json : :yaml
               ).export

             # Data export formats
             when "json", "json-data"
               Export::JsonData.new(schema, **export_opts.merge(
                 pretty: @options[:pretty],
                 envelope: @options[:envelope]
               )).export
             when "csv"
               Export::Csv.new(schema, **export_opts).export
             when "sql-insert", "insert"
               Export::SqlInsert.new(schema, **export_opts.merge(
                 dialect: @options[:sql_dialect],
                 batch_insert: @options[:batch_insert]
               )).export
             when "yaml", "yml"
               Export::YamlData.new(schema, **export_opts).export
             when "xml"
               Export::XmlData.new(schema, **export_opts.merge(
                 root: @options[:root],
                 item: @options[:item]
               )).export
             else
               $stderr.puts "Unknown export format: #{format}"
               $stderr.puts ""
               $stderr.puts "Schema formats: json-schema, typescript, javascript, python, sql, graphviz, protobuf, openapi"
               $stderr.puts "Data formats:   json, csv, sql-insert, yaml, xml"
               return EXIT_PARSE_ERROR
             end
  end

  # Determine output path
  output_path = if @options[:output]
                  @options[:output]
                elsif @options[:auto_file]
                  output_dir = @options[:output_dir] || "."
                  filename = Export.auto_filename(
                    @options[:all] ? "all_schemas" : schema_name,
                    format
                  )
                  File.join(output_dir, filename)
                else
                  nil
                end

  if output_path
    FileUtils.mkdir_p(File.dirname(output_path)) if File.dirname(output_path) != "."
    File.write(output_path, output)
    puts "✓ Exported to #{output_path}"
  else
    puts output
  end

  EXIT_SUCCESS
rescue KeyError
  $stderr.puts "Unknown schema: #{schema_name}"
  $stderr.puts "Available schemas: #{registry.names.join(", ")}"
  EXIT_PARSE_ERROR
end

#export_json_schema(schema, registry) ⇒ String (private)

Export schema as JSON Schema

:nocov:

Parameters:

  • schema (Schema)

    schema to export

  • registry (Registry)

    registry for resolving references

Returns:

  • (String)

    JSON Schema as JSON string



822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
# File 'lib/synthra/cli.rb', line 822

def export_json_schema(schema, registry)
  json_schema = {
    "$schema" => "https://json-schema.org/draft/2020-12/schema",
    "$id" => "#{schema.name.downcase}.json",
    "title" => schema.name,
    "type" => "object",
    "properties" => {},
    "required" => []
  }

  schema.fields.each do |field|
    prop = field_to_json_schema(field, registry)
    json_schema["properties"][field.name] = prop
    json_schema["required"] << field.name unless field.optional?
  end

  JSON.pretty_generate(json_schema)
end

#export_typescript(schema, registry) ⇒ String (private)

Export schema as TypeScript interface

:nocov:

Parameters:

  • schema (Schema)

    schema to export

  • registry (Registry)

    registry for resolving references

Returns:

  • (String)

    TypeScript interface definition



919
920
921
922
923
924
925
926
927
928
929
930
931
# File 'lib/synthra/cli.rb', line 919

def export_typescript(schema, registry)
  lines = ["interface #{schema.name} {"]
  
  schema.fields.each do |field|
    ts_type = field_to_typescript(field, registry)
    optional = field.optional? ? "?" : ""
    nullable = field.nullable? ? " | null" : ""
    lines << "  #{field.name}#{optional}: #{ts_type}#{nullable};"
  end
  
  lines << "}"
  lines.join("\n")
end

#field_to_json_schema(field, registry) ⇒ Hash (private)

Convert a field to JSON Schema property

Parameters:

  • field (Field)

    field to convert

  • registry (Registry)

    registry for resolving references

Returns:

  • (Hash)

    JSON Schema property definition



848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
# File 'lib/synthra/cli.rb', line 848

def field_to_json_schema(field, registry)
  type_name = field.type_name
  prop = {}

  case type_name
  when "uuid", "ulid"
    prop = { "type" => "string", "format" => "uuid" }
  when "email"
    prop = { "type" => "string", "format" => "email" }
  when "url"
    prop = { "type" => "string", "format" => "uri" }
  when "date", "past_date", "future_date"
    prop = { "type" => "string", "format" => "date" }
  when "timestamp", "now"
    prop = { "type" => "string", "format" => "date-time" }
  when "number", "integer", "id_sequence"
    prop = { "type" => "integer" }
    if field.type_args[:range]
      range = field.type_args[:range]
      prop["minimum"] = range.min if range.respond_to?(:min)
      prop["maximum"] = range.max if range.respond_to?(:max)
    end
  when "float", "money"
    prop = { "type" => "number" }
  when "boolean"
    prop = { "type" => "boolean" }
  when "text", "name", "full_name", "first_name", "last_name", "phone", "city", "country", "country_code", "postal_code"
    prop = { "type" => "string" }
  when "enum"
    values = field.type_args[:values]&.map { |v| v.respond_to?(:value) ? v.value : v } || []
    prop = { "type" => "string", "enum" => values }
  when "const"
    value = field.type_args[:value]
    prop = { "const" => value }
  when "array"
    element_type = field.type_args[:element]
    size = field.type_args[:size]
    prop = { "type" => "array" }
    if element_type && registry.schema?(element_type.to_s)
      prop["items"] = { "$ref" => "#/$defs/#{element_type}" }
    else
      prop["items"] = { "type" => "string" }
    end
    if size.is_a?(Range)
      prop["minItems"] = size.min
      prop["maxItems"] = size.max
    elsif size.is_a?(Hash)
      prop["minItems"] = size[:min] if size[:min]
      prop["maxItems"] = size[:max] if size[:max]
    end
  else
    # Assume it's a schema reference
    if registry.schema?(type_name)
      prop = { "$ref" => "#/$defs/#{type_name}" }
    else
      prop = { "type" => "string" }
    end
  end

  prop["nullable"] = true if field.nullable?
  prop
end

#field_to_typescript(field, registry) ⇒ String (private)

Convert a field to TypeScript type

Parameters:

  • field (Field)

    field to convert

  • registry (Registry)

    registry for resolving references

Returns:

  • (String)

    TypeScript type



940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
# File 'lib/synthra/cli.rb', line 940

def field_to_typescript(field, registry)
  type_name = field.type_name

  case type_name
  when "uuid", "ulid", "email", "url", "date", "past_date", "future_date", "timestamp", "now", "text", "name", "full_name", "first_name", "last_name", "phone", "city", "country", "country_code", "postal_code"
    "string"
  when "number", "integer", "id_sequence", "float", "money"
    "number"
  when "boolean"
    "boolean"
  when "enum"
    values = field.type_args[:values]&.map { |v| "\"#{v.respond_to?(:value) ? v.value : v}\"" } || []
    values.join(" | ")
  when "const"
    value = field.type_args[:value]
    value.is_a?(String) ? "\"#{value}\"" : value.to_s
  when "array"
    element_type = field.type_args[:element]
    if element_type && registry.schema?(element_type.to_s)
      "#{element_type}[]"
    else
      "string[]"
    end
  else
    registry.schema?(type_name) ? type_name : "any"
  end
end

#generate_commandInteger (private)

Execute the generate command

Loads schemas from a directory, finds the specified schema, and generates fake data according to options.

Returns:

  • (Integer)

    exit code



1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
# File 'lib/synthra/cli.rb', line 1445

def generate_command
  parse_generate_options
  schema_name = @args.shift

  unless schema_name
    $stderr.puts "Usage: synthra generate <SchemaName> [options]"
    return EXIT_PARSE_ERROR
  end

  registry = Registry.new
  load_schemas_from_dir(registry, @options[:dir])

  schema = registry.schema(schema_name)
  generate_output(schema)
  EXIT_SUCCESS
rescue KeyError
  $stderr.puts "Unknown schema: #{schema_name}"
  $stderr.puts "Available schemas: #{registry.schemas.keys.join(", ")}"
  EXIT_PARSE_ERROR
end

#generate_graphviz(registry) ⇒ String (private)

Generate GraphViz DOT representation of schemas

:nocov:

Parameters:

  • registry (Registry)

    registry containing schemas

Returns:

  • (String)

    DOT format string



1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
# File 'lib/synthra/cli.rb', line 1105

def generate_graphviz(registry)
  lines = [
    "digraph Synthra {",
    "  rankdir=LR;",
    "  node [shape=record, fontname=\"Helvetica\"];",
    "  edge [fontname=\"Helvetica\", fontsize=10];",
    ""
  ]

  registry.schemas.each do |name, schema|
    # Create node for schema
    fields = schema.fields.map do |f|
      optional = f.optional? ? "?" : ""
      nullable = f.nullable? ? "?" : ""
      "#{f.name}#{optional}: #{f.type_name}#{nullable}"
    end
    
    label = "{#{name}|#{fields.join("\\l")}\\l}"
    color = schema.deprecated? ? "gray" : "black"
    lines << "  #{name} [label=\"#{label}\", color=#{color}];"
  end

  lines << ""

  # Create edges for references
  registry.schemas.each do |name, schema|
    schema.fields.each do |field|
      type_name = field.type_name
      
      # Direct schema reference
      if registry.schema?(type_name)
        lines << "  #{name} -> #{type_name} [label=\"#{field.name}\"];"
      end
      
      # Array element type
      if type_name == "array"
        element = field.type_args[:element]
        if element && registry.schema?(element.to_s)
          lines << "  #{name} -> #{element} [label=\"#{field.name}[]\", style=dashed];"
        end
      end
      
      # map_by_field entries
      if type_name == "map_by_field"
        entries = field.type_args[:entries] || {}
        entries.each do |entry_name, |
          schema_name = .is_a?(Hash) ? [:schema] : .to_s
          if schema_name && registry.schema?(schema_name)
            lines << "  #{name} -> #{schema_name} [label=\"#{field.name}.#{entry_name}\", style=dotted];"
          end
        end
      end
    end
  end

  lines << "}"
  lines.join("\n")
end

#generate_output(schema) ⇒ void (private)

This method returns an undefined value.

Generate and output fake data

Handles both JSON and NDJSON output formats.

Parameters:

  • schema (Schema)

    schema to generate from



1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
# File 'lib/synthra/cli.rb', line 1518

def generate_output(schema)
  options = { seed: @options[:seed], mode: @options[:mode] }.compact

  if @options[:format] == :ndjson

    # NDJSON: One JSON object per line, streamed
    schema.generate_stream(count: @options[:count], **options).each do |record|
      puts JSON.generate(sanitize_for_json(record))
    end
  else

    # JSON: Single array or object
    records = schema.generate_many(@options[:count], **options)
    output = @options[:count] == 1 ? records.first : records

    if @options[:pretty]
      puts JSON.pretty_generate(sanitize_for_json(output))
    else
      puts JSON.generate(sanitize_for_json(output))
    end
  end
end

#graph_commandInteger (private)

Execute the graph command

Generates GraphViz DOT output for schema visualization.

Returns:

  • (Integer)

    exit code



1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
# File 'lib/synthra/cli.rb', line 1062

def graph_command
  parse_graph_options
  path = @args.shift || @options[:dir]

  registry = Registry.new
  if File.directory?(path)
    load_schemas_from_dir_no_validate(registry, path)
  else
    registry.load_file(path, validate: false)
  end

  # Use the Export::Graphviz module
  exporter = Export::Graphviz.new(nil, registry: registry, 
                                   direction: @options[:direction] || "LR",
                                   group: @options[:group])
  dot_output = exporter.export

  if @options[:output]
    File.write(@options[:output], dot_output)
    puts "✓ Generated graph to #{@options[:output]}"
    
    # Try to render PNG/SVG if graphviz is installed
    # :nocov:
    if @options[:render] && system("which dot > /dev/null 2>&1")
      render_format = @options[:render_format] || "png"
      output_file = @options[:output].sub(/\.dot$/, ".#{render_format}")
      system("dot", "-T#{render_format}", @options[:output], "-o", output_file)
      puts "✓ Rendered #{render_format.upcase} to #{output_file}"
    end
    # :nocov:
  else
    puts dot_output
  end

  EXIT_SUCCESS
end

#import_commandInteger (private)

Execute the import command

Imports schemas from external formats (OpenAPI, JSON Schema).

Returns:

  • (Integer)

    exit code



1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
# File 'lib/synthra/cli.rb', line 1880

def import_command
  parse_import_options
  path = @args.shift

  unless path
    $stderr.puts "Usage: synthra import <openapi.yaml|json-schema.json> [options]"
    return EXIT_PARSE_ERROR
  end

  cmd = CLI::Commands::Import.new
  cmd.call(path, @options)
end

#info_commandInteger (private)

Execute the info command

Shows information about schemas including version, fields, and deprecation status.

Returns:

  • (Integer)

    exit code



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/synthra/cli.rb', line 283

def info_command
  parse_info_options
  path = @args.shift

  unless path
    $stderr.puts "Usage: synthra info <path|directory> [options]"
    return EXIT_PARSE_ERROR
  end

  registry = Registry.new

  if File.directory?(path)
    load_schemas_from_dir_no_validate(registry, path)
  else
    registry.load_file(path, validate: false)
  end

  schemas = registry.schemas

  if schemas.empty?
    puts "No schemas found."
    return EXIT_SUCCESS
  end

  puts "=" * 60
  puts "Schema Information"
  puts "=" * 60
  puts

  schemas.each do |name, schema|
    puts "📦 #{name}"
    puts "   Version: #{schema.version || 'not specified'}"
    puts "   Fields: #{schema.fields.length}"
    puts "   Behaviors: #{schema.behaviors.length}"
    
    # :nocov:
    if schema.deprecated?
      puts "   ⚠️  DEPRECATED: #{schema.deprecation_message || 'No message'}"
    end
    # :nocov:
    
    if @options[:verbose]
      puts "   Field list:"
      schema.fields.each do |field|
        optional = field.optional? ? "?" : ""
        nullable = field.nullable? ? "?" : ""
        puts "     - #{field.name}#{optional}: #{field.type_name}#{nullable}"
      end
    end
    puts
  end

  EXIT_SUCCESS
end

#lint_commandInteger (private)

Execute the lint command

Loads schemas and checks for semantic errors:

  • Invalid copy() paths
  • Missing map_by_field key fields
  • Undefined schema references
  • Potential typos with suggestions

Returns:

  • (Integer)

    exit code



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/synthra/cli.rb', line 187

def lint_command
  parse_lint_options
  path = @args.shift

  unless path
    $stderr.puts "Usage: synthra lint <path|directory>"
    return EXIT_PARSE_ERROR
  end

  registry = Registry.new

  # Load schemas without automatic validation (we'll validate manually for better output)
  if File.directory?(path)
    load_schemas_from_dir_no_validate(registry, path)
  else
    registry.load_file(path, validate: false)
  end

  # Run validation and collect errors
  errors = registry.validate_paths

  if errors.empty?
    puts "✓ No lint errors found in #{registry.size} schema(s)"
    if @options[:verbose]
      puts "\nSchemas checked:"
      registry.names.sort.each { |name| puts "  - #{name}" }
    end
    EXIT_SUCCESS
  else
    puts "✗ Found #{errors.length} lint error(s):\n\n"

    errors.each_with_index do |error, i|
      puts "#{i + 1}. #{error.message}"
      # :nocov:
      if error.respond_to?(:suggestions) && error.suggestions&.any?
        puts "   Did you mean: #{error.suggestions.first(3).join(", ")}?"
      end
      # :nocov:
      puts
    end

    # In strict mode, treat warnings as errors
    if @options[:strict]
      $stderr.puts "Strict mode: #{errors.length} error(s) treated as failures"
    end

    EXIT_PARSE_ERROR
  end
end

#live_commandInteger (private)

Execute the live command

Starts a live preview server with web UI.

:nocov:

Returns:

  • (Integer)

    exit code



1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
# File 'lib/synthra/cli.rb', line 1919

def live_command
  parse_live_options
  path = @args.shift || @options[:dir]

  unless path && File.directory?(path)
    $stderr.puts "Usage: synthra live <schema-directory> [options]"
    return EXIT_PARSE_ERROR
  end

  cmd = CLI::Commands::Live.new
  cmd.call(path, @options)
end

#load_path(registry, path) ⇒ void (private)

This method returns an undefined value.

Load schemas from a path (file or directory)

Parameters:

  • registry (Registry)

    registry to load into

  • path (String)

    file or directory path



1315
1316
1317
1318
1319
1320
1321
# File 'lib/synthra/cli.rb', line 1315

def load_path(registry, path)
  if File.directory?(path)
    load_schemas_from_dir_no_validate(registry, path)
  else
    registry.load_file(path, validate: false)
  end
end

#load_registry(dir) ⇒ Registry (private)

Load registry from directory

Parameters:

  • dir (String)

    directory path

Returns:



2455
2456
2457
2458
2459
# File 'lib/synthra/cli.rb', line 2455

def load_registry(dir)
  registry = Registry.new
  registry.load_dir(dir || ".")
  registry
end

#load_schemas_from_dir(registry, dir) ⇒ void (private)

This method returns an undefined value.

Load all .dsl files from a directory

Parameters:

  • registry (Registry)

    registry to load schemas into

  • dir (String)

    directory path



1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
# File 'lib/synthra/cli.rb', line 1473

def load_schemas_from_dir(registry, dir)

  # Validate and sanitize directory path to prevent path traversal
  dir = File.expand_path(dir)

  # Reject paths with .. components (shouldn't happen after expand_path, but double-check)
  # :nocov:
  if dir.include?("..")
    raise ArgumentError, "Invalid directory path: #{dir}"
  end
  # :nocov:

  # Verify directory exists
  unless Dir.exist?(dir)
    raise Errno::ENOENT, "Directory not found: #{dir}"
  end

  pattern = File.join(dir, "*.dsl")
  files = Dir.glob(pattern)

  if files.empty?
    $stderr.puts "No .dsl files found in #{dir}"
    return
  end

  # Validate all files are within the directory (prevent symlink attacks)
  files.each do |f|
    file_path = File.expand_path(f)
    # :nocov:
    unless file_path.start_with?(dir)
      raise ArgumentError, "Invalid file path: #{f}"
    end
    # :nocov:
    registry.load_file(f)
  end
end

#load_schemas_from_dir_no_validate(registry, dir) ⇒ void (private)

This method returns an undefined value.

Load schemas from directory without validation

Parameters:

  • registry (Registry)

    registry to load schemas into

  • dir (String)

    directory path

Raises:

  • (ArgumentError)


1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
# File 'lib/synthra/cli.rb', line 1417

def load_schemas_from_dir_no_validate(registry, dir)
  dir = File.expand_path(dir)
  raise ArgumentError, "Invalid directory path: #{dir}" if dir.include?("..")
  raise Errno::ENOENT, "Directory not found: #{dir}" unless Dir.exist?(dir)

  pattern = File.join(dir, "*.dsl")
  files = Dir.glob(pattern)

  if files.empty?
    $stderr.puts "No .dsl files found in #{dir}"
    return
  end

  files.each do |f|
    file_path = File.expand_path(f)
    raise ArgumentError, "Invalid file path: #{f}" unless file_path.start_with?(dir)
    registry.load_file(f, validate: false)
  end
end

#lsp_commandInteger (private)

Execute the LSP server command

Starts the Language Server Protocol server for IDE integration. Provides features like go-to-definition, hover, completion, and diagnostics.

:nocov:

Returns:

  • (Integer)

    exit code (never returns normally - runs server loop)



1864
1865
1866
1867
1868
1869
1870
1871
# File 'lib/synthra/cli.rb', line 1864

def lsp_command
  require_relative "lsp/server"
  server = LSP::Server.new
  server.run
  EXIT_SUCCESS
rescue Interrupt
  EXIT_SUCCESS
end

#parse_contracts_optionsvoid (private)

This method returns an undefined value.

Parse options for contracts command



2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
# File 'lib/synthra/cli.rb', line 2206

def parse_contracts_options
  @options[:contracts_dir] = "contracts"
  @options[:version] = nil
  @options[:changelog] = nil

  OptionParser.new do |opts|
    opts.banner = "Usage: synthra contracts <subcommand> [options]"

    opts.on("-d", "--dir DIR", "Contracts directory (default: contracts)") do |v|
      @options[:contracts_dir] = v
    end

    opts.on("-s", "--schema-dir DIR", "Schema directory") do |v|
      @options[:schema_dir] = v
    end

    opts.on("-v", "--version VERSION", "Version (semver)") do |v|
      @options[:version] = v
    end

    opts.on("-m", "--message MSG", "Changelog message") do |v|
      @options[:changelog] = v
    end

    opts.on("--sunset DATE", "Sunset date for deprecation") do |v|
      @options[:sunset] = Date.parse(v)
    end
  end.parse!(@args)
end

#parse_diff_optionsvoid (private)

This method returns an undefined value.

Parse options for diff command



1388
1389
1390
1391
1392
# File 'lib/synthra/cli.rb', line 1388

def parse_diff_options
  OptionParser.new do |opts|
    opts.banner = "Usage: synthra diff <path1> <path2>"
  end.parse!(@args)
end

#parse_docs_optionsvoid (private)

This method returns an undefined value.

Parse options for docs command



1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
# File 'lib/synthra/cli.rb', line 1965

def parse_docs_options
  @options[:output] = "docs"
  @options[:title] = "Synthra Schema Documentation"

  OptionParser.new do |opts|
    opts.banner = "Usage: synthra docs <directory> [options]"

    opts.on("-d", "--dir DIR", "Directory containing .dsl files") do |v|
      @options[:dir] = v
    end

    opts.on("-o", "--output DIR", "Output directory for documentation") do |v|
      @options[:output] = v
    end

    opts.on("-t", "--title TITLE", "Documentation title") do |v|
      @options[:title] = v
    end
  end.parse!(@args)
end

#parse_export_optionsvoid (private)

This method returns an undefined value.

Parse options for export command



974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
# File 'lib/synthra/cli.rb', line 974

def parse_export_options
  @options[:export_format] = "json-schema"
  @options[:output] = nil
  @options[:output_dir] = nil
  @options[:auto_file] = false
  @options[:sql_dialect] = :postgresql
  @options[:count] = 10
  @options[:all] = false
  @options[:pretty] = true
  @options[:batch_insert] = false
  @options[:envelope] = nil
  @options[:root] = nil
  @options[:item] = nil
  @options[:python_style] = :pydantic

  OptionParser.new do |opts|
    opts.banner = "Usage: synthra export <SchemaName> [options]"

    opts.on("-d", "--dir DIR", "Directory containing .dsl files") do |v|
      @options[:dir] = v
    end

    opts.on("-f", "--format FORMAT", "Export format (see list below)") do |v|
      @options[:export_format] = v
    end

    opts.on("-o", "--output FILE", "Output file (default: stdout)") do |v|
      @options[:output] = v
    end

    opts.on("--out-dir DIR", "Output directory for auto-generated files") do |v|
      @options[:output_dir] = v
      @options[:auto_file] = true
    end

    opts.on("--auto-file", "Auto-generate filename based on schema and format") do
      @options[:auto_file] = true
    end

    opts.on("--dialect DIALECT", "SQL dialect: postgresql, mysql, sqlite") do |v|
      @options[:sql_dialect] = v.to_sym
    end

    opts.on("-c", "--count COUNT", Integer, "Number of records for data exports (default: 10)") do |v|
      @options[:count] = v
    end

    opts.on("-s", "--seed SEED", Integer, "Seed for deterministic generation") do |v|
      @options[:seed] = v
    end

    opts.on("-a", "--all", "Export all schemas (typescript/javascript/python/sql)") do
      @options[:all] = true
    end

    opts.on("--batch", "Use batch INSERT for sql-insert format") do
      @options[:batch_insert] = true
    end

    opts.on("--compact", "Compact output (no pretty-print)") do
      @options[:pretty] = false
    end

    opts.on("--envelope KEY", "Wrap JSON output in envelope with this key") do |v|
      @options[:envelope] = v
    end

    opts.on("--root NAME", "Root element name for XML") do |v|
      @options[:root] = v
    end

    opts.on("--item NAME", "Item element name for XML") do |v|
      @options[:item] = v
    end

    opts.on("--style STYLE", "Python style: pydantic, dataclass, typed_dict") do |v|
      @options[:python_style] = v.to_sym
    end
  end.parse!(@args)
end

#parse_generate_optionsvoid (private)

This method returns an undefined value.

Parse options for generate command



1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
# File 'lib/synthra/cli.rb', line 1625

def parse_generate_options
  OptionParser.new do |opts|
    opts.banner = "Usage: synthra generate <SchemaName> [options]"

    opts.on("-d", "--dir DIR", "Directory containing .dsl files") do |v|
      @options[:dir] = v
    end

    opts.on("-c", "--count COUNT", Integer, "Number of records to generate") do |v|
      @options[:count] = v
    end

    opts.on("-s", "--seed SEED", Integer, "Seed for deterministic generation") do |v|
      @options[:seed] = v
    end

    opts.on("-m", "--mode MODE", %i[random edge invalid mixed], "Generation mode") do |v|
      @options[:mode] = v
    end

    opts.on("--json", "Output as JSON array (default)") do
      @options[:format] = :json
    end

    opts.on("--ndjson", "Output as newline-delimited JSON") do
      @options[:format] = :ndjson
    end

    opts.on("--pretty", "Pretty-print JSON output") do
      @options[:pretty] = true
    end
  end.parse!(@args)
end

#parse_graph_optionsvoid (private)

This method returns an undefined value.

Parse options for graph command



1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
# File 'lib/synthra/cli.rb', line 1170

def parse_graph_options
  @options[:output] = nil
  @options[:render] = false
  @options[:render_format] = "png"
  @options[:direction] = "LR"
  @options[:group] = false

  OptionParser.new do |opts|
    opts.banner = "Usage: synthra graph <path|directory> [options]"

    opts.on("-d", "--dir DIR", "Directory containing .dsl files") do |v|
      @options[:dir] = v
    end

    opts.on("-o", "--output FILE", "Output DOT file") do |v|
      @options[:output] = v
    end

    opts.on("-r", "--render", "Also render image (requires graphviz)") do
      @options[:render] = true
    end

    opts.on("--format FORMAT", "Render format: png, svg, pdf (default: png)") do |v|
      @options[:render_format] = v
    end

    opts.on("--direction DIR", "Graph direction: LR, TB, BT, RL (default: LR)") do |v|
      @options[:direction] = v.upcase
    end

    opts.on("-g", "--group", "Group schemas by category") do
      @options[:group] = true
    end
  end.parse!(@args)
end

#parse_import_optionsvoid (private)

This method returns an undefined value.

Parse options for import command



1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
# File 'lib/synthra/cli.rb', line 1949

def parse_import_options
  @options[:output] = "schemas"

  OptionParser.new do |opts|
    opts.banner = "Usage: synthra import <file> [options]"

    opts.on("-o", "--output DIR", "Output directory for generated schemas") do |v|
      @options[:output] = v
    end
  end.parse!(@args)
end

#parse_info_optionsvoid (private)

This method returns an undefined value.

Parse options for info command



1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
# File 'lib/synthra/cli.rb', line 1371

def parse_info_options
  @options[:verbose] = false

  OptionParser.new do |opts|
    opts.banner = "Usage: synthra info <path|directory>"

    opts.on("-v", "--verbose", "Show field details") do
      @options[:verbose] = true
    end
  end.parse!(@args)
end

#parse_lint_optionsvoid (private)

This method returns an undefined value.

Parse options for lint command



1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
# File 'lib/synthra/cli.rb', line 1580

def parse_lint_options
  @options[:verbose] = false
  @options[:strict] = false

  OptionParser.new do |opts|
    opts.banner = "Usage: synthra lint <path|directory>"

    opts.on("-v", "--verbose", "Show detailed output including schema names") do
      @options[:verbose] = true
    end

    opts.on("--strict", "Treat warnings as errors (exit non-zero)") do
      @options[:strict] = true
    end
  end.parse!(@args)
end

#parse_live_optionsvoid (private)

This method returns an undefined value.

Parse options for live command

:nocov:



1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
# File 'lib/synthra/cli.rb', line 1991

def parse_live_options
  @options[:port] = 4567

  OptionParser.new do |opts|
    opts.banner = "Usage: synthra live <directory> [options]"

    opts.on("-d", "--dir DIR", "Directory containing .dsl files") do |v|
      @options[:dir] = v
    end

    opts.on("-p", "--port PORT", Integer, "Server port (default: 4567)") do |v|
      @options[:port] = v
    end
  end.parse!(@args)
end

#parse_perf_optionsvoid (private)

This method returns an undefined value.

Parse options for perf command



2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
# File 'lib/synthra/cli.rb', line 2240

def parse_perf_options
  @options[:count] = 100_000
  @options[:format] = :ndjson
  @options[:benchmark] = false

  OptionParser.new do |opts|
    opts.banner = "Usage: synthra perf <SchemaName> [options]"

    opts.on("-d", "--dir DIR", "Directory containing .dsl files") do |v|
      @options[:dir] = v
    end

    opts.on("-c", "--count COUNT", Integer, "Number of records (default: 100000)") do |v|
      @options[:count] = v
    end

    opts.on("-o", "--output FILE", "Output file") do |v|
      @options[:output] = v
    end

    opts.on("-f", "--format FORMAT", "Output format: ndjson, json, csv (default: ndjson)") do |v|
      @options[:format] = v.to_sym
    end

    opts.on("-s", "--seed SEED", Integer, "Random seed") do |v|
      @options[:seed] = v
    end

    opts.on("-m", "--mode MODE", "Generation mode") do |v|
      @options[:mode] = v.to_sym
    end

    opts.on("-b", "--benchmark", "Run benchmark instead of generating") do
      @options[:benchmark] = true
    end
  end.parse!(@args)
end

#parse_preview_optionsvoid (private)

This method returns an undefined value.

Parse options for preview command



1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
# File 'lib/synthra/cli.rb', line 1602

def parse_preview_options
  OptionParser.new do |opts|
    opts.banner = "Usage: synthra preview <SchemaName> [options]"

    opts.on("-d", "--dir DIR", "Directory containing .dsl files") do |v|
      @options[:dir] = v
    end

    opts.on("-s", "--seed SEED", Integer, "Seed for deterministic generation") do |v|
      @options[:seed] = v
    end

    opts.on("-m", "--mode MODE", %i[random edge invalid mixed], "Generation mode") do |v|
      @options[:mode] = v
    end
  end.parse!(@args)
end

#parse_repl_optionsvoid (private)

This method returns an undefined value.

Parse options for repl command

:nocov:



1399
1400
1401
1402
1403
1404
1405
1406
1407
# File 'lib/synthra/cli.rb', line 1399

def parse_repl_options
  @options[:verbose] = false
  OptionParser.new do |opts|
    opts.banner = "Usage: synthra repl [options]"
    opts.on("-v", "--verbose", "Show verbose output including stack traces") do
      @options[:verbose] = true
    end
  end.parse!(@args)
end

#parse_seed_optionsvoid (private)

This method returns an undefined value.

Parse options for seed command



2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
# File 'lib/synthra/cli.rb', line 2012

def parse_seed_options
  @options[:schema_dir] = "schemas"
  @options[:count] = 10
  @options[:file] = nil

  OptionParser.new do |opts|
    opts.banner = "Usage: synthra seed [options]"

    opts.on("-d", "--dir DIR", "Directory containing .dsl files") do |v|
      @options[:schema_dir] = v
    end

    opts.on("-c", "--count COUNT", Integer, "Records per schema (default: 10)") do |v|
      @options[:count] = v
    end

    opts.on("-f", "--file FILE", "Seed file to execute") do |v|
      @options[:file] = v
    end
  end.parse!(@args)
end

#parse_server_optionsvoid (private)

This method returns an undefined value.

Parse options for server command

:nocov:



2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
# File 'lib/synthra/cli.rb', line 2160

def parse_server_options
  @options[:port] = 3000
  @options[:host] = "0.0.0.0"
  @options[:environment] = :development
  @options[:cors] = true
  @options[:rate_limit] = 0
  @options[:auth] = nil

  OptionParser.new do |opts|
    opts.banner = "Usage: synthra server <directory> [options]"

    opts.on("-d", "--dir DIR", "Directory containing .dsl files") do |v|
      @options[:dir] = v
    end

    opts.on("-p", "--port PORT", Integer, "Server port (default: 3000)") do |v|
      @options[:port] = v
    end

    opts.on("-h", "--host HOST", "Server host (default: 0.0.0.0)") do |v|
      @options[:host] = v
    end

    opts.on("-e", "--env ENV", "Environment: development, production") do |v|
      @options[:environment] = v.to_sym
    end

    opts.on("--no-cors", "Disable CORS headers") do
      @options[:cors] = false
    end

    opts.on("--rate-limit LIMIT", Integer, "Rate limit per minute (0 = unlimited)") do |v|
      @options[:rate_limit] = v
    end

    opts.on("--api-key KEY", "Require API key authentication") do |v|
      @options[:auth] = { type: :api_key, keys: [v] }
    end
  end.parse!(@args)
end

#parse_validate_optionsvoid (private)

This method returns an undefined value.

Parse options for validate command



1569
1570
1571
1572
1573
# File 'lib/synthra/cli.rb', line 1569

def parse_validate_options
  OptionParser.new do |opts|
    opts.banner = "Usage: synthra validate <path>"
  end.parse!(@args)
end

#parse_watch_optionsvoid (private)

This method returns an undefined value.

Parse options for watch command

:nocov:



1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
# File 'lib/synthra/cli.rb', line 1295

def parse_watch_options
  @options[:interval] = 1

  OptionParser.new do |opts|
    opts.banner = "Usage: synthra watch <directory>"

    opts.on("-i", "--interval SECONDS", Integer, "Check interval in seconds") do |v|
      @options[:interval] = v
    end
  end.parse!(@args)
end

#perf_commandInteger (private)

Execute the perf command

High-performance generation for millions of records.

Returns:

  • (Integer)

    exit code



2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
# File 'lib/synthra/cli.rb', line 2107

def perf_command
  parse_perf_options
  schema_name = @args.shift

  unless schema_name
    $stderr.puts "Usage: synthra perf <SchemaName> [options]"
    return EXIT_PARSE_ERROR
  end

  registry = load_registry(@options[:dir])
  unless registry.schema?(schema_name)
    $stderr.puts "Schema not found: #{schema_name}"
    return EXIT_PARSE_ERROR
  end

  schema = registry.schema(schema_name)
  count = @options[:count]

  if @options[:output]
    # Generate to file
    result = Synthra::PerformanceMode.to_file(
      schema,
      count: count,
      output: @options[:output],
      format: @options[:format],
      seed: @options[:seed],
      mode: @options[:mode]
    ) do |progress|
      print "\r#{progress[:current]} / #{progress[:total]} (#{progress[:percent]}%) - #{progress[:rate]} rec/s - ETA: #{progress[:eta]}s"
    end
    puts "\n\nGenerated #{result[:records]} records in #{result[:elapsed].round(2)}s (#{result[:rate].round(0)} rec/s)"
  elsif @options[:benchmark]
    # Benchmark mode
    puts "Benchmarking #{schema_name}..."
    results = Synthra::PerformanceMode.benchmark(schema, counts: [1000, 10_000, 100_000])
    results.each do |c, r|
      puts "  #{c.to_s.rjust(7)} records: #{r[:elapsed]}s (#{r[:rate]} rec/s, #{r[:memory_mb]} MB)"
    end
  else
    # Stream to stdout
    Synthra::PerformanceMode.stream(schema, count: count, seed: @options[:seed]).each do |record|
      puts JSON.generate(record)
    end
  end

  EXIT_SUCCESS
end

#preview_commandInteger (private)

Execute the preview command

Generates a single record for quick schema testing. Always outputs pretty-printed JSON with schema info header.

Returns:

  • (Integer)

    exit code



245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/synthra/cli.rb', line 245

def preview_command
  parse_preview_options
  schema_name = @args.shift

  unless schema_name
    $stderr.puts "Usage: synthra preview <SchemaName> [options]"
    return EXIT_PARSE_ERROR
  end

  registry = Registry.new
  load_schemas_from_dir(registry, @options[:dir])

  schema = registry.schema(schema_name)
  
  # Generate single record
  options = { registry: registry, mode: @options[:mode] }
  options[:seed] = @options[:seed] if @options[:seed]
  
  record = schema.generate(**options)

  # Output with header
  puts "# #{schema_name} (#{schema.fields.length} fields)"
  puts JSON.pretty_generate(record)
  
  EXIT_SUCCESS
rescue KeyError
  $stderr.puts "Unknown schema: #{schema_name}"
  $stderr.puts "Available schemas: #{registry.schemas.keys.join(", ")}"
  EXIT_PARSE_ERROR
end

#repl_commandObject (private)

:nocov:



405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
# File 'lib/synthra/cli.rb', line 405

def repl_command
  parse_repl_options
  require_relative "repl/formatter"
  
  puts "=" * 60
  puts "Synthra Interactive REPL (Enhanced)"
  puts "=" * 60
  puts
  puts "Commands:"
  puts "  load <path>        - Load schemas from file/directory"
  puts "  list               - List loaded schemas"
  puts "  info <Schema>      - Show schema details"
  puts "  gen <Schema>       - Generate one record (JSON)"
  puts "  gen <Schema> N     - Generate N records (JSON)"
  puts "  table <Schema>     - Generate one record (table view)"
  puts "  table <Schema> N   - Generate N records (table view)"
  puts "  debug <Schema>     - Step-through debug generation"
  puts "  inspect <Schema>   - Generate and show detailed inspection"
  puts "  seed <number>      - Set seed for deterministic output"
  puts "  mode <mode>        - Set mode (random/edge/invalid/hostile/mixed)"
  puts "  help               - Show this help"
  puts "  quit/exit          - Exit REPL"
  puts

  registry = Registry.new
  current_seed = nil
  current_mode = :random

  loop do
    print "synthra> "
    input = $stdin.gets&.strip
    break if input.nil?

    case input
    when /^load\s+(.+)$/
      path = $1.strip
      begin
        if File.directory?(path)
          load_schemas_from_dir_no_validate(registry, path)
        else
          registry.load_file(path, validate: false)
        end
        puts "✅ Loaded #{registry.size} schema(s)"
      rescue => e
        puts "❌ Error: #{e.message}"
      end

    when "list"
      if registry.size == 0
        puts "No schemas loaded. Use 'load <path>' first."
      else
        puts "Loaded schemas:"
        registry.schemas.each do |name, schema|
          version = schema.version ? " v#{schema.version}" : ""
          deprecated = schema.deprecated? ? " [DEPRECATED]" : ""
          puts "  - #{name}#{version}#{deprecated}"
        end
      end

    when /^info\s+(\w+)$/
      name = $1
      begin
        schema = registry.schema(name)
        puts "Schema: #{name}"
        puts "  Version: #{schema.version || 'not specified'}"
        puts "  Fields: #{schema.fields.length}"
        schema.fields.each do |f|
          optional = f.optional? ? "?" : ""
          nullable = f.nullable? ? " (nullable)" : ""
          puts "    - #{f.name}#{optional}: #{f.type_name}#{nullable}"
        end
      rescue KeyError
        puts "❌ Schema '#{name}' not found"
      end

    when /^gen\s+(\w+)(?:\s+(\d+))?$/
      name = $1
      count = ($2 || "1").to_i
      begin
        schema = registry.schema(name)
        opts = { registry: registry, mode: current_mode }
        opts[:seed] = current_seed if current_seed
        
        if count == 1
          record = schema.generate(**opts)
          puts JSON.pretty_generate(record)
        else
          records = schema.generate_many(count, **opts)
          puts JSON.pretty_generate(records)
        end
      rescue KeyError
        puts "❌ Schema '#{name}' not found"
      rescue => e
        puts "❌ Error: #{e.message}"
        puts e.backtrace.first(3).join("\n") if @options[:verbose]
      end

    when /^table\s+(\w+)(?:\s+(\d+))?$/
      name = $1
      count = ($2 || "1").to_i
      begin
        schema = registry.schema(name)
        opts = { registry: registry, mode: current_mode }
        opts[:seed] = current_seed if current_seed
        
        if count == 1
          record = schema.generate(**opts)
          puts REPL::Formatter.table(record)
        else
          records = schema.generate_many(count, **opts)
          puts REPL::Formatter.table(records)
        end
      rescue KeyError
        puts "❌ Schema '#{name}' not found"
      rescue => e
        puts "❌ Error: #{e.message}"
      end

    when /^debug\s+(\w+)$/
      name = $1
      begin
        schema = registry.schema(name)
        debug_generation(schema, registry, current_mode, current_seed)
      rescue KeyError
        puts "❌ Schema '#{name}' not found"
      rescue => e
        puts "❌ Error: #{e.message}"
      end

    when /^inspect\s+(\w+)$/
      name = $1
      begin
        schema = registry.schema(name)
        opts = { registry: registry, mode: current_mode }
        opts[:seed] = current_seed if current_seed
        
        record = schema.generate(**opts)
        puts "📊 Generated Record:"
        puts "=" * 60
        puts REPL::Formatter.table(record)
        puts
        puts "📋 JSON Format:"
        puts JSON.pretty_generate(record)
        puts
        puts "📏 Statistics:"
        puts "  Fields: #{record.keys.length}"
        puts "  Total size: #{JSON.generate(record).bytesize} bytes"
        record.each do |key, value|
          type = value.class.name
          size = case value
                 when String then value.bytesize
                 when Hash then "#{value.keys.length} keys"
                 when Array then "#{value.length} items"
                 else value.inspect.length
                 end
          puts "    #{key}: #{type} (#{size})"
        end
      rescue KeyError
        puts "❌ Schema '#{name}' not found"
      rescue => e
        puts "❌ Error: #{e.message}"
      end

    when /^seed\s+(\d+)$/
      current_seed = $1.to_i
      puts "✅ Seed set to #{current_seed}"

    when /^mode\s+(random|edge|invalid|hostile|mixed)$/
      current_mode = $1.to_sym
      puts "✅ Mode set to #{current_mode}"

    when "help"
      puts "Commands: load, list, info, gen, table, debug, inspect, seed, mode, help, quit"

    when "quit", "exit"
      puts "Goodbye!"
      break

    when ""
      # Ignore empty input

    else
      puts "Unknown command. Type 'help' for available commands."
    end
  end

  EXIT_SUCCESS
end

#runInteger

Run the CLI command

Parses the command and options, executes the appropriate command, and returns an exit code.

Examples:

exit_code = cli.run
exit(exit_code)

Returns:

  • (Integer)

    exit code (0 = success, 1 = parse error, 2 = runtime error)



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/synthra/cli.rb', line 90

def run
  return show_help if @args.empty?

  command = @args.shift
  case command
  when "validate"
    validate_command
  when "lint"
    lint_command
  when "preview"
    preview_command
  when "generate"
    generate_command
  when "info"
    info_command
  when "diff"
    diff_command
  when "repl"
    repl_command  # :nocov:
  when "export"
    export_command
  when "graph"
    graph_command
  when "watch"
    watch_command  # :nocov:
  when "lsp"
    lsp_command
  when "import"
    import_command
  when "docs"
    docs_command
  when "live"
    live_command  # :nocov:
  when "seed"
    seed_command
  when "server"
    server_command  # :nocov:
  when "contracts"
    contracts_command
  when "perf"
    perf_command
  when "--help", "-h", "help"
    show_help
  when "--version", "-v"
    show_version
  else
    $stderr.puts "Unknown command: #{command}"
    $stderr.puts "Run 'synthra --help' for usage information."
    EXIT_PARSE_ERROR
  end
rescue Synthra::Errors::ParseError => e
  $stderr.puts "Parse error: #{e.message}"
  EXIT_PARSE_ERROR
rescue Synthra::Errors::Error => e
  $stderr.puts "Error: #{e.message}"
  EXIT_RUNTIME_ERROR
rescue Errno::ENOENT => e
  $stderr.puts "File not found: #{e.message}"
  EXIT_PARSE_ERROR
end

#sanitize_for_json(value) ⇒ Object (private)

Sanitize values for JSON serialization Converts Infinity/NaN to null for valid JSON output

Parameters:

  • value (Object)

    value to sanitize

Returns:

  • (Object)

    sanitized value



1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
# File 'lib/synthra/cli.rb', line 1547

def sanitize_for_json(value)
  case value
  when Float
    if value.infinite? || value.nan?
      nil
    else
      value
    end
  when Hash
    value.transform_values { |v| sanitize_for_json(v) }
  when Array
    value.map { |v| sanitize_for_json(v) }
  else
    value
  end
end

#seed_commandInteger (private)

Execute the seed command

Seeds databases with generated data.

Returns:

  • (Integer)

    exit code



1939
1940
1941
1942
1943
# File 'lib/synthra/cli.rb', line 1939

def seed_command
  parse_seed_options
  cmd = CLI::Commands::Seed.new
  cmd.call(@options)
end

#server_commandInteger (private)

Execute the server command

Starts the production API server for data generation.

:nocov:

Returns:

  • (Integer)

    exit code



2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
# File 'lib/synthra/cli.rb', line 2041

def server_command
  parse_server_options
  path = @args.shift || @options[:dir]

  unless path && File.directory?(path)
    $stderr.puts "Usage: synthra server <schema-directory> [options]"
    return EXIT_PARSE_ERROR
  end

  puts "Starting Synthra API Server..."
  Synthra::APIServer.start(
    schema_dir: path,
    port: @options[:port],
    host: @options[:host],
    environment: @options[:environment],
    cors: @options[:cors],
    rate_limit: @options[:rate_limit],
    auth: @options[:auth]
  )
  EXIT_SUCCESS
rescue Interrupt
  puts "\nServer stopped."
  EXIT_SUCCESS
end

#show_helpInteger (private)

Display help information

Returns:

  • (Integer)

    EXIT_SUCCESS



1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
# File 'lib/synthra/cli.rb', line 1664

def show_help
  puts <<~HELP
    Synthra - Generate fake data from a human-friendly DSL

    Usage:
      synthra <command> [options]

    Commands:
      validate <path>         Validate a DSL schema file (syntax check)
      lint <path|dir>         Lint schemas for semantic errors (copy paths, etc.)
      preview <SchemaName>    Preview a single record (quick testing)
      generate <SchemaName>   Generate fake data for a schema
      info <path|dir>         Show schema information (version, fields, etc.)
      diff <path1> <path2>    Compare two schema files/directories
      export <SchemaName>     Export schema as JSON Schema or TypeScript
      graph <path|dir>        Generate GraphViz visualization of schemas
      watch <directory>       Watch directory for changes and validate
      repl                    Start interactive REPL for testing
      lsp                     Start Language Server Protocol server
      import <file>           Import schemas from OpenAPI/Swagger
      docs <directory>        Generate HTML documentation
      live <directory>        Start live preview server with web UI
      seed                    Seed database with generated data
      server                  Start production API server
      contracts               Manage data contracts registry
      perf                    High-performance generation mode

    Lint Options:
      -v, --verbose           Show detailed output including schema names
      --strict                Treat warnings as errors (exit non-zero)

    Preview Options:
      -d, --dir DIR           Directory containing .dsl files (default: .)
      -s, --seed SEED         Seed for deterministic generation
      -m, --mode MODE         Generation mode: random, edge, invalid, mixed

    Generate Options:
      -d, --dir DIR           Directory containing .dsl files (default: .)
      -c, --count COUNT       Number of records to generate (default: 1)
      -s, --seed SEED         Seed for deterministic generation
      -m, --mode MODE         Generation mode: random, edge, invalid, mixed
      --json                  Output as JSON array (default)
      --ndjson                Output as newline-delimited JSON
      --pretty                Pretty-print JSON output

    Export Options:
      -d, --dir DIR           Directory containing .dsl files
      -f, --format FORMAT     Export format (see formats below)
      -o, --output FILE       Output file (default: stdout)
      --out-dir DIR           Output directory (auto-generates filename)
      --auto-file             Auto-generate filename based on schema and format
      -c, --count COUNT       Number of records for data exports (default: 10)
      -s, --seed SEED         Seed for deterministic generation
      -a, --all               Export all schemas (typescript/javascript/python/sql)
      --dialect DIALECT       SQL dialect: postgresql, mysql, sqlite
      --style STYLE           Python style: pydantic, dataclass, typed_dict
      --batch                 Use batch INSERT for sql-insert format
      --compact               No pretty-printing for JSON output
      --envelope KEY          Wrap JSON in envelope with this key
      --root NAME             Root element name for XML
      --item NAME             Item element name for XML

    Schema Export Formats (types/structure):
      json-schema             JSON Schema (draft 2020-12)
      typescript, ts          TypeScript interfaces
      javascript, js          JavaScript with JSDoc types
      python, py, pydantic    Python Pydantic models
      dataclass               Python dataclasses
      sql, sql-ddl            SQL CREATE TABLE (use --dialect)
      graphviz, dot           GraphViz DOT diagram
      protobuf, proto         Protocol Buffers / gRPC
      openapi, swagger        OpenAPI 3.0 specification

    Data Export Formats (generated records):
      json, json-data         JSON array of records
      csv                     CSV with headers
      sql-insert, insert      SQL INSERT statements (use --dialect)
      yaml, yml               YAML format
      xml                     XML format (use --root, --item)

    Graph Options:
      -d, --dir DIR           Directory containing .dsl files
      -o, --output FILE       Output DOT file
      -r, --render            Also render image (requires graphviz installed)
      --format FORMAT         Render format: png, svg, pdf (default: png)
      --direction DIR         Graph direction: LR, TB, BT, RL (default: LR)
      -g, --group             Group schemas by category

    Watch Options:
      -i, --interval SECONDS  Check interval in seconds (default: 1)

    Info Options:
      -v, --verbose           Show field details

    Server Options (Production API):
      -d, --dir DIR           Directory containing .dsl files
      -p, --port PORT         Server port (default: 3000)
      -h, --host HOST         Server host (default: 0.0.0.0)
      -e, --env ENV           Environment: development, production
      --no-cors               Disable CORS headers
      --rate-limit LIMIT      Rate limit per minute (0 = unlimited)
      --api-key KEY           Require API key authentication

    Contracts Options:
      -d, --dir DIR           Contracts directory (default: contracts)
      -s, --schema-dir DIR    Schema directory
      -v, --version VERSION   Version (semver)
      -m, --message MSG       Changelog message
      --sunset DATE           Sunset date for deprecation

    Performance Mode Options:
      -d, --dir DIR           Directory containing .dsl files
      -c, --count COUNT       Number of records (default: 100000)
      -o, --output FILE       Output file (for file generation)
      -f, --format FORMAT     Output format: ndjson, json, csv
      -s, --seed SEED         Random seed for deterministic generation
      -b, --benchmark         Run benchmark instead of generating

    Examples:
      synthra validate schemas/user.dsl
      synthra lint schemas/
      synthra lint schemas/ --strict
      synthra preview User --dir schemas
      synthra generate User --dir schemas --count 10
      synthra generate User --dir schemas --count 1000 --ndjson
      
      # Schema exports (types/structure)
      synthra export User -d schemas -f json-schema
      synthra export User -d schemas -f typescript -o user.ts
      synthra export User -d schemas -f javascript -o user.js
      synthra export User -d schemas -f python --style pydantic
      synthra export User -d schemas -f dataclass -o models.py
      synthra export User -d schemas -f sql --dialect mysql
      synthra export --all -d schemas -f typescript -o types.ts
      synthra export --all -d schemas -f python -o models.py
      
      # Auto-generate files (--auto-file or --out-dir)
      synthra export User -d schemas -f typescript --auto-file
      synthra export User -d schemas -f python --out-dir ./generated
      synthra export User -d schemas -f csv -c 100 --out-dir ./data
      
      # Data exports (generated records)
      synthra export User -d schemas -f json -c 100 -o users.json
      synthra export User -d schemas -f csv -c 1000 -o users.csv
      synthra export User -d schemas -f sql-insert -c 100 --dialect postgresql
      synthra export User -d schemas -f sql-insert -c 1000 --batch
      synthra export User -d schemas -f yaml -c 50 -o users.yaml
      synthra export User -d schemas -f xml -c 20 --root users --item user
      
      # GraphViz visualization
      synthra graph schemas/ --output schema.dot --render
      synthra graph schemas/ -o schema.dot --render --format svg
      
      # Other commands
      synthra watch schemas/
      synthra info schemas/ --verbose
      synthra diff schemas/v1/ schemas/v2/
      synthra repl
      synthra lsp
      
      # Production API Server
      synthra server schemas/ --port 3000
      synthra server schemas/ --port 8080 --env production
      synthra server schemas/ --api-key secret123 --rate-limit 100
      
      # Data Contracts Registry
      synthra contracts publish User -v 1.0.0 -s schemas/
      synthra contracts deprecate User -v 1.0.0 --sunset 2026-06-01
      synthra contracts list
      synthra contracts history User
      synthra contracts diff User 1.0.0 2.0.0
      
      # High-Performance Mode (millions of records)
      synthra perf User -d schemas/ -c 1000000 -o users.ndjson
      synthra perf User -d schemas/ -c 100000 -f csv -o users.csv
      synthra perf User -d schemas/ --benchmark
      
      # Protocol Buffers / gRPC export
      synthra export User -d schemas/ -f protobuf -o user.proto
      synthra export --all -d schemas/ -f protobuf -o all.proto
      
      # OpenAPI export
      synthra export --all -d schemas/ -f openapi -o api.yaml

    Exit Codes:
      0  Success
      1  Parse/validation error
      2  Runtime error
  HELP
  EXIT_SUCCESS
end

#show_versionInteger (private)

Display version information

Returns:

  • (Integer)

    EXIT_SUCCESS



2466
2467
2468
2469
# File 'lib/synthra/cli.rb', line 2466

def show_version
  puts "Synthra version #{Synthra::VERSION}"
  EXIT_SUCCESS
end

#validate_and_report(path) ⇒ void (private)

This method returns an undefined value.

Validate a file and report results

:nocov:

Parameters:

  • path (String)

    file path



1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
# File 'lib/synthra/cli.rb', line 1271

def validate_and_report(path)
  registry = Registry.new
  begin
    registry.load_file(path)
    errors = registry.validate_paths
    
    if errors.empty?
      puts "   ✓ Valid"
    else
      puts "#{errors.length} error(s)"
      errors.each { |e| puts "     - #{e.message}" }
    end
  rescue Synthra::Errors::ParseError => e
    puts "   ✗ Parse error: #{e.message}"
  end
  puts
end

#validate_commandInteger (private)

Execute the validate command

Loads and parses a DSL file to check for syntax errors. Prints success message or error details.

Returns:

  • (Integer)

    exit code



161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/synthra/cli.rb', line 161

def validate_command
  parse_validate_options
  path = @args.shift

  unless path
    $stderr.puts "Usage: synthra validate <path>"
    return EXIT_PARSE_ERROR
  end

  registry = Registry.new
  registry.load_file(path)
  puts "Schema valid: #{path}"
  EXIT_SUCCESS
end

#watch_commandObject (private)

:nocov:



1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
# File 'lib/synthra/cli.rb', line 1214

def watch_command
  parse_watch_options
  path = @args.shift || @options[:dir]

  unless File.directory?(path)
    $stderr.puts "Watch requires a directory path"
    return EXIT_PARSE_ERROR
  end

  puts "👁  Watching #{path} for changes..."
  puts "   Press Ctrl+C to stop"
  puts

  # Track file modification times
  mtimes = {}
  Dir.glob(File.join(path, "*.dsl")).each do |f|
    mtimes[f] = File.mtime(f)
  end

  loop do
    sleep(@options[:interval] || 1)

    # Check for new or modified files
    current_files = Dir.glob(File.join(path, "*.dsl"))
    
    current_files.each do |f|
      current_mtime = File.mtime(f)
      
      if mtimes[f].nil?
        puts "➕ New file: #{File.basename(f)}"
        validate_and_report(f)
        mtimes[f] = current_mtime
      elsif mtimes[f] < current_mtime
        puts "📝 Changed: #{File.basename(f)}"
        validate_and_report(f)
        mtimes[f] = current_mtime
      end
    end

    # Check for deleted files
    (mtimes.keys - current_files).each do |f|
      puts "➖ Deleted: #{File.basename(f)}"
      mtimes.delete(f)
    end
  end
rescue Interrupt
  puts "\n👋 Stopped watching"
  EXIT_SUCCESS
end