Skir

repository·main·Indexed 43 days ago

https://github.com/gepheum/skir

A modern declarative schema language for data models and APIs, serving as a high-level alternative to Protocol Buffers. Skir allows developers to define data structures in .skir files and generate type-safe, immutable code for languages including TypeScript, Python, Java, Go, C++, Rust, and Swift. Features include sum types, key-indexed arrays, multiple serialization modes (JSON and binary), and SkirRPC for lightweight HTTP RPC implementation.

Tokens
3.3K
Snippets
8
Records
12
Agent score
56%

What's inside Skir

  1. Overview of Skir

    main
    Skir is a modern declarative schema language designed for data models and APIs. It allows you to write a single .skir schema file and automatically generate clean, idiomatic, and type-safe code for multiple languages including TypeScript, Python, Java, Go, C++, and more. It is designed to replace or reimagine the role of Protocol Buffers in modern development workflows.
  2. Skir Syntax and Features

    main

    Skir uses a declarative syntax to define enums, structs, constants, and methods. Key language features include:

    • Sum Types (Polymorphism): Enums can act as sum types where variants are either constants or carry typed payloads.
    • Immutability: Generated types are deeply immutable, requiring all fields to be provided at construction time.
    • Key-indexed Arrays: You can declare arrays with specific keys (e.g., [User|user_id]) to enable fast key-based lookup APIs.
    • Serialization Modes: Supports dense JSON (for APIs/DBs), readable JSON (for debugging), and binary (for performance).
    • Schema Evolution: Built-in rules to ensure backward and forward compatibility.
    • SkirRPC: A lightweight HTTP RPC implementation with a built-in Studio app for testing methods.
    // robot.skir
    
    enum RobotAction {
      wave;
      say: string;
      move: Point;
    }
    
    struct Point {
      x: int32;
      y: int32;
    }
    
    const GREET_ACTIONS: [RobotAction] = [
      "wave",
      {
        kind: "say",
        value: "Hi!",
      },
    ];
    
    method PerformAction(RobotAction): bool = 12345;
  3. Understand SkirConfigResult and error reporting

    main

    The result of a configuration parse is a SkirConfigResult object. It provides feedback on whether the configuration is valid and where errors occurred.

    SkirConfigResult Structure

    • skirConfig: The validated SkirConfig object. It is undefined if any errors were found.
    • errors: An array of SkirConfigError objects.
    • maybeForgotToEditAfterInit: A boolean that is true if the user has a generators: null configuration, suggesting they ran npx skir init but haven't configured their generators yet.

    SkirConfigError Structure

    Errors provide precise locations in the source YAML to help with debugging:

    • message: A descriptive error message. If a property is missing, the message is prefixed with Missing property '...'.
    • range: An optional SkirConfigErrorRange indicating the start and end positions of the error.

    SkirConfigErrorPos

    • offset: 0-based character offset.
    • lineNumber: 1-based line number.
    • colNumber: 1-based column number.
  4. Understand the Package and Dependency data structures

    main

    When working with skir, packages and their dependency trees are represented by specific types.

    • A Package contains its unique packageId, its version, a map of modules (where keys are module paths and values are content/hashes), and a dependencies object.
    • dependencies is a PackageIdToVersion map, which is a dictionary where keys are package IDs and values are version strings.
    • Dependency resolution results are returned as a DependenciesResult union, which is either a success object containing the packages and a changed boolean (indicating if the dependency tree differs from the previous state), or a DependencyError.
  5. Using generated Skir code in Python

    main

    Once a .skir file is compiled, you can import the generated classes and use their built-in serializers for round-trip JSON operations. The generated code provides helper methods (like wrap_<variant>) to construct sum type variants and access their underlying data.

    # my_project.py
    
    from skirout.robot_skir import Point, RobotAction
    
    wave = RobotAction.WAVE
    say_hi = RobotAction.wrap_say("Hi!")
    move_origin = RobotAction.wrap_move(Point(x=0, y=0))
    
    # Round-trip serialization to JSON
    action_json = RobotAction.serializer.to_json(say_hi)
    restored = RobotAction.serializer.from_json(action_json)
    
    assert restored == say_hi
    assert move_origin.union.kind == "move"
  6. Supported Languages for Code Generation

    main

    Skir supports code generation for a wide variety of languages. For specific implementation details and examples, refer to the documentation for each language:

    • Web/Frontend: TypeScript
    • Backend/Systems: Python, C++, Java, C#, Kotlin, Rust, Go, Zig
    • Mobile/Other: Dart, Swift, Gleam, MoonBit
  7. Use parseCommandLine to parse arguments

    main

    The parseCommandLine function takes an array of command-line arguments (typically process.argv.slice(2)) and returns a structured ParsedArgs object. This object uses a discriminated union (kind) to identify the command and its associated subcommands or options.

    Return Types (ParsedArgs):

    • { kind: "gen", subcommand?: "watch", root?: string }
    • { kind: "format", subcommand?: "ci", root?: string }
    • { kind: "snapshot", subcommand?: "ci" | "view" | "dry-run", root?: string }
    • { kind: "init", root?: string }
    • { kind: "help" }
    • { kind: "version" }
    • { kind: "error" }
    import { parseCommandLine } from './command_line_parser';
    
    const args = process.argv.slice(2);
    const parsed = parseCommandLine(args);
    
    if (parsed.kind === 'gen') {
      console.log('Generating code at root:', parsed.root);
      if (parsed.subcommand === 'watch') {
        console.log('Watching for changes...');
      }
    }
  8. Parse Skir configuration with dynamic imports

    main

    Use parseSkirConfigWithDynamicImports to parse a YAML string when you need to support generators that are not part of the static built-in list. This function is asynchronous because it dynamically imports generator modules via importCodeGenerator.

    This is the preferred method if you are using custom or third-party generator modules that must be loaded at runtime.

    import { parseSkirConfigWithDynamicImports } from "skir";
    
    const yamlCode = `
    generators:
      - mod: "some-custom-generator-package"
        config: { key: "value" }
    `;
    
    async function run() {
      const result = await parseSkirConfigWithDynamicImports(yamlCode);
      if (result.errors.length > 0) {
        console.error("Config errors:", result.errors);
      } else {
        console.log("Parsed config:", result.skirConfig);
      }
    }
    
    run();
  9. Parse Skir configuration synchronously

    main

    Use parseSkirConfig to parse a YAML string into a SkirConfig object. This is a synchronous function that uses a static list of built-in generators. It is suitable when you only need to support the standard generators included in the package.

    If the parsing is successful, skirConfig will be defined and errors will be empty. If there are errors (YAML syntax errors, schema validation errors, or missing generator modules), skirConfig will be undefined and the errors array will contain detailed error information including line and column numbers.

    import { parseSkirConfig } from "skir";
    
    const yamlCode = `
    generators:
      - mod: "skir-typescript-gen"
        config: {}
    `;
    
    const result = parseSkirConfig(yamlCode);
    
    if (result.errors.length > 0) {
      console.error("Config errors:", result.errors);
    } else {
      console.log("Parsed config:", result.skirConfig);
    }
  10. Skir CLI Commands and Options

    main

    The Skir CLI (run via npx skir) provides several commands for managing code generation, formatting, and snapshots.

    Commands

    • init: Initialize a new Skir project in the current directory.
    • gen: Generate code from .skir files to target languages.
    • format: Format all .skir files in the source directory.
    • snapshot: Take a snapshot of the source directory and look for breaking changes since the last snapshot.
    • help: Display the help message.

    Global Options

    • --root, -r <path>: Path to the directory containing the skir.yml file.

    Command-Specific Options

    • gen
      • --watch, -w: Automatically run code generation when .skir files change.
    • format
      • --ci: Fail if code is not properly formatted.
    • snapshot
      • --ci: Fail if code has changed since the last snapshot.
      • --dry-run: Look for breaking changes since the last snapshot without taking a new snapshot.
      • --view: Display the last snapshot.
    Usage: npx skir <command> [options]
    
    Commands:
      init               Initialize a new Skir project in the current directory
      gen                Generate code from .skir files to target languages
      format             Format all .skir files in the source directory
      snapshot           Take a snapshot of the source directory, look for
                           breaking changes since the last snapshot
      help               Display this help message
    
    Options:
      --root, -r <path>  Path to the directory containing the skir.yml file
      --watch, -w        Automatically run code generation when .skir files change
                           (gen only)
      --ci               Fail if code is not properly formatted (format) or if code
                           has changed since the last snapshot (snapshot)
      --dry-run          Look for breaking changes since the last snapshot without
                           taking a new snapshot (snapshot only)
      --view             Display the last snapshot (snapshot only)
  11. Implement a custom PackageDownloader

    main

    If you are extending skir or mocking its behavior, you can implement the PackageDownloader type. This function is responsible for fetching a package given its ID, version, and an optional GitHub token.

    It must return a Promise that resolves to a DownloadPackageResult, which is either a success containing the Package or an error containing a message.

    type PackageDownloader = (
      packageId: string,
      version: string,
      githubToken: string | undefined,
    ) => Promise<DownloadPackageResult>;
  12. Handle CommandLineParseError

    main

    When parsing command-line arguments, the parser may throw a CommandLineParseError if invalid commands, unknown options, or conflicting options (e.g., using --ci and --dry-run together in snapshot) are provided. You should catch this error to handle user input mistakes gracefully.

    import { parseCommandLine, CommandLineParseError } from './command_line_parser';
    
    try {
      const args = process.argv.slice(2);
      const parsed = parseCommandLine(args);
      // ... handle parsed args
    } catch (error) {
      if (error instanceof CommandLineParseError) {
        // Handle the error (e.g., print to stderr and exit)
        console.error(`Invalid usage: ${error.message}`);
        process.exit(1);
      }
      throw error;
    }