[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\n\n[*.md]\ntrim_trailing_whitespace = false\n\n[*.go]\nindent_size = 4\n"
  },
  {
    "path": ".github/workflows/build_and_test.yml",
    "content": "---\nname: build and test\n\non: push\n\njobs:\n  goreleaser:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n        with:\n          fetch-depth: 0\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4\n      - name: Set up Go\n        uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6\n        with:\n          go-version: ^1.21\n      - name: Run GoReleaser\n        uses: goreleaser/goreleaser-action@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8 # v7\n        with:\n          distribution: goreleaser\n          version: latest\n          args: release --snapshot --clean\n      - name: Upload result\n        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7\n        with:\n          name: helm-schema-tarball\n          path: dist/helm-schema_*-next_Linux_x86_64.tar.gz\n  test:\n    runs-on: ubuntu-latest\n    needs: goreleaser\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n        with:\n          fetch-depth: 0\n      - name: Download helm-schema\n        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8\n        with:\n          name: helm-schema-tarball\n          path: .\n      - name: Install jq\n        run: sudo apt-get update && sudo apt-get install -y jq\n      - shell: bash\n        run: |-\n          tar xf helm-schema_*-next_Linux_x86_64.tar.gz -C tests helm-schema\n          cd tests && ./run.sh\n  test-sign-plugin-script:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n      - name: Set up Helm\n        uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5\n      - name: Test plugin signing\n        run: ./tests/test-sign-plugin.sh\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "---\nname: release\n\non:\n  push:\n    tags:\n      - \"*\"\n\npermissions:\n  packages: write\n  contents: write\n\njobs:\n  goreleaser:\n    runs-on: ubuntu-latest\n    env:\n      HAS_GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY != '' }}\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n        with:\n          fetch-depth: 0\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4\n      - name: Set up Go\n        uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6\n        with:\n          go-version: ^1.21\n      - name: Generate release notes\n        uses: orhun/git-cliff-action@f50e11560dce63f7c33227798f90b924471a88b5 # v4\n        with:\n          config: cliff.toml\n          args: --latest --strip header --verbose\n        env:\n          OUTPUT: RELEASE_NOTES.md\n          GITHUB_REPO: ${{ github.repository }}\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      - name: Import GPG key for plugin signing\n        id: import_gpg\n        if: ${{ env.HAS_GPG_KEY == 'true' }}\n        uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7\n        with:\n          gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}\n          passphrase: ${{ secrets.GPG_PASSPHRASE }}\n      - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4\n        with:\n          registry: ghcr.io\n          username: ${{ github.repository_owner }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n      - name: Run GoReleaser\n        uses: goreleaser/goreleaser-action@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8 # v7\n        with:\n          distribution: goreleaser\n          version: latest\n          args: release --clean\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint || '' }}\n      - name: Sign plugin tarballs for Helm v4 verification\n        if: ${{ env.HAS_GPG_KEY == 'true' }}\n        env:\n          GPG_SIGNING_KEY: ${{ steps.import_gpg.outputs.fingerprint }}\n          GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}\n        run: |\n          echo \"Signing plugin tarballs with GPG for Helm v4 verification...\"\n          chmod +x sign-plugin.sh\n\n          # Get version from tag\n          VERSION=\"${GITHUB_REF#refs/tags/}\"\n          VERSION=\"${VERSION#v}\"  # Remove 'v' prefix if present\n\n          # Sign all tar.gz files (plugin packages)\n          for tarball in dist/*.tar.gz; do\n            if [ -f \"$tarball\" ]; then\n              echo \"Signing: $tarball\"\n              ./sign-plugin.sh \"$VERSION\" \"$tarball\" \"$GPG_SIGNING_KEY\" || {\n                echo \"Warning: Failed to sign $tarball\"\n              }\n            fi\n          done\n\n          # List created .prov files\n          echo \"Created provenance files:\"\n          ls -lh dist/*.prov || echo \"No .prov files created\"\n      - name: Set up Helm v4\n        uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5\n      - name: Test signed plugin tarballs with Helm v4\n        if: ${{ env.HAS_GPG_KEY == 'true' }}\n        run: |\n          echo \"Testing signed plugin tarballs with Helm...\"\n\n          # Create keyring from the public key in the repo\n          KEYRING=$(mktemp)\n          gpg --dearmor < signing-key.asc > \"$KEYRING\"\n\n          FAILED=0\n          for prov_file in dist/*.prov; do\n            if [ -f \"$prov_file\" ]; then\n              tarball=\"${prov_file%.prov}\"\n              if [ -f \"$tarball\" ]; then\n                echo \"Verifying: $tarball\"\n                helm plugin verify \"$tarball\" --keyring \"$KEYRING\" || {\n                  echo \"FAIL: Verification failed for $tarball\"\n                  cat \"$prov_file\"\n                  FAILED=1\n                }\n              fi\n            fi\n          done\n\n          if [ \"$FAILED\" -eq 1 ]; then\n            exit 1\n          fi\n      - name: Update GitHub release notes\n        uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3\n        with:\n          body_path: RELEASE_NOTES.md\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      - name: Upload provenance files to release\n        if: ${{ env.HAS_GPG_KEY == 'true' }}\n        uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3\n        with:\n          files: dist/*.prov\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "./values.yaml\n./values.schema.json\n\ndist/\nRELEASE_NOTES.md\n.vscode\n\n/helm-schema\n/tests/helm-schema\n"
  },
  {
    "path": ".goreleaser.yaml",
    "content": "---\n# yaml-language-server: $schema=https://goreleaser.com/static/schema.json\n# vim: set ts=2 sw=2 tw=0 fo=cnqoj\n\nversion: 2\n\nbefore:\n  hooks:\n    - go mod tidy\n    - go test ./...\n\nbuilds:\n  - main: ./cmd/helm-schema\n    env:\n      - CGO_ENABLED=0\n    goarch:\n      - amd64\n      - arm\n      - arm64\n    goarm:\n      - \"6\"\n      - \"7\"\n    goos:\n      - linux\n      - windows\n      - darwin\n\narchives:\n  - formats: tar.gz\n    # this name template makes the OS and Arch compatible with the results of uname.\n    name_template: >-\n      {{ .ProjectName }}_\n      {{- .Version }}_\n      {{- title .Os }}_\n      {{- if eq .Arch \"amd64\" }}x86_64 {{- else if eq .Arch \"386\" }}i386 {{- else }}{{ .Arch }}{{ end }}\n      {{- if .Arm }}v{{ .Arm }}{{ end }}\n    # use zip for windows archives\n    format_overrides:\n      - goos: windows\n        formats: zip\n    # Include plugin files for Helm plugin installation\n    files:\n      - plugin.yaml\n      - install-binary.sh\n      - README.md\n      - LICENSE\n\nchecksum:\n  name_template: checksums.txt\n\n# Sign artifacts for Helm v4 plugin verification\n# The actual .prov file creation happens in GitHub Actions workflow\n# after goreleaser completes, using sign-plugin.sh script\n# Checksum signing is optional and only happens when GPG_FINGERPRINT is set\nsigns:\n  - cmd: sh\n    args:\n      - -c\n      - |\n        if [ -n \"${GPG_FINGERPRINT}\" ]; then\n          gpg --batch --local-user \"${GPG_FINGERPRINT}\" --output \"${signature}\" --detach-sign \"${artifact}\"\n        else\n          echo \"Skipping signing: GPG_FINGERPRINT not set\"\n          touch \"${signature}\"\n        fi\n    signature: \"${artifact}.sig\"\n    artifacts: checksum\n\nsnapshot:\n  version_template: \"{{ .Tag }}-next\"\n\ndockers_v2:\n  - images:\n      - \"ghcr.io/dadav/helm-schema\"\n    tags:\n      - \"v{{ .Version }}\"\n      - \"{{ if .IsNightly }}nightly{{ end }}\"\n      - \"{{ if not .IsNightly }}latest{{ end }}\"\n    labels:\n      \"org.opencontainers.image.created\": \"{{.Date}}\"\n      \"org.opencontainers.image.authors\": dadav\n      \"org.opencontainers.image.url\": \"{{.GitURL}}\"\n      \"org.opencontainers.image.title\": \"{{.ProjectName}}\"\n      \"org.opencontainers.image.revision\": \"{{.FullCommit}}\"\n      \"org.opencontainers.image.version\": \"{{.Version}}\"\n\nchangelog:\n  sort: asc\n  filters:\n    include:\n      - \"^feat:\"\n      - \"^fix:\"\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "---\nrepos:\n  - repo: https://github.com/dadav/helm-schema\n    rev: 0.22.0\n    hooks:\n      - id: helm-schema\n        # for all available options: helm-schema -h\n        args:\n          # directory to search recursively within for charts\n          - --chart-search-root=.\n\n          # don't analyze dependencies\n          - --no-dependencies\n\n          # add references to values file if not exist\n          - --add-schema-reference\n\n          # list of fields to skip from being created by default\n          # e.g. generate a relatively permissive schema\n          # - \"--skip-auto-generation=required,additionalProperties\"\n"
  },
  {
    "path": ".pre-commit-hooks.yaml",
    "content": "---\n- id: helm-schema\n  description: Uses helm-schema to create a jsonschema.\n  entry: helm-schema\n  files: (Chart|values)\\.yaml$\n  language: golang\n  name: Generate jsonschema\n  require_serial: true\n- id: helm-schema-system\n  description: Uses pre-installed helm-schema to create a jsonschema.\n  entry: helm-schema\n  files: (Chart|values)\\.yaml$\n  language: system\n  name: Generate jsonschema\n  require_serial: true\n- id: helm-schema-container\n  args: []\n  description: Uses the container image of 'helm-schema' to create schema from the Helm chart's 'values.yaml' file, and inserts the result into a corresponding 'values.schema.json' file.\n  entry: ghcr.io/dadav/helm-schema:latest\n  files: values.yaml\n  language: docker_image\n  name: Helm Schema Container\n  require_serial: true\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project Overview\n\n`helm-schema` is a Go tool that generates JSON Schema (Draft 7) files for Helm chart values. It traverses directories to find `Chart.yaml` files, reads associated `values.yaml` files, parses special `@schema` annotations in comments, and generates `values.schema.json` files that enable IDE auto-completion and validation for Helm values.\n\n## Build and Test Commands\n\n### Build\n\n```bash\n# Build the binary\ngo build -o helm-schema ./cmd/helm-schema\n\n# Build with goreleaser (for releases)\ngoreleaser release --snapshot --clean\n```\n\n### Test\n\n```bash\n# Run all tests\ngo test ./...\n\n# Run tests for a specific package\ngo test ./pkg/schema\ngo test ./pkg/chart\n\n# Run a specific test\ngo test ./pkg/schema -run TestTopoSort\n\n# Run tests with verbose output\ngo test -v ./...\n\n# Integration tests (requires helm-schema binary in tests/)\ncd tests && ./run.sh\n```\n\n### Linting and Formatting\n\n```bash\n# Format code\ngo fmt ./...\n\n# Tidy dependencies\ngo mod tidy\n```\n\n## Code Architecture\n\n### High-Level Flow\n\n1. **Chart Discovery** (`pkg/chart/searching/`): Recursively searches for `Chart.yaml` files starting from a root directory. Also extracts `.tgz` chart archives if found.\n\n2. **Parallel Processing** (`cmd/helm-schema/main.go`): Uses a worker pool (2x CPU cores) to process charts concurrently. Each worker receives chart paths via a channel.\n\n3. **Schema Generation** (`pkg/schema/worker.go`): For each chart, reads the `values.yaml` file and parses YAML with annotations to build a JSON Schema object.\n\n4. **Annotation Parsing** (`pkg/schema/schema.go`): Parses `# @schema` and `# @schema.root` comment blocks to extract JSON Schema properties (type, description, enum, pattern, etc.).\n\n5. **Dependency Resolution** (`cmd/helm-schema/main.go`): After all charts are processed, topologically sorts them by dependencies and merges dependency schemas into parent charts. Library charts (type: library) have their properties merged at the top level.\n\n6. **Output** (`cmd/helm-schema/main.go`): Writes `values.schema.json` files to each chart directory.\n\n7. **Annotation Mode** (`pkg/schema/annotate.go`): With `--annotate` / `-A` flag, writes inferred `@schema` type annotation blocks into `values.yaml` files for keys that don't already have them. This is a separate execution mode that modifies values files instead of generating JSON schema. Keys that already have `@schema` blocks are skipped.\n\n### Key Components\n\n#### Schema Parsing (`pkg/schema/schema.go`)\n\n- **`ParseValues()`**: Main entry point that parses a values.yaml file and returns a Schema\n- **`parseYamlNode()`**: Recursively traverses YAML nodes, extracting schema annotations and inferring types\n- **Annotation blocks**: Comments between `# @schema` markers are parsed as YAML to extract JSON Schema properties\n- **Root annotations**: Comments between `# @schema.root` markers apply to the root schema object itself\n- **Type inference**: If no type is specified, the tool infers it from YAML tags (!!str, !!int, !!bool, etc.)\n\n#### Worker Pattern (`pkg/schema/worker.go`)\n\n- Workers pull chart paths from a channel and process them independently\n- Each worker:\n  1. Reads Chart.yaml\n  2. For schema generation, finds all configured values files that exist for the chart and merges them in CLI order\n  3. Parses merged values into a Schema\n  4. Sends Result to results channel\n\n- When multiple values files are present, later files override earlier files using Helm-style nested map merge precedence.\n- `--annotate` and `--add-schema-reference` still operate on the first matching values file only; they do not merge multiple files.\n\n#### Dependency Graph (`pkg/schema/toposort.go`)\n\n- **TopoSort()**: Uses DFS-based topological sorting to ensure dependencies are processed before dependents\n- Detects circular dependencies and can either fail or warn based on `allowCircular` flag\n- Returns charts in dependency order (dependencies first, parents last)\n\n#### Chart Models (`pkg/chart/chart.go`)\n\n- **ChartFile**: Represents Chart.yaml structure\n- **Dependency**: Represents a chart dependency with name, version, alias, condition\n\n#### Schema Merging (in `main.go`)\n\n- Regular dependencies: Nested under dependency name (or alias) in parent schema\n- Library charts: Properties merged directly into parent schema at top level\n- Import-values: Properties from dependency's `exports` section (or custom paths) merged into parent at specified location\n- Conditional dependencies: If a dependency has a `condition` field, the corresponding boolean property is auto-created in the dependency's schema\n- Skip validation flag (`-m`): Can disable strict validation for dependencies by setting `additionalProperties: true`\n\n#### Import-Values Processing (in `main.go`)\n\nThe `processImportValues()` function handles Helm's `import-values` directive:\n\n- **Simple form** (`import-values: [defaults]`): Imports from `exports.defaults` in dependency to parent's root\n- **Complex form** (`import-values: [{child: \"path\", parent: \"path\"}]`): Explicit source and target paths\n- Properties are merged using `mergeSchemaProperties()`, which skips \"global\" and warns on conflicts\n- When import-values is used on a non-library dependency, the dependency is NOT auto-nested (user controls what's imported)\n\n#### Schema Path Navigation (`pkg/schema/schema.go`)\n\n- **`GetPropertyAtPath(path string)`**: Navigates dot-separated paths (e.g., \"exports.defaults\") and returns the schema at that location, or nil if not found\n- **`SetPropertyAtPath(path string)`**: Creates intermediate object schemas as needed and returns the schema at the target path\n\n### Important Patterns\n\n1. **BoolOrArrayOfString**: The `required` field in JSON Schema can be either a boolean or an array of strings. This custom type handles both cases during marshaling/unmarshaling.\n\n2. **SchemaOrBool**: Some JSON Schema fields like `additionalProperties` can be either a boolean or a Schema object. This is represented as `interface{}`.\n\n3. **Annotation Comments**: The tool looks for comments in specific formats:\n   - `# @schema` / `# @schema` blocks for field-level annotations\n   - `# @schema.root` / `# @schema.root` blocks for root-level annotations\n   - Comments outside these blocks become descriptions (unless `description` is explicitly set)\n\n4. **Helm-docs compatibility**: With `-p` flag, parses `-- helm-docs description` and `@default` annotations from helm-docs format.\n   - Helm-docs type `tpl` is mapped to JSON Schema `string` in `helmDocsTypeToSchemaType`.\n\n## Testing Strategy\n\n- **Unit tests**: Each package has `*_test.go` files testing individual functions\n- **Integration tests**: `tests/run.sh` compares generated schemas against expected outputs\n- **Test files**: `tests/test_*.yaml` are input values files, `tests/test_*_expected.schema.json` are expected outputs\n\n## Plugin Verification (Helm v4)\n\nThe project implements Helm v4 plugin verification through GPG signing:\n\n### Signing Infrastructure\n\n1. **sign-plugin.sh**: Script that creates `.prov` (provenance) files for plugin tarballs\n   - Takes version, tarball path, and GPG key as arguments\n   - Creates a signed provenance file containing metadata and SHA256 hash\n   - Uses GPG to sign the provenance\n\n2. **GitHub Actions Workflow**: `.github/workflows/release.yml`\n   - Generates release notes with `orhun/git-cliff-action@v4` using `cliff.toml`\n   - Imports GPG private key from secrets (`GPG_PRIVATE_KEY`, `GPG_PASSPHRASE`)\n   - Runs goreleaser to build and package binaries\n   - Updates the GitHub release body from `RELEASE_NOTES.md`\n   - Signs all `.tar.gz` files with `sign-plugin.sh`\n   - Uploads `.prov` files to GitHub releases\n\n3. **GoReleaser Config**: `.goreleaser.yaml`\n   - Archives include plugin files: `plugin.yaml`, `install-binary.sh`, `README.md`, `LICENSE`\n   - Configured to sign checksums with GPG\n\n4. **git-cliff Config**: `cliff.toml`\n   - Groups conventional commits into release note sections\n   - Uses GitHub metadata to link pull requests in generated release notes\n   - Generates only the latest tagged release notes in CI via `--latest --strip header`\n\n### Setup for Maintainers\n\n- See `.github/SETUP_SIGNING.md` for initial GPG key setup\n- Public key should be in `signing-key.asc` (currently a template)\n- Key details must be updated in `VERIFICATION.md`\n\n### Verification Process\n\nUsers can verify plugins with:\n\n```bash\nhelm plugin install <tarball> --verify\nhelm plugin verify schema\n```\n\n## Supported JSON Schema Draft 7 Keywords\n\nThe Schema struct (`pkg/schema/schema.go`) supports the following JSON Schema Draft 7 keywords:\n\n### Core Keywords\n\n- `$schema`, `$id`, `$ref`, `$comment`\n- `type` (single type or array of types)\n- `title`, `description`\n- `default`, `examples`\n- `definitions` (also supports `$defs` from Draft 2019-09+ - automatically converted to `definitions`)\n\n### Validation Keywords\n\n#### Numeric (number, integer)\n\n- `minimum`, `maximum` (float64 - supports decimal values like `1.5`)\n- `exclusiveMinimum`, `exclusiveMaximum` (float64)\n- `multipleOf` (float64 - supports `0.1`, `0.01`, etc.)\n\n#### String\n\n- `minLength`, `maxLength`\n- `pattern` (regex pattern)\n- `format` (date-time, email, uri, ipv4, ipv6, uuid, etc.)\n- `contentEncoding`, `contentMediaType`\n\n#### Array\n\n- `items` (single schema or handled via anyOf for arrays)\n- `additionalItems` (boolean or schema)\n- `minItems`, `maxItems`\n- `uniqueItems`\n- `contains`\n\n#### Object\n\n- `properties`, `patternProperties`\n- `additionalProperties` (boolean or schema)\n- `required` (boolean or array of strings)\n- `minProperties`, `maxProperties`\n- `propertyNames`\n- `dependencies`\n\n### Composition Keywords\n\n- `allOf`, `anyOf`, `oneOf`, `not`\n- `if`, `then`, `else`\n\n### Annotation Keywords\n\n- `deprecated`, `readOnly`, `writeOnly`\n- `enum`, `const`\n- `const-from-value` copies the YAML value into the generated `const` keyword and must not be combined with explicit `const`\n\n### Custom Annotations\n\n- Any key prefixed with `x-` is treated as a custom annotation\n\n## Documentation Notes\n\n- README plugin verification examples should use a `vX.Y.Z` placeholder to avoid version drift.\n- GPG public key fingerprint (from `signing-key.asc`) is `806F 70D2 5667 D42A AE4E 07CE F587 0796 9D0F BFA5`; key ID is `F58707969D0FBFA5`.\n\n## Validation Behavior\n\nThe schema validation (`Validate()` method) performs type-specific constraint checks:\n\n- Numeric constraints (`minimum`, `maximum`, etc.) require `type: number` or `type: integer`\n- String constraints (`minLength`, `maxLength`, `pattern`, `format`, `contentEncoding`) require `type: string`\n- Array constraints (`items`, `minItems`, `maxItems`, `contains`, `additionalItems`) require `type: array`\n- Object constraints (`minProperties`, `maxProperties`, `propertyNames`, `additionalProperties`) require `type: object`\n\nSome keywords like `uniqueItems` are accepted on any type per the JSON Schema spec (keywords are ignored if the type doesn't match).\n\n## Common Gotchas\n\n1. **Draft 7 limitation**: The tool uses JSON Schema Draft 7 because Helm's validation library only supports that version. When referencing external schemas that use `$defs` (Draft 2019-09+), they are automatically converted to `definitions` and `$ref` paths are rewritten from `#/$defs/` to `#/definitions/`.\n\n2. **Root annotations placement**: `@schema.root` blocks must be at the top of values.yaml with no blank lines after (unless using `-s` flag).\n\n3. **Dependency schema immutability**: You cannot modify a dependency's schema using annotations in the parent chart's values.yaml. The schema comes from the dependency's own values.yaml.\n\n4. **Library chart merging**: When a library chart property name conflicts with a parent property, the parent takes precedence (with a warning logged).\n\n5. **Import-values behavior**: When `import-values` is used on a non-library dependency, the dependency's full schema is NOT auto-nested under its name. Only explicitly imported properties appear in the parent schema. This matches Helm's behavior where import-values gives the user explicit control over what's imported.\n\n6. **Comment parsing**: By default, descriptions are cut at the first empty line in comments. Use `-s` to keep full comments.\n\n7. **Plugin signing**: Signing only works if GPG secrets are configured in GitHub. Missing secrets cause signing steps to be skipped gracefully.\n\n8. **Dependency schema regeneration**: By default every discovered chart — including those declared as dependencies of another chart — has its `values.schema.json` regenerated from its own `values.yaml`. Pass `--keep-existing-dep-schemas` (`-K`) to instead reuse a dependency chart's pre-existing `values.schema.json` (useful for third-party charts pulled via `helm dep up`, so their rich constraints and `x-*` annotations are preserved). When the flag is set, such dependency schemas are merged into the parent as-is and are not overwritten on disk.\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM alpine:3.23@sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc91145e68878dd4a5019afde11\nARG TARGETPLATFORM\nRUN adduser -k /dev/null -u 10001 -D helm-schema \\\n  && chgrp 0 /home/helm-schema \\\n  && chmod -R g+rwX /home/helm-schema\nCOPY $TARGETPLATFORM/helm-schema /\nUSER 10001\nVOLUME [ \"/home/helm-schema\" ]\nWORKDIR /home/helm-schema\nENTRYPOINT [\"/helm-schema\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 dadav\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# helm-schema\n\n<p align=\"center\">\n  <img src=\"images/logo.png\" width=\"400\" />\n  <br />\n  <a href=\"https://github.com/dadav/helm-schema/releases\"><img src=\"https://img.shields.io/github/release/dadav/helm-schema.svg\" alt=\"Latest Release\"></a>\n  <a href=\"https://pkg.go.dev/github.com/dadav/helm-schema?tab=doc\"><img src=\"https://godoc.org/github.com/golang/gddo?status.svg\" alt=\"Go Docs\"></a>\n  <a href=\"https://github.com/dadav/helm-schema/actions\"><img src=\"https://img.shields.io/github/actions/workflow/status/dadav/helm-schema/build_and_test.yml\" alt=\"Build Status\"></a>\n  <a href=\"https://opensource.org/licenses/MIT\"><img src=\"https://img.shields.io/badge/License-MIT-green.svg\" alt=\"MIT LICENSE\"></a>\n  <a href=\"https://github.com/pre-commit/pre-commit\"><img src=\"https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit\" alt=\"pre-commit\" style=\"max-width:100%;\"></a>\n  <a href=\"https://goreportcard.com/badge/github.com/dadav/helm-schema\"><img src=\"https://goreportcard.com/badge/github.com/dadav/helm-schema\" alt=\"Go Report\"></a>\n</p>\n\n<p align=\"center\">This tool tries to help you to easily create some nice <a href=\"https://json-schema.org/\" target=\"_blank\"><strong>JSON schema</strong></a> for your helm chart.</p>\n\nBy default it will traverse the current directory and look for `Chart.yaml` files.\nFor every file, helm-schema will try to find one of the given value filenames.\nThe first files found will be read and a jsonschema will be created.\nFor every dependency defined in the `Chart.yaml` file, a reference to the dependencies JSON schema\nwill be created.\n\n> [!NOTE]\n> The tool uses `jsonschema` Draft 7, because the library helm uses only supports that version.\n\n## Installation\n\nVia `go` install:\n\n```sh\ngo install github.com/dadav/helm-schema/cmd/helm-schema@latest\n```\n\nFrom `aur`:\n\n```sh\nparu -S helm-schema\n```\n\nVia `podman/docker`:\n\n```sh\npodman run --rm -v $PWD:/home/helm-schema ghcr.io/dadav/helm-schema:latest\n```\n\nAs `helm plugin`:\n\n```sh\nhelm plugin install https://github.com/dadav/helm-schema\n```\n\n### Plugin Verification (Helm v4+)\n\nHelm v4 introduced plugin verification for enhanced security. All helm-schema releases are signed with GPG and include provenance files (`.prov`) for verification.\n\n**Automatic Verification (Recommended)**\n\nHelm v4 verifies plugin signatures by default:\n\n```sh\n# Install from a specific release with automatic verification\nhelm plugin install https://github.com/dadav/helm-schema/releases/download/vX.Y.Z/helm-schema_X.Y.Z_Linux_x86_64.tar.gz\n```\n\n**Manual Verification**\n\nBefore installing, import the signing key:\n\n```sh\n# Import the public signing key (from file)\ngpg --import signing-key.asc\n\n# Or import by key ID (last 16 hex chars of the fingerprint)\ngpg --keyserver keyserver.ubuntu.com --recv-keys F58707969D0FBFA5\n\n# Verify the imported key fingerprint (expect: 806F 70D2 5667 D42A AE4E 07CE F587 0796 9D0F BFA5)\ngpg --fingerprint F58707969D0FBFA5\n\n# Export from kdx and save in old gpg format\ngpg --export F58707969D0FBFA5 > ~/.gnupg/pubring.gpg\n\n# Install with explicit verification\nhelm plugin install https://github.com/dadav/helm-schema/releases/download/vX.Y.Z/helm-schema_X.Y.Z_Linux_x86_64.tar.gz --verify\n```\n\n**Verify Installed Plugin**\n\n```sh\n# Verify an already installed plugin\nhelm plugin verify schema\n```\n\n> [!NOTE]\n> Plugin verification requires Helm v4 or later. If using Helm v3, signatures will be ignored.\n\n## Usage\n\n### Pre-commit hook\n\nIf you want to automatically generate a new `values.schema.json` if you change the `values.yaml`\nfile, you can do the following:\n\n1. Install [`pre-commit`](https://pre-commit.com/#install)\n2. Copy the [`.pre-commit-config.yaml`](./.pre-commit-config.yaml) to your helm chart repository.\n3. Then run these commands:\n\n```sh\npre-commit install\npre-commit install-hooks\n```\n\n### Running the binary directly\n\nYou can also just run the binary yourself:\n\n```sh\nhelm-schema\n```\n\n### Options\n\nThe binary has the following options:\n\n```sh\nFlags:\n  -A, --annotate                               \"write inferred @schema type blocks into the first matching values file instead of generating schema\"\n  -r, --add-schema-reference                   \"add reference to schema in values.yaml if not found\"\n  -w, --allow-circular-dependencies            \"allow circular dependencies between charts (will log a warning instead of failing)\"\n  -a, --append-newline                         \"append newline to generated jsonschema at the end of the file\"\n  -c, --chart-search-root string               \"directory to search recursively within for charts (default \".\")\"\n  -i, --dependencies-filter strings            \"only generate schema for specified dependencies (comma-separated list of dependency names)\"\n  -g, --dont-add-global                        \"don't auto add global property\"\n  -x, --dont-strip-helm-docs-prefix            \"disable the removal of the helm-docs prefix (--)\"\n  -d, --dry-run                                \"don't actually create files just print to stdout passed\"\n  -p, --helm-docs-compatibility-mode           \"parse and use helm-docs comments\"\n  -h, --help                                   \"help for helm-schema\"\n  -K, --keep-existing-dep-schemas              \"use dependency charts' pre-existing values.schema.json instead of regenerating from values.yaml\"\n  -s, --keep-full-comment                      \"keep the whole leading comment (default: cut at empty line)\"\n  -l, --log-level string                       \"level of logs that should be printed, one of (panic, fatal, error, warning, info, debug, trace) (default \"info\")\"\n  -n, --no-dependencies                        \"skip dependency charts: don't merge them into parents and don't generate their schemas\"\n  -o, --output-file string                     \"jsonschema file path relative to each chart directory to which jsonschema will be written (default 'values.schema.json')\"\n  -m, --skip-dependencies-schema-validation    \"skip schema validation for dependencies by setting additionalProperties to true and removing from required\"\n  -f, --value-files strings                    \"filenames to look for chart values; schema generation merges all matches in the order provided (default [values.yaml])\"\n  -k, --skip-auto-generation strings           \"skip the auto generation for these fields (default [])\"\n  -u, --uncomment                              \"consider yaml which is commented out\"\n  -v, --version                                \"version for helm-schema\"\n```\n\nFor schema generation, `helm-schema` checks each `--value-files` entry for the chart, keeps the ones that exist, and merges them in the order provided. Later files take precedence over earlier files, following Helm's `-f/--values` behavior.\n\n`--annotate` does not merge multiple files. It only annotates the first matching values file.\n\n`--add-schema-reference` also targets the first matching values file.\n\n### Annotate mode\n\nUse `--annotate` to add inferred `# @schema` type blocks to a values file instead of generating `values.schema.json`.\n\n- Keys that already have `@schema` annotations are left unchanged.\n- With `-d, --dry-run`, the annotated file is printed to stdout instead of being written back.\n- When multiple `--value-files` entries are configured, annotate mode uses only the first matching file.\n\n## Annotations\n\nThe `jsonschema` must be between two entries of `# @schema` :\n\n```yaml\n# @schema\n# my: annotation\n# @schema\n# you can add comment here as well\nfoo: bar\n```\n\n> [!WARNING]\n> It must be written just above the key you want to annotate.\n\n> [!NOTE]\n> If you don't use the `properties` option on hashes/objects or don't use `items` on arrays, it will be parsed from the values and their annotations instead.\n\n### Root-level annotations\n\nYou can apply schema annotations to the root schema object itself using `# @schema.root`:\n\n```yaml\n# @schema.root\n# title: My Chart Values\n# description: Configuration values for my Helm chart\n# x-custom-field: custom-value\n# @schema.root\n# @schema\n# enum: [dev, staging, prod]\n# @schema\n# Example description foo baz\nstage: dev\n```\n\n> [!NOTE]\n> The `@schema.root` block must be placed before the first key in your `values.yaml` file, without blank lines after it (unless you use the `-s` flag to keep full comments).\n\n### Available annotations\n\n<!-- prettier-ignore -->\n| Key| Description | Values |\n|-|-|-|\n| [`type`](#type) | Defines the [jsonschema-type](https://json-schema.org/understanding-json-schema/reference/type.html) of the object. Multiple values are supported (e.g. `[string, integer]`) as a shortcut to `anyOf` | `object`, `array`, `string`, `number`, `integer`, `boolean` or `null` |\n| [`title`](#title) | Defines the [title field](https://json-schema.org/understanding-json-schema/reference/generic.html?highlight=title) of the object | Defaults to the key itself |\n| [`description`](#description) | Defines the [description field](https://json-schema.org/understanding-json-schema/reference/generic.html?highlight=description) of the object. | Defaults to the comments just above or below the `@schema` annotations block |\n| [`default`](#default) | Sets the default value and will be displayed first on the users IDE| Takes a `string` |\n| [`properties`](#properties) | Contains a map with keys as property names and values as schema | Takes an `object` |\n| [`pattern`](#pattern) | Regex pattern to test the value | Takes an `string` |\n| [`format`](#format) | The [format keyword](https://json-schema.org/understanding-json-schema/reference/string.html#format) allows for basic semantic identification of certain kinds of string values | Takes a [keyword](https://json-schema.org/understanding-json-schema/reference/string.html#format) |\n| [`required`](#required) | Adds the key to the required items | `true` or `false` or `array` |\n| [`deprecated`](#deprecated) | Marks the option as deprecated | `true` or `false` |\n| [`items`](#items) | Contains the schema that describes the possible array items | Takes an `object` |\n| [`enum`](#enum) | Multiple allowed values. Accepts an array of `string` | Takes an `array` |\n| [`const`](#const) | Single allowed value | Takes a `string`|\n| [`examples`](#examples) | Some examples you can provide for the end user | Takes an `array` |\n| [`minimum`](#minimum) | Minimum value. Can't be used with `exclusiveMinimum` | Takes a `number` (integer or float). Must be smaller than `maximum` or `exclusiveMaximum` (if used) |\n| [`exclusiveMinimum`](#exclusiveminimum) | Exclusive minimum. Can't be used with `minimum` | Takes a `number` (integer or float). Must be smaller than `maximum` or `exclusiveMaximum` (if used) |\n| [`maximum`](#maximum) | Maximum value. Can't be used with `exclusiveMaximum` | Takes a `number` (integer or float). Must be bigger than `minimum` or `exclusiveMinimum` (if used) |\n| [`exclusiveMaximum`](#exclusivemaximum) | Exclusive maximum value. Can't be used with `maximum` | Takes a `number` (integer or float). Must be bigger than `minimum` or `exclusiveMinimum` (if used) |\n| [`multipleOf`](#multipleof) | The yaml-value must be a multiple of. For example: If you set this to 0.1, allowed values would be 0, 0.1, 0.2... | Takes a `number` (integer or float, must be > 0) |\n| [`additionalProperties`](#additionalproperties) | Allow additional keys in maps. Useful if you want to use for example `additionalAnnotations`, which will be filled with keys that the `jsonschema` can't know| Defaults to `false` if the map is not an empty map. Takes a schema or boolean value |\n| [`patternProperties`](#patternproperties) | Contains a map which maps schemas to pattern. If properties match the patterns, the given schema is applied| Takes an `object` |\n| [`anyOf`](#anyof) | Accepts an array of schemas. None or one must apply | Takes an `array` |\n| [`oneOf`](#oneof) | Accepts an array of schemas. One or more must apply | Takes an `array` |\n| [`allOf`](#allof) | Accepts an array of schemas. All must apply| Takes an `array` |\n| [`not`](#not) | A schema that must not be matched. | Takes an `object` |\n| [`if/then/else`](#ifthenelse) | `if` the given schema applies, `then` also apply the given schema or `else` the other schema| Takes an `object` |\n| [`$ref`](#ref) | Accepts an URI to a valid `jsonschema`. Extend the schema for the current key | Takes an URI (or relative file) |\n| [`minLength`](#minlength) | Minimum string length. | Takes an `integer`. Must be smaller or equal than `maxLength` (if used) |\n| [`maxLength`](#maxlength) | Maximum string length. | Takes an `integer`. Must be greater or equal than `minLength` (if used) |\n| [`minItems`](#minItems) | Minimum length of an array. | Takes an `integer`. Must be smaller or equal than `maxItems` (if used) |\n| [`maxItems`](#maxItems) | Maximum length of an array. | Takes an `integer`. Must be greater or equal than `minItems` (if used) |\n| [`contains`](#contains) | Array must contain at least one item matching this schema | Takes a schema `object` |\n| [`additionalItems`](#additionalItems) | Schema for array items beyond those defined in `items` tuple | Takes a `boolean` or schema `object` |\n| [`minProperties`](#minProperties) | Minimum number of properties in an object | Takes an `integer` >= 0 |\n| [`maxProperties`](#maxProperties) | Maximum number of properties in an object | Takes an `integer` >= 0 |\n| [`propertyNames`](#propertyNames) | Schema that all property names must match | Takes a schema `object` |\n| [`dependencies`](#dependencies) | Property dependencies (presence of one property requires others) | Takes an `object` mapping property names to arrays or schemas |\n| [`definitions`](#definitions) | Reusable schema definitions for use with `$ref`. Also supports `$defs` from newer JSON Schema drafts (automatically converted) | Takes an `object` mapping names to schemas |\n| [`$comment`](#comment) | Comment for schema maintainers (not shown to end users) | Takes a `string` |\n| [`contentEncoding`](#contentEncoding) | Encoding for string content (e.g., base64) | Takes a `string` |\n| [`contentMediaType`](#contentMediaType) | MIME type for string content | Takes a `string` |\n\n## Validation & completion\n\nTo take advantage of the generated `values.schema.json`, you can use it within your IDE through a plugin supporting the `yaml-language-server` annotation (e.g. [VSCode - YAML](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml))\n\nYou'll have to place this line at the top of your `values.yaml` (`$schema=<path-or-url-to-your-schema>`) :\n\n```yaml\n# vim: set ft=yaml:\n# yaml-language-server: $schema=values.schema.json\n\n# @schema\n# required: true\n# @schema\n# -- This is an example description\nfoo: bar\n```\n\nYou can use the `-r` flag to make sure this line exists.\n\n> [!NOTE]\n> You can also point to an online available schema, if you upload a version of yours and want other to be able to implement it.\n>\n> `yaml-language-server: $schema=https://example.org/my-json-schema.json`\n>\n> e.g. from github `https://raw.githubusercontent.com/<user>/<repo>/main/values.schema.json`\n\n### helm-docs\n\nIf you're using [`helm-docs`](https://github.com/norwoodj/helm-docs), then you can combine both annotations and use both pre-commit hooks to automatically generate your documentation (e.g. `README.md`) alongside your `values.schema.json`.\n\nIf not provided, `title` will be the key and the `description` will be parsed from the `helm-docs` formatted comment.\n\n```yaml\n# @schema\n# type: array\n# @schema\n# -- helm-docs description here\nfoo: []\n```\n\nIf you use `-p`/`--helm-docs-compatibility-mode` flags, the `@default`, `(type)` annotations and helm-docs descriptions\nare used if detected.\n\n> [!NOTE]\n> Make sure to place the `@schema` annotations **before** the actual key description to avoid having it in your `helm-docs` generated table\n\n## Dependencies\n\nBy default, `helm-schema` will try to also create the schemas for the dependencies in their respective chart directory. These schemas will be merged as properties in the main schema, but the `requiredProperties` field will be nullified, otherwise you would have to always overwrite all the required fields.\n\nIf you don't want to generate `jsonschema` for chart dependencies, you can use the `-n, --no-dependencies` option to only generate the `values.schema.json` for your parent chart(s). With this flag, any discovered chart that is declared as a dependency of another discovered chart is skipped entirely — the dependency is not merged into its parent and its own `values.schema.json` is not generated.\n\n### Reusing a Dependency's Pre-existing Schema\n\nBy default, `helm-schema` regenerates `values.schema.json` for every discovered chart — including subcharts that already ship with a hand-written `values.schema.json`. If you instead want to preserve a dependency's shipped schema (typical for third-party charts pulled via `helm dep up` that carry rich constraints such as `minimum`, `pattern`, `format`, or custom `x-*` annotations), pass `-K, --keep-existing-dep-schemas`:\n\n```sh\nhelm-schema -K\n```\n\nWhen this flag is set:\n\n1. A dependency chart's pre-existing `values.schema.json` is used as-is and merged into the parent.\n2. That dependency's schema file is not overwritten on disk.\n\nWithout the flag, every discovered chart's schema is regenerated from its `values.yaml`.\n\n### Library Charts\n\nWhen a dependency has `type: library` in its `Chart.yaml`, `helm-schema` will merge its schema properties directly into the parent chart's schema at the top level, rather than nesting them under the dependency name. This reflects how [Helm library charts](https://helm.sh/docs/topics/library_charts/) work in practice, where the values scope is identical to the parent chart.\n\nFor example, if you have a library chart named `common` with properties `environment` and `region`, these will appear at the top level of the parent schema alongside the parent's own properties, rather than under a `common` key.\n\n**Note:** If a library chart property has the same name as a property already defined in the parent chart, the parent's property takes precedence and a warning will be logged.\n\n### Skip Dependency Schema Validation\n\nBy default, when dependency schemas are merged into the parent chart schema, they inherit strict validation rules. This means that if you add unknown keys at the top level of a dependency's values (e.g., `subchart.unknownKey`), validation may not fail as expected.\n\nIf you want to allow additional properties in dependency schemas and ensure they are not required, you can use the `-m, --skip-dependencies-schema-validation` flag. This will:\n\n1. Set `additionalProperties: true` for all dependency schemas in the parent chart\n2. Remove dependency names from the parent chart's required properties list\n\nExample usage:\n\n```sh\nhelm-schema -m\n```\n\nThis is useful when you have umbrella charts with multiple dependencies and want to allow flexibility in overriding dependency values without strict schema validation.\n\n### Handling Circular Dependencies\n\nIn some scenarios, you may have charts that reference each other to share values, creating circular dependencies. For example:\n\n- A cert-manager chart depends on a grafana chart to get the instance name for creating dashboards\n- The grafana chart depends on the cert-manager chart to get the ACME issuer value\n\nBy default, `helm-schema` will detect this as a circular dependency and log a warning. The tool will still continue processing, but the results may not be sorted in dependency order.\n\nIf you want to explicitly allow circular dependencies and acknowledge this behavior, you can use the `-w, --allow-circular-dependencies` flag:\n\n```sh\nhelm-schema -w\n```\n\nWhen this flag is enabled, circular dependencies are treated as warnings rather than errors, and the charts are processed in their original discovery order instead of topologically sorted order.\n\n**Note:** This is primarily useful when charts have cross-dependencies purely for value sharing, not for actual build order dependencies.\n\n## Limitations\n\nYou can't change the `jsonschema` for dependencies by using `@schema` annotations on dependency config values. For example:\n\n```yaml\n# foo is a dependency chart\nfoo:\n  # You can't change the schema here, this has no effect.\n  # @schema\n  # type: number\n  # @schema\n  bar: 1\n```\n\n## Examples\n\nSome annotation examples you may want to use, to help you get started!\n\n> [!NOTE]\n> See how the schema behaves with live examples : [values.yaml](./examples/values.yaml)\n\nBelow a snippet to test it out, with the current options `helm-schema` will not analyze dependencies (`-n`) and will omit the `additionalProperties` (`-k`, when not explicitly defined) in the generated schema. It will start looking for `Chart.yaml` and `values.yaml` files in `examples/` (`-c`)\n\n```sh\ncd examples\nhelm-schema -n -k additionalProperties\n\n# or\n\nhelm-schema -c examples -n -k additionalProperties\n```\n\nIf you'd like to use `helm-schema` on your chart dependencies as well, you have to build and unpack them before. You'll avoid the \"missing dependency\" error message.\n\n```sh\n# go where your Chart.lock/yaml is located\ncd <chart-name>\n\n# build dependencies and untar them\nhelm dep build\nls charts/*.tgz |xargs -n1 tar -C charts/ -xzf\n```\n\n#### `type`\n\nIf `type` isn't specified, current value type will be used.\n\n```yaml\n# Will be parsed as 'string'\n# @schema\n# title: Some title\n# description: Some description\n# @schema\nname: foo\n\n# Will be parsed as 'boolean'\n# @schema\n# type: boolean\n# @schema\nenabled: true\n\n# You can define multiple types as an array.\n# @schema\n# type: [string, integer]\n# minimum: 0\n# @schema\ncpu: 1\n```\n\n#### Root schema annotations\n\nApply schema annotations to the root document itself. This is especially useful for setting the `additionalProperties` on the root level of the schema.\n\n```yaml\n# @schema.root\n# additionalProperties: true\n# @schema.root\n# Main application settings\napp:\n  # @schema\n  # type: string\n  # @schema\n  name: my-app\n  # @schema\n  # type: boolean\n  # @schema\n  enabled: true\n```\n\n#### `title`\n\nBy default, the `title` will be parsed from the key name. If the key is `foo`, then `title: foo`.\n\n```yaml\n# Define a custom title for the key\n# @schema\n# title: My custom title for 'foo'\n# @schema\nbar: foo\n```\n\n#### `description`\n\nYou can provide the `description` through its property or let it be parsed from your comments. If `description` is provided, the comments will not be parsed as description.\n\nIf you're implementing it alongside `helm-docs`, read [this](#helm-docs) to do it correctly.\n\n```yaml\n# This text will be used as description.\n# @schema\n# type: integer\n# minimum: 1\n# @schema\nreplica: 1\n\n# @schema\n# type: integer\n# minimum: 1\n# @schema\n# This text will be used as description.\nreplica: 1\n\n# @schema\n# type: integer\n# minimum: 1\n# description: This text will be used as description.\n# @schema\n# And not this one\nreplica: 1\n```\n\n#### `default`\n\nHelp users when using their IDE to quickly retrieve the `default` value, for example through <kbd>CTRL+SPACE</kbd>.\n\n```yaml\n# @schema\n# default: standalone\n# enum: [standalone,cluster]\n# @schema\narchitecture: \"\"\n\n# @schema\n# type: boolean\n# default: true\n# @schema\nenabled: true\n```\n\n#### `properties`\n\nAllows user to define valid keys without defining them yet. Give the user an insight of the possible properties, their types and description.\n\nBy default, `title` for the keys defined under `properties` will inherit from the main key (e.g. here `title: env`). You need to provide `title` explicitly if you want to change it.\n\n```yaml\n# @schema\n# properties:\n#   CONFIG_PATH:\n#     title: CONFIG_PATH\n#     type: string\n#     description: The local path to the service configuration file\n#   ADMIN_EMAIL:\n#     title: ADMIN_EMAIL\n#     type: string\n#     format: idn-email\n#   API_URL:\n#     type: string\n#     format: idn-hostname\n#     description: Title will be 'env' as we do not specify it here\n# @schema\n# -- Environment variables. If you want to provide auto-completion to the user\nenv: {}\n```\n\n#### `pattern`\n\nPattern that'll be used to test the value.\n\n```yaml\n# @schema\n# pattern: ^api-key\n# @schema\n# The value have to start with the 'api-key-' prefix\napiKey: \"api-key-xxxxx\"\n```\n\n#### `format`\n\nKnown formats that the value must match. Formats available at [JSON Schema - Formats](https://json-schema.org/understanding-json-schema/reference/string.html#format).\n\n```yaml\n# @schema\n# format: idn-email\n# @schema\n# Requires a valid email format\nemail: foo@example.org\n```\n\n#### `required`\n\nBy default every property is a required property, you can disable this with `required: false` for a single key. You can also invert this behaviour with the option `helm-schema -k required`, now every property is an optional one.\n\n```yaml\n# @schema\n# required: false\n# @schema\naltName: foo\n```\n\nIt's also possible to define an array of required properties on the parent.\n\n```yaml\n# @schema\n# required: [foo]\n# @schema\naltName:\n  foo: bar\n```\n\n#### `deprecated`\n\nLet the user know if the key is deprecated, hence should be avoided.\n\n```yaml\n# @schema\n# deprecated: true\n# @schema\nsecret: foo\n```\n\n#### `items`\n\nIf you want to specify a schema for possible array values without using a default value. E.g. to define the structure of the hosts definition in an k8s ingress resource.\n\n```yaml\n# @schema\n# type: array\n# items:\n#   type: object\n#   properties:\n#     host:\n#       type: object\n#       properties:\n#         url:\n#           type: string\n#           format: idn-hostname\n# @schema\n# Will give auto-completion for the below structure\n# hosts:\n#  - name:\n#      url: my.example.org\nhosts: []\n```\n\n#### `enum`\n\nAllows user to define available values for a given key. Validation will fail and error shown if you try to put another value.\n\n```yaml\n# @schema\n# enum:\n# - application\n# - controller\n# - api\n# @schema\n# Only those three values are accepted\ntype: application\n\n# @schema\n# type: array\n# items:\n#   enum: [api,frontend,backend,microservice,teamA,teamB,us-west-1,us-west-2]\n# @schema\n# For each array index, only one of those values are accepted\ntags:\n  - \"api\"\n  - \"teamA\"\n  - \"us-west-2\"\n```\n\n#### `const`\n\nDefines a constant value which shouldn't be changed.\n\n```yaml\n# @schema\n# const: maintainer@example.org\n# @schema\nmaintainer: maintainer@example.org\n```\n\n#### `const-from-value`\n\nCopies the YAML value into the generated JSON Schema `const` without duplicating the payload in the annotation block.\n\n```yaml\n# @schema\n# const-from-value: true\n# @schema\nmessage: |\n  long message with {{ .gotemplate }}\n```\n\n#### `examples`\n\nProvides example values to the user when hovering the key in IDE, or by auto-completion mechanism.\n\n```yaml\n# @schema\n# format: ipv4\n# examples: [192.168.0.1]\n# @schema\nclusterIP: \"\"\n\n# @schema\n# properties:\n#   CONFIG_PATH:\n#     type: string\n#     description: The local path to the service configuration file\n#     examples: [/path/to/config]\n#   ADMIN_EMAIL:\n#     type: string\n#     format: idn-email\n#     examples: [admin@example.org]\n#   API_URL:\n#     type: string\n#     format: idn-hostname\n#     examples: [https://api.example.org]\n# @schema\n# -- Provide auto-completion and examples to the user\nenv: {}\n```\n\n#### `minimum`\n\nThe value have to be above or equal the given `integer`.\n\n```yaml\n# @schema\n# minimum: 1\n# @schema\nreplica: \"\"\n```\n\n#### `exclusiveMinimum`\n\nThe value have to be strictly above the given `integer`.\n\n```yaml\n# @schema\n# exclusiveMinimum: 0\n# @schema\nreplica: \"\"\n```\n\n#### `maximum`\n\nThe value have to be below or equal the given `integer`.\n\n```yaml\n# @schema\n# maximum: 10\n# @schema\nreplica: \"\"\n```\n\n#### `exclusiveMaximum`\n\nThe value have to be strictly below the given `integer`.\n\n```yaml\n# @schema\n# exclusiveMaximum: 5\n# @schema\ncpu: \"\"\n```\n\n#### `multipleOf`\n\nThe value have to be a multiple of the given `integer`.\n\n```yaml\n# @schema\n# multipleOf: 1024\n# @schema\nstorageCapacity: 2048\n```\n\n#### `additionalProperties`\n\nBy default, `additionalProperties` is set to `false` unless you use the `-k additionalProperties` option. Useful when you don't know what nested keys you'll have.\n\n```yaml\n# @schema\n# additionalProperties: true\n# @schema\n# You'll be able to add as many keys below `env:` as you want without invalidating the schema\nenv:\n  LONG: foo\n  LIST: bar\n  OF: baz\n  VARIABLES: bat\n\n# @schema\n# additionalProperties: true\n# properties:\n#   REQUIRED_VAR:\n#     type: string\n# @schema\nenv:\n  REQUIRED_VAR: foo\n  OPTIONAL_VAR: bar\n```\n\n#### `patternProperties`\n\nMapping schemas to key name patterns. If properties match the patterns, the given schema is applied.\n\nUseful when you work with a long list of keys and want to define a common schema for a group of them, for example.\n\nE.g. `patternProperties.\"^API_.*\"` key defines the pattern whose schema will be applied on any user provided key that match that pattern.\n\n```yaml\n# @schema\n# type: object\n# patternProperties:\n#   \"^API_.*\":\n#     type: string\n#     pattern: ^api-key\n#   \"^EMAIL_.*\":\n#     type: string\n#     format: idn-email\n# @schema\nenv:\n  API_PROVIDER_ONE: api-key-xxxxx\n  API_PROVIDER_TWO: api-key-xxxxx\n  EMAIL_ADMIN: admin@example.org\n  EMAIL_DEFAULT_USER: user@example.org\n```\n\n#### `anyOf`\n\nAllows user to define multiple schema fo a single key. Key can be `anyOf` the given schemas or none of them.\n\n```yaml\n# Accepts multiple types\n# @schema\n# anyOf:\n#   - type: string\n#   - type: integer\n# minimum: 0\n# @schema\nfoot: 1\n\n# The above can be simplified with `type:`\n# @schema\n# type: [string, integer]\n# minimum: 0\n# @schema\nfool: 1\n\n# A pattern is also possible.\n# In this case null or some string starting with foo.\n# @schema\n# anyOf:\n#   - type: \"null\"\n#   - pattern: ^foo\n# @schema\nbar:\n```\n\n#### `oneOf`\n\nAllows user to define multiple schema fo a single key. Key must match `oneOf` the given schemas.\n\n```yaml\n# @schema\n# oneOf:\n#   - type: integer\n#   - pattern: Gib$\n#   - pattern: gib$\n# @schema\nstorage: 30Gib\n```\n\n#### `allOf`\n\nAllows user to define multiple schema for a single key. Key must match `oneOf` the given schemas.\n\n```yaml\n# @schema\n# allOf:\n#   - type: string\n#     pattern: Gib$\n#   - enum: [5Gib,10Gib,15Gib]\n# @schema\nstorage: 10Gib\n```\n\n#### `not`\n\nAllows to define a schema that must not be matched.\n\n```yaml\n# @schema\n# not:\n#   type: string\n# @schema\nfoo: bar\n```\n\n#### `if/then/else`\n\nConditional schema settings with `if`/`then`/`else`\n\n```yaml\n# @schema\n# anyOf:\n#   - type: \"null\"\n#   - type: string\n# if:\n#   type: \"null\"\n# then:\n#   description: It's a null value\n# else:\n#   description: It's a string\n# @schema\nunknown: foo\n```\n\n#### `minLength`\n\nThe value must be an integer greater or equal to zero and defines the minimum length of a string value.\n\n```yaml\n# @schema\n# minLength: 1\n# @schema\nnamespace: foo\n```\n\n#### `maxLength`\n\nThe value must be an integer greater than zero and defines the maximum length of a string value.\n\n```yaml\n# @schema\n# maxLength: 3\n# @schema\nnamespace: foo\n```\n\n#### `minItems`\n\nThe value must be an integer greater than zero and defines the minimum length of an array value.\n\n```yaml\n# @schema\n# minItems: 1\n# @schema\nnamespace:\n  - foo\n```\n\n#### `maxItems`\n\nThe value must be an integer greater than zero and defines the maximum length of an array value.\n\n```yaml\n# @schema\n# maxItems: 2\n# @schema\nnamespace:\n  - foo\n  - bar\n```\n\n#### `uniqueItems`\n\nA schema can ensure that each of the items in an array is unique. Simply set the uniqueItems keyword to true.\n\n```yaml\n# @schema\n# uniqueItems: true\n# @schema\nnamespace:\n  - foo\n  - bar\n```\n\n#### `$ref`\n\nThe value must be an URI or relative file.\n\nRelative files are imported on creation time. If you update the referenced file, you need\nto run helm-schema again.\n\n**foo.json:**\n\n```json\n{\n  \"foo\": {\n    \"type\": \"string\",\n    \"minLength\": 10\n  }\n}\n```\n\n```yaml\n# @schema\n# $ref: foo.json#/foo\n# @schema\nnamespace: foo\n```\n\nis the same as\n\n```yaml\n# @schema\n# type: string\n# minLength: 10\n# @schema\nnamespace: foo\n```\n\n#### `contains`\n\nSpecifies that an array must contain at least one item matching the given schema.\n\n```yaml\n# @schema\n# type: array\n# contains:\n#   type: string\n#   pattern: ^admin\n# @schema\n# At least one item must be a string starting with 'admin'\nusers:\n  - admin-user\n  - regular-user\n```\n\n#### `additionalItems`\n\nControls validation of array items beyond those specified in an `items` tuple.\n\n```yaml\n# @schema\n# type: array\n# additionalItems: false\n# @schema\n# No additional items allowed beyond what's defined\nfixedArray:\n  - foo\n  - bar\n```\n\n#### `minProperties`\n\nMinimum number of properties an object must have.\n\n```yaml\n# @schema\n# type: object\n# minProperties: 1\n# @schema\n# Object must have at least one property\nconfig: {}\n```\n\n#### `maxProperties`\n\nMaximum number of properties an object can have.\n\n```yaml\n# @schema\n# type: object\n# maxProperties: 5\n# @schema\n# Object can have at most 5 properties\nlabels:\n  app: myapp\n  env: prod\n```\n\n#### `propertyNames`\n\nSchema that all property names in an object must match.\n\n```yaml\n# @schema\n# type: object\n# propertyNames:\n#   pattern: ^[a-z][a-z0-9-]*$\n# @schema\n# All property names must be lowercase with hyphens\nannotations:\n  app-name: myapp\n  version: v1\n```\n\n#### `dependencies`\n\nDefine property dependencies - when one property is present, others must be too.\n\n```yaml\n# @schema\n# type: object\n# dependencies:\n#   creditCard: [billingAddress]\n#   billingAddress: [creditCard]\n# @schema\n# If creditCard is present, billingAddress must also be present\npayment:\n  creditCard: \"1234-5678\"\n  billingAddress: \"123 Main St\"\n```\n\n#### `definitions`\n\nDefine reusable schema fragments that can be referenced with `$ref`.\n\n> [!NOTE]\n> When referencing external JSON Schema files that use `$defs` (JSON Schema Draft 2019-09+), helm-schema automatically converts them to `definitions` and rewrites `$ref` paths from `#/$defs/` to `#/definitions/` for Draft 7 compatibility.\n\n```yaml\n# @schema\n# definitions:\n#   port:\n#     type: integer\n#     minimum: 1\n#     maximum: 65535\n# properties:\n#   httpPort:\n#     $ref: \"#/definitions/port\"\n#   httpsPort:\n#     $ref: \"#/definitions/port\"\n# @schema\nservice:\n  httpPort: 80\n  httpsPort: 443\n```\n\n#### `$comment`\n\nAdd comments for schema maintainers that won't be shown to end users.\n\n```yaml\n# @schema\n# type: string\n# $comment: This field is deprecated and will be removed in v2.0\n# @schema\nlegacyField: foo\n```\n\n#### `contentEncoding`\n\nSpecify the encoding for string content, such as base64.\n\n```yaml\n# @schema\n# type: string\n# contentEncoding: base64\n# @schema\n# Value is expected to be base64 encoded\ncertificate: \"LS0tLS1CRUdJTi...\"\n```\n\n#### `contentMediaType`\n\nSpecify the MIME type for string content.\n\n```yaml\n# @schema\n# type: string\n# contentMediaType: application/json\n# contentEncoding: base64\n# @schema\n# Value is base64-encoded JSON\nconfigData: \"eyJmb28iOiAiYmFyIn0=\"\n```\n\n## License\n\n[MIT](https://github.com/dadav/helm-schema/blob/main/LICENSE)\n"
  },
  {
    "path": "cliff.toml",
    "content": "[changelog]\nheader = \"\"\nbody = \"\"\"\n{% if version %}## {{ version }} - {{ timestamp | date(format=\"%Y-%m-%d\") }}\n{% else %}## Unreleased\n{% endif %}\n\n{% for group, commits in commits | group_by(attribute=\"group\") %}\n### {{ group }}\n{% for commit in commits %}\n- {{ commit.message | split(pat=\"\\n\") | first | trim }}{% if commit.remote.pr_number %} ([#{{ commit.remote.pr_number }}](https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}/pull/{{ commit.remote.pr_number }})){% endif %}{% if commit.remote.username %} by @{{ commit.remote.username }}{% endif %}\n{% endfor %}\n\n{% endfor %}\n\"\"\"\ntrim = true\n\n[git]\nconventional_commits = true\nfilter_unconventional = true\nsplit_commits = false\nprotect_breaking_commits = false\nfilter_commits = true\ntag_pattern = \"[0-9].*\"\nsort_commits = \"oldest\"\ncommit_parsers = [\n  { message = \"^feat\", group = \"Features\" },\n  { message = \"^fix\", group = \"Bug Fixes\" },\n  { message = \"^docs?\", group = \"Documentation\" },\n  { message = \"^refactor\", group = \"Refactoring\" },\n  { message = \"^test\", group = \"Testing\" },\n  { message = \"^chore\", group = \"Chores\" },\n]\n\n[remote.github]\nowner = \"dadav\"\nrepo = \"helm-schema\"\n"
  },
  {
    "path": "cmd/helm-schema/cli.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\nfunc possibleLogLevels() []string {\n\tlevels := make([]string, 0)\n\n\tfor _, l := range log.AllLevels {\n\t\tlevels = append(levels, l.String())\n\t}\n\n\treturn levels\n}\n\nfunc configureLogging() {\n\tlogLevelName := viper.GetString(\"log-level\")\n\tlogLevel, err := log.ParseLevel(logLevelName)\n\tif err != nil {\n\t\tlog.Errorf(\"Failed to parse provided log level %s: %s\", logLevelName, err)\n\t\tos.Exit(1)\n\t}\n\n\tlog.SetFormatter(&log.TextFormatter{FullTimestamp: true})\n\tlog.SetLevel(logLevel)\n}\n\nfunc newCommand(run func(cmd *cobra.Command, args []string) error) (*cobra.Command, error) {\n\tcmd := &cobra.Command{\n\t\tUse:           \"helm-schema\",\n\t\tShort:         \"helm-schema automatically generates a jsonschema file for helm charts from values files\",\n\t\tVersion:       version,\n\t\tRunE:          run,\n\t\tSilenceUsage:  true,\n\t\tSilenceErrors: true,\n\t}\n\n\tlogLevelUsage := fmt.Sprintf(\n\t\t\"level of logs that should printed, one of (%s)\",\n\t\tstrings.Join(possibleLogLevels(), \", \"),\n\t)\n\tcmd.PersistentFlags().\n\t\tStringP(\"chart-search-root\", \"c\", \".\", \"directory to search recursively within for charts\")\n\tcmd.PersistentFlags().\n\t\tBoolP(\"dry-run\", \"d\", false, \"don't actually create files just print to stdout passed\")\n\tcmd.PersistentFlags().\n\t\tBoolP(\"append-newline\", \"a\", false, \"append newline to generated jsonschema at the end of the file\")\n\tcmd.PersistentFlags().\n\t\tBoolP(\"keep-full-comment\", \"s\", false, \"keep the whole leading comment (default: cut at empty line)\")\n\tcmd.PersistentFlags().\n\t\tBoolP(\"uncomment\", \"u\", false, \"consider yaml which is commented out\")\n\tcmd.PersistentFlags().\n\t\tBoolP(\"helm-docs-compatibility-mode\", \"p\", false, \"parse and use helm-docs comments\")\n\tcmd.PersistentFlags().\n\t\tBoolP(\"dont-strip-helm-docs-prefix\", \"x\", false, \"disable the removal of the helm-docs prefix (--)\")\n\tcmd.PersistentFlags().\n\t\tBoolP(\"no-dependencies\", \"n\", false, \"skip dependency charts: don't merge them into parents and don't generate their schemas\")\n\tcmd.PersistentFlags().\n\t\tBoolP(\"add-schema-reference\", \"r\", false, \"add reference to schema in values.yaml if not found\")\n\tcmd.PersistentFlags().StringP(\"log-level\", \"l\", \"info\", logLevelUsage)\n\tcmd.PersistentFlags().\n\t\tStringSliceP(\"value-files\", \"f\", []string{\"values.yaml\"}, \"filenames to check for chart values\")\n\tcmd.PersistentFlags().\n\t\tStringP(\"output-file\", \"o\", \"values.schema.json\", \"jsonschema file path relative to each chart directory to which jsonschema will be written\")\n\tcmd.PersistentFlags().\n\t\tStringSliceP(\"skip-auto-generation\", \"k\", []string{}, \"comma separated list of fields to skip from being created by default (possible: title, description, required, default, additionalProperties)\")\n\tcmd.PersistentFlags().\n\t\tStringSliceP(\"dependencies-filter\", \"i\", []string{}, \"only generate schema for specified dependencies (comma-separated list of dependency names)\")\n\tcmd.PersistentFlags().\n\t\tBoolP(\"dont-add-global\", \"g\", false, \"dont auto add global property\")\n\tcmd.PersistentFlags().\n\t\tBoolP(\"skip-dependencies-schema-validation\", \"m\", false, \"skip schema validation for dependencies by setting additionalProperties to true and removing from required\")\n\tcmd.PersistentFlags().\n\t\tBoolP(\"allow-circular-dependencies\", \"w\", false, \"allow circular dependencies between charts (will log a warning instead of failing)\")\n\tcmd.PersistentFlags().\n\t\tBoolP(\"annotate\", \"A\", false, \"write inferred @schema annotations into values.yaml files for unannotated keys\")\n\tcmd.PersistentFlags().\n\t\tBoolP(\"keep-existing-dep-schemas\", \"K\", false, \"use dependency charts' pre-existing values.schema.json instead of regenerating from values.yaml\")\n\n\tviper.AutomaticEnv()\n\tviper.SetEnvPrefix(\"HELM_SCHEMA\")\n\tviper.SetEnvKeyReplacer(strings.NewReplacer(\"-\", \"_\"))\n\terr := viper.BindPFlags(cmd.PersistentFlags())\n\n\treturn cmd, err\n}\n"
  },
  {
    "path": "cmd/helm-schema/main.go",
    "content": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/dadav/helm-schema/pkg/chart\"\n\t\"github.com/dadav/helm-schema/pkg/chart/searching\"\n\t\"github.com/dadav/helm-schema/pkg/schema\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// getDependencyNames extracts dependency names (or aliases if present) from a chart\n// filtering based on the provided dependenciesFilterMap\nfunc getDependencyNames(dependencies []*chart.Dependency, dependenciesFilterMap map[string]bool) []string {\n\tvar depNames []string\n\tfor _, dep := range dependencies {\n\t\tif len(dependenciesFilterMap) > 0 && !dependenciesFilterMap[dep.Name] {\n\t\t\tcontinue\n\t\t}\n\t\tif dep.Alias != \"\" {\n\t\t\tdepNames = append(depNames, dep.Alias)\n\t\t} else if dep.Name != \"\" {\n\t\t\tdepNames = append(depNames, dep.Name)\n\t\t}\n\t}\n\treturn depNames\n}\n\n// mergeSchemaProperties merges properties from source to target schema.\n// It skips \"global\" and properties in the skip map, and returns merged property names.\n// Follows Helm's value coalescing behavior with one exception:\n// - If target has explicit @schema annotation (HasData=true), target wins\n// - If target only has inferred schema (HasData=false), source wins\nfunc mergeSchemaProperties(\n\ttarget *schema.Schema,\n\tsource *schema.Schema,\n\tskip map[string]bool,\n\tsourceName string,\n\ttargetName string,\n) map[string]bool {\n\tmerged := make(map[string]bool)\n\n\tif source.Properties == nil {\n\t\treturn merged\n\t}\n\n\tif target.Properties == nil {\n\t\ttarget.Properties = make(map[string]*schema.Schema)\n\t}\n\n\tfor propName, propSchema := range source.Properties {\n\t\tif propName == \"global\" {\n\t\t\tcontinue\n\t\t}\n\t\tif skip != nil && skip[propName] {\n\t\t\tcontinue\n\t\t}\n\t\texistingProp, exists := target.Properties[propName]\n\t\tif !exists {\n\t\t\ttarget.Properties[propName] = propSchema\n\t\t\tmerged[propName] = true\n\t\t} else if !existingProp.HasData && propSchema.HasData {\n\t\t\t// Target only has inferred schema, source has explicit annotation - source wins\n\t\t\ttarget.Properties[propName] = propSchema\n\t\t\tmerged[propName] = true\n\t\t\tlog.Debugf(\"Property %s from %s replaces inferred schema in %s\", propName, sourceName, targetName)\n\t\t} else if existingProp.HasData {\n\t\t\t// Target has explicit @schema annotation, keep it\n\t\t\tlog.Debugf(\"Property %s from %s skipped: %s has explicit @schema annotation\", propName, sourceName, targetName)\n\t\t} else {\n\t\t\t// Both are inferred schemas, keep target (first wins)\n\t\t\tlog.Debugf(\"Property %s from %s skipped: both schemas are inferred, keeping first\", propName, sourceName)\n\t\t}\n\t}\n\n\treturn merged\n}\n\n// processImportValues processes the import-values directive for a dependency.\n// It returns a map of property names that were imported (to track what was handled).\nfunc processImportValues(\n\tparentSchema *schema.Schema,\n\tdepSchema *schema.Schema,\n\tdep *chart.Dependency,\n\tparentChartName string,\n) map[string]bool {\n\timportedProps := make(map[string]bool)\n\n\tif len(dep.ImportValues) == 0 {\n\t\treturn importedProps\n\t}\n\n\tfor _, importValue := range dep.ImportValues {\n\t\tvar childPath, parentPath string\n\n\t\tswitch v := importValue.(type) {\n\t\tcase string:\n\t\t\t// Simple form: \"defaults\" -> imports from exports.<value> to root\n\t\t\tchildPath = \"exports.\" + v\n\t\t\tparentPath = \"\"\n\t\tcase map[string]interface{}:\n\t\t\t// Complex form: {child: \"path\", parent: \"path\"}\n\t\t\tif child, ok := v[\"child\"].(string); ok {\n\t\t\t\tchildPath = child\n\t\t\t}\n\t\t\tif parent, ok := v[\"parent\"].(string); ok {\n\t\t\t\tparentPath = parent\n\t\t\t}\n\t\tcase map[interface{}]interface{}:\n\t\t\t// YAML sometimes produces this type variation\n\t\t\tif child, ok := v[\"child\"].(string); ok {\n\t\t\t\tchildPath = child\n\t\t\t}\n\t\t\tif parent, ok := v[\"parent\"].(string); ok {\n\t\t\t\tparentPath = parent\n\t\t\t}\n\t\tdefault:\n\t\t\tlog.Warnf(\"Unknown import-values format for dependency %s in chart %s: %T\", dep.Name, parentChartName, importValue)\n\t\t\tcontinue\n\t\t}\n\n\t\tif childPath == \"\" {\n\t\t\tlog.Warnf(\"Empty child path in import-values for dependency %s in chart %s\", dep.Name, parentChartName)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Get the source schema from the dependency\n\t\tsourceSchema := depSchema.GetPropertyAtPath(childPath)\n\t\tif sourceSchema == nil {\n\t\t\tlog.Warnf(\"Could not find path %q in dependency %s schema for chart %s\", childPath, dep.Name, parentChartName)\n\t\t\tcontinue\n\t\t}\n\n\t\tif sourceSchema.Properties == nil {\n\t\t\tlog.Warnf(\"No properties found at path %q in dependency %s for chart %s\", childPath, dep.Name, parentChartName)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Determine target schema in parent\n\t\tvar targetSchema *schema.Schema\n\t\tif parentPath == \"\" {\n\t\t\ttargetSchema = parentSchema\n\t\t} else {\n\t\t\ttargetSchema = parentSchema.SetPropertyAtPath(parentPath)\n\t\t}\n\n\t\tmerged := mergeSchemaProperties(\n\t\t\ttargetSchema,\n\t\t\tsourceSchema,\n\t\t\tnil,\n\t\t\tfmt.Sprintf(\"import-values of %s\", dep.Name),\n\t\t\tparentChartName,\n\t\t)\n\t\ttargetPathDisplay := parentPath\n\t\tif targetPathDisplay == \"\" {\n\t\t\ttargetPathDisplay = \"root\"\n\t\t}\n\t\tfor k := range merged {\n\t\t\timportedProps[k] = true\n\t\t\tlog.Debugf(\"Imported property %q from %s.%s to %s in chart %s\",\n\t\t\t\tk, dep.Name, childPath, targetPathDisplay, parentChartName)\n\t\t}\n\t}\n\n\treturn importedProps\n}\n\nfunc exec(cmd *cobra.Command, _ []string) error {\n\tconfigureLogging()\n\n\tvar skipAutoGeneration, valueFileNames []string\n\n\tchartSearchRoot := viper.GetString(\"chart-search-root\")\n\tdryRun := viper.GetBool(\"dry-run\")\n\tnoDeps := viper.GetBool(\"no-dependencies\")\n\taddSchemaReference := viper.GetBool(\"add-schema-reference\")\n\tkeepFullComment := viper.GetBool(\"keep-full-comment\")\n\thelmDocsCompatibilityMode := viper.GetBool(\"helm-docs-compatibility-mode\")\n\tuncomment := viper.GetBool(\"uncomment\")\n\toutFile := viper.GetString(\"output-file\")\n\tdontRemoveHelmDocsPrefix := viper.GetBool(\"dont-strip-helm-docs-prefix\")\n\tappendNewline := viper.GetBool(\"append-newline\")\n\tdependenciesFilter := viper.GetStringSlice(\"dependencies-filter\")\n\tdependenciesFilterMap := make(map[string]bool)\n\tdontAddGlobal := viper.GetBool(\"dont-add-global\")\n\tskipDepsSchemaValidation := viper.GetBool(\"skip-dependencies-schema-validation\")\n\tallowCircularDeps := viper.GetBool(\"allow-circular-dependencies\")\n\tannotate := viper.GetBool(\"annotate\")\n\tkeepExistingDepSchemas := viper.GetBool(\"keep-existing-dep-schemas\")\n\tfor _, dep := range dependenciesFilter {\n\t\tdependenciesFilterMap[dep] = true\n\t}\n\tif err := viper.UnmarshalKey(\"value-files\", &valueFileNames); err != nil {\n\t\treturn err\n\t}\n\tif err := viper.UnmarshalKey(\"skip-auto-generation\", &skipAutoGeneration); err != nil {\n\t\treturn err\n\t}\n\tworkersCount := runtime.NumCPU() * 2\n\n\tskipConfig, err := schema.NewSkipAutoGenerationConfig(skipAutoGeneration)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tqueue := make(chan string)\n\tresultsChan := make(chan schema.Result)\n\tresults := []*schema.Result{}\n\terrs := make(chan error, 100) // Buffered to prevent deadlock when errors occur before goroutines start\n\tdone := make(chan struct{})\n\n\ttempDir := searching.SearchArchivesOpenTemp(chartSearchRoot, errs)\n\tif tempDir != \"\" {\n\t\tdefer os.RemoveAll(tempDir)\n\t}\n\n\tgo searching.SearchFiles(chartSearchRoot, chartSearchRoot, \"Chart.yaml\", dependenciesFilterMap, queue, errs)\n\n\twg := sync.WaitGroup{}\n\n\tfor i := 0; i < workersCount; i++ {\n\t\twg.Add(1)\n\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tschema.Worker(\n\t\t\t\tdryRun,\n\t\t\t\tuncomment,\n\t\t\t\taddSchemaReference,\n\t\t\t\tkeepFullComment,\n\t\t\t\thelmDocsCompatibilityMode,\n\t\t\t\tdontRemoveHelmDocsPrefix,\n\t\t\t\tdontAddGlobal,\n\t\t\t\tannotate,\n\t\t\t\tvalueFileNames,\n\t\t\t\tskipConfig,\n\t\t\t\toutFile,\n\t\t\t\tqueue,\n\t\t\t\tresultsChan,\n\t\t\t)\n\t\t}()\n\t}\n\n\t// Close resultsChan after all workers are done\n\tgo func() {\n\t\twg.Wait()\n\t\tclose(resultsChan)\n\t\tclose(done)\n\t}()\n\n\t// Collect results and errors until both channels are closed\n\tresultsChanOpen := true\n\tfor resultsChanOpen {\n\t\tselect {\n\t\tcase err, ok := <-errs:\n\t\t\tif ok {\n\t\t\t\tlog.Error(err)\n\t\t\t}\n\t\tcase res, ok := <-resultsChan:\n\t\t\tif !ok {\n\t\t\t\tresultsChanOpen = false\n\t\t\t} else {\n\t\t\t\tresults = append(results, &res)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Drain any remaining errors\ndrainErrors:\n\tfor {\n\t\tselect {\n\t\tcase err, ok := <-errs:\n\t\t\tif ok {\n\t\t\t\tlog.Error(err)\n\t\t\t}\n\t\tdefault:\n\t\t\tbreak drainErrors\n\t\t}\n\t}\n\n\t// In annotate mode, just report errors and return (no schema generation)\n\tif annotate {\n\t\tfoundErrors := false\n\t\tfor _, result := range results {\n\t\t\tif len(result.Errors) > 0 {\n\t\t\t\tfoundErrors = true\n\t\t\t\tif result.Chart != nil {\n\t\t\t\t\tlog.Errorf(\"Found %d errors while annotating chart %s (%s)\", len(result.Errors), result.Chart.Name, result.ChartPath)\n\t\t\t\t} else {\n\t\t\t\t\tlog.Errorf(\"Found %d errors while annotating chart %s\", len(result.Errors), result.ChartPath)\n\t\t\t\t}\n\t\t\t\tfor _, err := range result.Errors {\n\t\t\t\t\tlog.Error(err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif foundErrors {\n\t\t\treturn errors.New(\"some errors were found\")\n\t\t}\n\t\treturn nil\n\t}\n\n\tif !noDeps {\n\t\tresults, err = schema.TopoSort(results, allowCircularDeps)\n\t\tif err != nil {\n\t\t\tif _, ok := err.(*schema.CircularError); ok {\n\t\t\t\tlog.Errorf(\"Error while sorting results: %s\", err)\n\t\t\t\treturn err\n\t\t\t} else {\n\t\t\t\tlog.Warnf(\"Could not sort results: %s\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Identify charts that are declared as dependencies of some other discovered\n\t// chart. Used both to skip dependency charts entirely with --no-dependencies\n\t// and to opt-in reuse of a dependency's pre-existing schema.\n\tisDependencyChart := make(map[string]bool)\n\tfor _, result := range results {\n\t\tif result.Chart == nil || len(result.Errors) > 0 {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, dep := range result.Chart.Dependencies {\n\t\t\tisDependencyChart[dep.Name] = true\n\t\t}\n\t}\n\n\t// For dependency charts with pre-existing schema files, load them instead of\n\t// using the worker-generated schema from values.yaml. Opt-in via\n\t// --keep-existing-dep-schemas; default is to regenerate every discovered\n\t// chart's schema.\n\tif !noDeps && keepExistingDepSchemas {\n\t\tfor _, result := range results {\n\t\t\tif result.Chart == nil || len(result.Errors) > 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !isDependencyChart[result.Chart.Name] {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tschemaPath := filepath.Join(filepath.Dir(result.ChartPath), outFile)\n\t\t\tschemaData, err := os.ReadFile(schemaPath)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tvar existingSchema schema.Schema\n\t\t\tif err := json.Unmarshal(schemaData, &existingSchema); err != nil {\n\t\t\t\tlog.Warnf(\"Found existing %s for dependency %s but failed to parse it: %s\", outFile, result.Chart.Name, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlog.Debugf(\"Using pre-existing schema for dependency chart %s\", result.Chart.Name)\n\t\t\tresult.Schema = existingSchema\n\t\t\tresult.PreExistingSchema = true\n\t\t}\n\t}\n\n\tconditionsToPatch := make(map[string][][]string)\n\tif !noDeps {\n\t\tfor _, result := range results {\n\t\t\tif len(result.Errors) > 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfor _, dep := range result.Chart.Dependencies {\n\t\t\t\tif len(dependenciesFilterMap) > 0 && !dependenciesFilterMap[dep.Name] {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tif dep.Condition != \"\" {\n\t\t\t\t\tconditionKeys := strings.Split(dep.Condition, \".\")\n\t\t\t\t\tif len(conditionKeys) == 1 {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\ttargetName := conditionKeys[0]\n\t\t\t\t\tif dep.Alias != \"\" && dep.Alias == conditionKeys[0] {\n\t\t\t\t\t\ttargetName = dep.Name\n\t\t\t\t\t}\n\t\t\t\t\tif targetName != \"\" {\n\t\t\t\t\t\tconditionsToPatch[targetName] = append(conditionsToPatch[targetName], conditionKeys[1:])\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tchartNameToResult := make(map[string]*schema.Result)\n\tfoundErrors := false\n\n\tfor _, result := range results {\n\t\tif len(result.Errors) > 0 {\n\t\t\tfoundErrors = true\n\t\t\tif result.Chart != nil {\n\t\t\t\tlog.Errorf(\n\t\t\t\t\t\"Found %d errors while processing the chart %s (%s)\",\n\t\t\t\t\tlen(result.Errors),\n\t\t\t\t\tresult.Chart.Name,\n\t\t\t\t\tresult.ChartPath,\n\t\t\t\t)\n\t\t\t} else {\n\t\t\t\tlog.Errorf(\"Found %d errors while processing the chart %s\", len(result.Errors), result.ChartPath)\n\t\t\t}\n\t\t\tfor _, err := range result.Errors {\n\t\t\t\tlog.Error(err)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif result.Chart == nil {\n\t\t\tlog.Warnf(\"Skipping result with nil Chart at path: %s\", result.ChartPath)\n\t\t\tcontinue\n\t\t}\n\n\t\t// With --no-dependencies, skip charts that are declared as dependencies of\n\t\t// some other discovered chart. Top-level charts are still processed.\n\t\tif noDeps && isDependencyChart[result.Chart.Name] {\n\t\t\tlog.Debugf(\"Skipping dependency chart %s (--no-dependencies)\", result.Chart.Name)\n\t\t\tcontinue\n\t\t}\n\n\t\tlog.Debugf(\"Processing result for chart: %s (%s)\", result.Chart.Name, result.ChartPath)\n\t\tif !noDeps {\n\t\t\tchartNameToResult[result.Chart.Name] = result\n\t\t\tlog.Debugf(\"Stored chart %s in chartNameToResult\", result.Chart.Name)\n\n\t\t\tif patches, ok := conditionsToPatch[result.Chart.Name]; ok {\n\t\t\t\tfor _, patch := range patches {\n\t\t\t\t\tschemaToPatch := &result.Schema\n\t\t\t\t\tlastIndex := len(patch) - 1\n\t\t\t\t\tfor i, key := range patch {\n\t\t\t\t\t\t// Ensure Properties map is initialized\n\t\t\t\t\t\tif schemaToPatch.Properties == nil {\n\t\t\t\t\t\t\tschemaToPatch.Properties = make(map[string]*schema.Schema)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif alreadyPresentSchema, ok := schemaToPatch.Properties[key]; !ok {\n\t\t\t\t\t\t\tlog.Debugf(\n\t\t\t\t\t\t\t\t\"Patching conditional field \\\"%s\\\" into schema of chart %s\",\n\t\t\t\t\t\t\t\tkey,\n\t\t\t\t\t\t\t\tresult.Chart.Name,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\tif i == lastIndex {\n\t\t\t\t\t\t\t\tschemaToPatch.Properties[key] = &schema.Schema{\n\t\t\t\t\t\t\t\t\tType:        []string{\"boolean\"},\n\t\t\t\t\t\t\t\t\tTitle:       key,\n\t\t\t\t\t\t\t\t\tDescription: \"Conditional property used in parent chart\",\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tschemaToPatch.Properties[key] = &schema.Schema{\n\t\t\t\t\t\t\t\t\tType:       []string{\"object\"},\n\t\t\t\t\t\t\t\t\tTitle:      key,\n\t\t\t\t\t\t\t\t\tProperties: make(map[string]*schema.Schema),\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tschemaToPatch = schemaToPatch.Properties[key]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tschemaToPatch = alreadyPresentSchema\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor _, dep := range result.Chart.Dependencies {\n\t\t\t\tif len(dependenciesFilterMap) > 0 && !dependenciesFilterMap[dep.Name] {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tif dep.Name != \"\" {\n\t\t\t\t\tif dependencyResult, ok := chartNameToResult[dep.Name]; ok {\n\t\t\t\t\t\tlog.Debugf(\n\t\t\t\t\t\t\t\"Found chart of dependency %s (%s)\",\n\t\t\t\t\t\t\tdependencyResult.Chart.Name,\n\t\t\t\t\t\t\tdependencyResult.ChartPath,\n\t\t\t\t\t\t)\n\n\t\t\t\t\t\t// Process import-values first (before regular dependency nesting)\n\t\t\t\t\t\timportedProps := processImportValues(\n\t\t\t\t\t\t\t&result.Schema,\n\t\t\t\t\t\t\t&dependencyResult.Schema,\n\t\t\t\t\t\t\tdep,\n\t\t\t\t\t\t\tresult.Chart.Name,\n\t\t\t\t\t\t)\n\t\t\t\t\t\thasImportValues := len(dep.ImportValues) > 0\n\n\t\t\t\t\t\t// Check if this is a library chart\n\t\t\t\t\t\tif dependencyResult.Chart.Type == \"library\" {\n\t\t\t\t\t\t\t// For library charts, merge properties directly into parent schema\n\t\t\t\t\t\t\tlog.Debugf(\"Merging library chart %s properties into parent chart %s at top level\", dep.Name, result.Chart.Name)\n\t\t\t\t\t\t\tmergeSchemaProperties(\n\t\t\t\t\t\t\t\t&result.Schema,\n\t\t\t\t\t\t\t\t&dependencyResult.Schema,\n\t\t\t\t\t\t\t\timportedProps,\n\t\t\t\t\t\t\t\tfmt.Sprintf(\"library chart %s\", dep.Name),\n\t\t\t\t\t\t\t\tfmt.Sprintf(\"parent chart %s\", result.Chart.Name),\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t} else if !hasImportValues {\n\t\t\t\t\t\t\t// For non-library charts WITHOUT import-values, nest under dependency name\n\t\t\t\t\t\t\t// (If import-values is used, user explicitly controls what's imported)\n\t\t\t\t\t\t\tdepSchema := schema.Schema{\n\t\t\t\t\t\t\t\tType:        []string{\"object\"},\n\t\t\t\t\t\t\t\tTitle:       dep.Name,\n\t\t\t\t\t\t\t\tDescription: dependencyResult.Chart.Description,\n\t\t\t\t\t\t\t\tProperties:  dependencyResult.Schema.Properties,\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif dep.Condition != \"\" && !strings.Contains(dep.Condition, \".\") {\n\t\t\t\t\t\t\t\tdepSchema.Type = []string{\"object\", \"boolean\"}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tdepSchema.DisableRequiredProperties()\n\n\t\t\t\t\t\t\tif dep.Alias != \"\" {\n\t\t\t\t\t\t\t\tresult.Schema.Properties[dep.Alias] = &depSchema\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tresult.Schema.Properties[dep.Name] = &depSchema\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlog.Warnf(\"Dependency (%s->%s) specified but no schema found. If you want to create jsonschemas for external dependencies, you need to run helm dep up\", result.Chart.Name, dep.Name)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tlog.Warnf(\"Dependency without name found (checkout %s).\", result.ChartPath)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Handle skip-dependencies-schema-validation flag\n\t\tif skipDepsSchemaValidation && !noDeps {\n\t\t\t// Collect dependency names using helper function\n\t\t\tdepNames := getDependencyNames(result.Chart.Dependencies, dependenciesFilterMap)\n\n\t\t\t// Remove dependency names from required properties\n\t\t\toldRequired := result.Schema.Required.Strings\n\t\t\tvar newRequired []string\n\t\t\tfor _, n := range oldRequired {\n\t\t\t\tif !slices.Contains(depNames, n) {\n\t\t\t\t\tnewRequired = append(newRequired, n)\n\t\t\t\t}\n\t\t\t}\n\t\t\tresult.Schema.Required.Strings = newRequired\n\n\t\t\t// Set additionalProperties to true for dependency schemas\n\t\t\tfor _, depName := range depNames {\n\t\t\t\tif prop, ok := result.Schema.Properties[depName]; ok && prop != nil {\n\t\t\t\t\tlog.Debugf(\"Setting additionalProperties to true for dependency %s in chart %s\", depName, result.Chart.Name)\n\t\t\t\t\tadditionalPropsTrue := true\n\t\t\t\t\tprop.AdditionalProperties = &additionalPropsTrue\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Hoist all nested definitions to the root level so $ref pointers resolve correctly\n\t\tresult.Schema.HoistDefinitions()\n\n\t\t// Skip writing output for dependency charts with pre-existing schema files\n\t\tif result.PreExistingSchema {\n\t\t\tlog.Debugf(\"Skipping output for dependency chart %s: using pre-existing schema\", result.Chart.Name)\n\t\t\tcontinue\n\t\t}\n\n\t\tjsonStr, err := result.Schema.ToJson()\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tif appendNewline {\n\t\t\tjsonStr = append(jsonStr, '\\n')\n\t\t}\n\n\t\tif dryRun {\n\t\t\tlog.Infof(\"Printing jsonschema for %s chart (%s)\", result.Chart.Name, result.ChartPath)\n\t\t\tif appendNewline {\n\t\t\t\tfmt.Printf(\"%s\", jsonStr)\n\t\t\t} else {\n\t\t\t\tfmt.Printf(\"%s\\n\", jsonStr)\n\t\t\t}\n\t\t} else {\n\t\t\tchartBasePath := filepath.Dir(result.ChartPath)\n\t\t\tif err := os.WriteFile(filepath.Join(chartBasePath, outFile), jsonStr, 0o644); err != nil {\n\t\t\t\terrs <- err\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t}\n\tif foundErrors {\n\t\treturn errors.New(\"some errors were found\")\n\t}\n\treturn nil\n}\n\nfunc main() {\n\tcommand, err := newCommand(exec)\n\tif err != nil {\n\t\tlog.Errorf(\"Failed to create the CLI commander: %s\", err)\n\t\tos.Exit(1)\n\t}\n\n\tif err := command.Execute(); err != nil {\n\t\tlog.Errorf(\"Execution error: %s\", err)\n\t\tos.Exit(1)\n\t}\n}\n"
  },
  {
    "path": "cmd/helm-schema/main_test.go",
    "content": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/spf13/viper\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype stringOrArray []string\n\nfunc (s *stringOrArray) UnmarshalJSON(value []byte) error {\n\tif len(value) == 0 {\n\t\treturn nil\n\t}\n\tif value[0] == '\"' {\n\t\tvar single string\n\t\tif err := json.Unmarshal(value, &single); err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*s = []string{single}\n\t\treturn nil\n\t}\n\tvar multi []string\n\tif err := json.Unmarshal(value, &multi); err != nil {\n\t\treturn err\n\t}\n\t*s = multi\n\treturn nil\n}\n\ntype schemaDoc struct {\n\tProperties map[string]schemaProperty `json:\"properties\"`\n}\n\ntype schemaProperty struct {\n\tType       stringOrArray              `json:\"type\"`\n\tProperties map[string]schemaProperty  `json:\"properties\"`\n}\n\nfunc TestExec_ConditionPatchingAndRootConditions(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\twriteFile := func(relPath, content string) {\n\t\tpath := filepath.Join(tmpDir, relPath)\n\t\terr := os.MkdirAll(filepath.Dir(path), 0o755)\n\t\tassert.NoError(t, err)\n\t\terr = os.WriteFile(path, []byte(content), 0o644)\n\t\tassert.NoError(t, err)\n\t}\n\n\twriteFile(\"dep/Chart.yaml\", `\napiVersion: v2\nname: dep\nversion: 1.0.0\n`)\n\twriteFile(\"dep/values.yaml\", `\nkey: value\n`)\n\n\twriteFile(\"dep2/Chart.yaml\", `\napiVersion: v2\nname: dep2\nversion: 1.0.0\n`)\n\twriteFile(\"dep2/values.yaml\", `\nkey: value\n`)\n\n\twriteFile(\"parent1/Chart.yaml\", `\napiVersion: v2\nname: parent1\nversion: 1.0.0\ndependencies:\n  - name: dep\n    version: 1.0.0\n    condition: dep.enabled\n  - name: dep2\n    version: 1.0.0\n    condition: dep2\n`)\n\twriteFile(\"parent1/values.yaml\", `\nroot: value\n`)\n\n\twriteFile(\"parent2/Chart.yaml\", `\napiVersion: v2\nname: parent2\nversion: 1.0.0\ndependencies:\n  - name: dep\n    version: 1.0.0\n    condition: dep.extra.flag\n`)\n\twriteFile(\"parent2/values.yaml\", `\nroot: value\n`)\n\n\tviper.Reset()\n\tviper.Set(\"chart-search-root\", tmpDir)\n\tviper.Set(\"dry-run\", false)\n\tviper.Set(\"no-dependencies\", false)\n\tviper.Set(\"add-schema-reference\", false)\n\tviper.Set(\"keep-full-comment\", false)\n\tviper.Set(\"helm-docs-compatibility-mode\", false)\n\tviper.Set(\"uncomment\", false)\n\tviper.Set(\"output-file\", \"values.schema.json\")\n\tviper.Set(\"dont-strip-helm-docs-prefix\", false)\n\tviper.Set(\"append-newline\", false)\n\tviper.Set(\"dependencies-filter\", []string{})\n\tviper.Set(\"dont-add-global\", true)\n\tviper.Set(\"skip-dependencies-schema-validation\", false)\n\tviper.Set(\"allow-circular-dependencies\", false)\n\tviper.Set(\"annotate\", false)\n\tviper.Set(\"keep-existing-dep-schemas\", false)\n\tviper.Set(\"value-files\", []string{\"values.yaml\"})\n\tviper.Set(\"skip-auto-generation\", []string{})\n\tviper.Set(\"log-level\", \"info\")\n\n\terr := exec(nil, nil)\n\tassert.NoError(t, err)\n\n\tdepSchemaPath := filepath.Join(tmpDir, \"dep\", \"values.schema.json\")\n\tdepSchemaBytes, err := os.ReadFile(depSchemaPath)\n\tassert.NoError(t, err)\n\n\tvar depSchema schemaDoc\n\terr = json.Unmarshal(depSchemaBytes, &depSchema)\n\tassert.NoError(t, err)\n\n\tenabledProp, ok := depSchema.Properties[\"enabled\"]\n\tassert.True(t, ok)\n\tassert.Contains(t, []string(enabledProp.Type), \"boolean\")\n\n\textraProp, ok := depSchema.Properties[\"extra\"]\n\tassert.True(t, ok)\n\tflagProp, ok := extraProp.Properties[\"flag\"]\n\tassert.True(t, ok)\n\tassert.Contains(t, []string(flagProp.Type), \"boolean\")\n\n\tparent1SchemaPath := filepath.Join(tmpDir, \"parent1\", \"values.schema.json\")\n\tparent1SchemaBytes, err := os.ReadFile(parent1SchemaPath)\n\tassert.NoError(t, err)\n\n\tvar parent1Schema schemaDoc\n\terr = json.Unmarshal(parent1SchemaBytes, &parent1Schema)\n\tassert.NoError(t, err)\n\n\tdep2Prop, ok := parent1Schema.Properties[\"dep2\"]\n\tassert.True(t, ok)\n\tassert.Contains(t, []string(dep2Prop.Type), \"object\")\n\tassert.Contains(t, []string(dep2Prop.Type), \"boolean\")\n}\n\nfunc TestExec_DependencyFilterSkipsConditionPatching(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\twriteFile := func(relPath, content string) {\n\t\tpath := filepath.Join(tmpDir, relPath)\n\t\terr := os.MkdirAll(filepath.Dir(path), 0o755)\n\t\tassert.NoError(t, err)\n\t\terr = os.WriteFile(path, []byte(content), 0o644)\n\t\tassert.NoError(t, err)\n\t}\n\n\twriteFile(\"dep/Chart.yaml\", `\napiVersion: v2\nname: dep\nversion: 1.0.0\n`)\n\twriteFile(\"dep/values.yaml\", `\nkey: value\n`)\n\n\twriteFile(\"dep2/Chart.yaml\", `\napiVersion: v2\nname: dep2\nversion: 1.0.0\n`)\n\twriteFile(\"dep2/values.yaml\", `\nkey: value\n`)\n\n\twriteFile(\"Chart.yaml\", `\napiVersion: v2\nname: parent\nversion: 1.0.0\ndependencies:\n  - name: dep\n    version: 1.0.0\n    condition: dep.enabled\n  - name: dep2\n    version: 1.0.0\n    condition: dep2.flag\n`)\n\twriteFile(\"values.yaml\", `\nroot: value\n`)\n\n\tviper.Reset()\n\tviper.Set(\"chart-search-root\", tmpDir)\n\tviper.Set(\"dry-run\", false)\n\tviper.Set(\"no-dependencies\", false)\n\tviper.Set(\"add-schema-reference\", false)\n\tviper.Set(\"keep-full-comment\", false)\n\tviper.Set(\"helm-docs-compatibility-mode\", false)\n\tviper.Set(\"uncomment\", false)\n\tviper.Set(\"output-file\", \"values.schema.json\")\n\tviper.Set(\"dont-strip-helm-docs-prefix\", false)\n\tviper.Set(\"append-newline\", false)\n\tviper.Set(\"dependencies-filter\", []string{\"dep\"})\n\tviper.Set(\"dont-add-global\", true)\n\tviper.Set(\"skip-dependencies-schema-validation\", false)\n\tviper.Set(\"allow-circular-dependencies\", false)\n\tviper.Set(\"annotate\", false)\n\tviper.Set(\"keep-existing-dep-schemas\", false)\n\tviper.Set(\"value-files\", []string{\"values.yaml\"})\n\tviper.Set(\"skip-auto-generation\", []string{})\n\tviper.Set(\"log-level\", \"info\")\n\n\terr := exec(nil, nil)\n\tassert.NoError(t, err)\n\n\tparentSchemaPath := filepath.Join(tmpDir, \"values.schema.json\")\n\tparentSchemaBytes, err := os.ReadFile(parentSchemaPath)\n\tassert.NoError(t, err)\n\n\tvar parentSchema schemaDoc\n\terr = json.Unmarshal(parentSchemaBytes, &parentSchema)\n\tassert.NoError(t, err)\n\n\t_, hasDep := parentSchema.Properties[\"dep\"]\n\t_, hasDep2 := parentSchema.Properties[\"dep2\"]\n\tassert.True(t, hasDep)\n\tassert.False(t, hasDep2)\n\n\tdepSchemaPath := filepath.Join(tmpDir, \"dep\", \"values.schema.json\")\n\tdepSchemaBytes, err := os.ReadFile(depSchemaPath)\n\tassert.NoError(t, err)\n\n\tvar depSchema schemaDoc\n\terr = json.Unmarshal(depSchemaBytes, &depSchema)\n\tassert.NoError(t, err)\n\n\tenabledProp, ok := depSchema.Properties[\"enabled\"]\n\tassert.True(t, ok)\n\tassert.Contains(t, []string(enabledProp.Type), \"boolean\")\n\n\tdep2SchemaPath := filepath.Join(tmpDir, \"dep2\", \"values.schema.json\")\n\t_, err = os.Stat(dep2SchemaPath)\n\tassert.True(t, os.IsNotExist(err))\n}\n\nfunc TestExec_DependencyAliasConditionPatching(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\twriteFile := func(relPath, content string) {\n\t\tpath := filepath.Join(tmpDir, relPath)\n\t\terr := os.MkdirAll(filepath.Dir(path), 0o755)\n\t\tassert.NoError(t, err)\n\t\terr = os.WriteFile(path, []byte(content), 0o644)\n\t\tassert.NoError(t, err)\n\t}\n\n\twriteFile(\"dep/Chart.yaml\", `\napiVersion: v2\nname: dep\nversion: 1.0.0\n`)\n\twriteFile(\"dep/values.yaml\", `\nkey: value\n`)\n\n\twriteFile(\"Chart.yaml\", `\napiVersion: v2\nname: parent\nversion: 1.0.0\ndependencies:\n  - name: dep\n    alias: depalias\n    version: 1.0.0\n    condition: depalias.enabled\n`)\n\twriteFile(\"values.yaml\", `\nroot: value\n`)\n\n\tviper.Reset()\n\tviper.Set(\"chart-search-root\", tmpDir)\n\tviper.Set(\"dry-run\", false)\n\tviper.Set(\"no-dependencies\", false)\n\tviper.Set(\"add-schema-reference\", false)\n\tviper.Set(\"keep-full-comment\", false)\n\tviper.Set(\"helm-docs-compatibility-mode\", false)\n\tviper.Set(\"uncomment\", false)\n\tviper.Set(\"output-file\", \"values.schema.json\")\n\tviper.Set(\"dont-strip-helm-docs-prefix\", false)\n\tviper.Set(\"append-newline\", false)\n\tviper.Set(\"dependencies-filter\", []string{})\n\tviper.Set(\"dont-add-global\", true)\n\tviper.Set(\"skip-dependencies-schema-validation\", false)\n\tviper.Set(\"allow-circular-dependencies\", false)\n\tviper.Set(\"annotate\", false)\n\tviper.Set(\"keep-existing-dep-schemas\", false)\n\tviper.Set(\"value-files\", []string{\"values.yaml\"})\n\tviper.Set(\"skip-auto-generation\", []string{})\n\tviper.Set(\"log-level\", \"info\")\n\n\terr := exec(nil, nil)\n\tassert.NoError(t, err)\n\n\tdepSchemaPath := filepath.Join(tmpDir, \"dep\", \"values.schema.json\")\n\tdepSchemaBytes, err := os.ReadFile(depSchemaPath)\n\tassert.NoError(t, err)\n\n\tvar depSchema schemaDoc\n\terr = json.Unmarshal(depSchemaBytes, &depSchema)\n\tassert.NoError(t, err)\n\n\tenabledProp, ok := depSchema.Properties[\"enabled\"]\n\tassert.True(t, ok)\n\tassert.Contains(t, []string(enabledProp.Type), \"boolean\")\n\n\tparentSchemaPath := filepath.Join(tmpDir, \"values.schema.json\")\n\tparentSchemaBytes, err := os.ReadFile(parentSchemaPath)\n\tassert.NoError(t, err)\n\n\tvar parentSchema schemaDoc\n\terr = json.Unmarshal(parentSchemaBytes, &parentSchema)\n\tassert.NoError(t, err)\n\n\t_, ok = parentSchema.Properties[\"depalias\"]\n\tassert.True(t, ok)\n}\n\nfunc TestExec_KeepExistingDepSchemasPreservesDependencySchema(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\twriteFile := func(relPath, content string) {\n\t\tpath := filepath.Join(tmpDir, relPath)\n\t\terr := os.MkdirAll(filepath.Dir(path), 0o755)\n\t\tassert.NoError(t, err)\n\t\terr = os.WriteFile(path, []byte(content), 0o644)\n\t\tassert.NoError(t, err)\n\t}\n\n\twriteFile(\"dep/Chart.yaml\", `\napiVersion: v2\nname: dep\nversion: 1.0.0\n`)\n\twriteFile(\"dep/values.yaml\", `\nport: 8080\n`)\n\tpreExistingDepSchema := `{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"port\": {\n      \"type\": \"integer\",\n      \"description\": \"The port to listen on\",\n      \"minimum\": 1,\n      \"maximum\": 65535,\n      \"x-custom-annotation\": \"preserve-me\"\n    }\n  },\n  \"required\": [\"port\"]\n}\n`\n\twriteFile(\"dep/values.schema.json\", preExistingDepSchema)\n\n\twriteFile(\"parent/Chart.yaml\", `\napiVersion: v2\nname: parent\nversion: 1.0.0\ndependencies:\n  - name: dep\n    version: 1.0.0\n    repository: file://../dep\n`)\n\twriteFile(\"parent/values.yaml\", `\nroot: value\n`)\n\n\tviper.Reset()\n\tviper.Set(\"chart-search-root\", tmpDir)\n\tviper.Set(\"dry-run\", false)\n\tviper.Set(\"no-dependencies\", false)\n\tviper.Set(\"add-schema-reference\", false)\n\tviper.Set(\"keep-full-comment\", false)\n\tviper.Set(\"helm-docs-compatibility-mode\", false)\n\tviper.Set(\"uncomment\", false)\n\tviper.Set(\"output-file\", \"values.schema.json\")\n\tviper.Set(\"dont-strip-helm-docs-prefix\", false)\n\tviper.Set(\"append-newline\", false)\n\tviper.Set(\"dependencies-filter\", []string{})\n\tviper.Set(\"dont-add-global\", true)\n\tviper.Set(\"skip-dependencies-schema-validation\", false)\n\tviper.Set(\"allow-circular-dependencies\", false)\n\tviper.Set(\"annotate\", false)\n\tviper.Set(\"keep-existing-dep-schemas\", true)\n\tviper.Set(\"value-files\", []string{\"values.yaml\"})\n\tviper.Set(\"skip-auto-generation\", []string{})\n\tviper.Set(\"log-level\", \"info\")\n\n\terr := exec(nil, nil)\n\tassert.NoError(t, err)\n\n\tdepSchemaPath := filepath.Join(tmpDir, \"dep\", \"values.schema.json\")\n\tdepSchemaBytes, err := os.ReadFile(depSchemaPath)\n\tassert.NoError(t, err)\n\tassert.Equal(t, preExistingDepSchema, string(depSchemaBytes),\n\t\t\"dependency's pre-existing values.schema.json must not be overwritten when --keep-existing-dep-schemas is set\")\n\n\tparentSchemaPath := filepath.Join(tmpDir, \"parent\", \"values.schema.json\")\n\tparentSchemaBytes, err := os.ReadFile(parentSchemaPath)\n\tassert.NoError(t, err)\n\n\tvar parentRaw map[string]any\n\terr = json.Unmarshal(parentSchemaBytes, &parentRaw)\n\tassert.NoError(t, err)\n\n\tprops, _ := parentRaw[\"properties\"].(map[string]any)\n\tdepNode, ok := props[\"dep\"].(map[string]any)\n\tassert.True(t, ok, \"parent must nest dependency schema under dep key\")\n\tdepProps, _ := depNode[\"properties\"].(map[string]any)\n\tportNode, ok := depProps[\"port\"].(map[string]any)\n\tassert.True(t, ok, \"parent must include port from merged dependency schema\")\n\tassert.Equal(t, \"preserve-me\", portNode[\"x-custom-annotation\"],\n\t\t\"pre-existing x-* annotations must be carried into the merged parent schema\")\n}\n\nfunc TestExec_DefaultRegeneratesDependencySchema(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\twriteFile := func(relPath, content string) {\n\t\tpath := filepath.Join(tmpDir, relPath)\n\t\terr := os.MkdirAll(filepath.Dir(path), 0o755)\n\t\tassert.NoError(t, err)\n\t\terr = os.WriteFile(path, []byte(content), 0o644)\n\t\tassert.NoError(t, err)\n\t}\n\n\twriteFile(\"dep/Chart.yaml\", `\napiVersion: v2\nname: dep\nversion: 1.0.0\n`)\n\twriteFile(\"dep/values.yaml\", `\nport: 8080\n`)\n\tpreExistingDepSchema := `{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"port\": {\n      \"type\": \"integer\",\n      \"x-custom-annotation\": \"should-be-lost\"\n    }\n  }\n}\n`\n\twriteFile(\"dep/values.schema.json\", preExistingDepSchema)\n\n\twriteFile(\"parent/Chart.yaml\", `\napiVersion: v2\nname: parent\nversion: 1.0.0\ndependencies:\n  - name: dep\n    version: 1.0.0\n    repository: file://../dep\n`)\n\twriteFile(\"parent/values.yaml\", `\nroot: value\n`)\n\n\tviper.Reset()\n\tviper.Set(\"chart-search-root\", tmpDir)\n\tviper.Set(\"dry-run\", false)\n\tviper.Set(\"no-dependencies\", false)\n\tviper.Set(\"add-schema-reference\", false)\n\tviper.Set(\"keep-full-comment\", false)\n\tviper.Set(\"helm-docs-compatibility-mode\", false)\n\tviper.Set(\"uncomment\", false)\n\tviper.Set(\"output-file\", \"values.schema.json\")\n\tviper.Set(\"dont-strip-helm-docs-prefix\", false)\n\tviper.Set(\"append-newline\", false)\n\tviper.Set(\"dependencies-filter\", []string{})\n\tviper.Set(\"dont-add-global\", true)\n\tviper.Set(\"skip-dependencies-schema-validation\", false)\n\tviper.Set(\"allow-circular-dependencies\", false)\n\tviper.Set(\"annotate\", false)\n\tviper.Set(\"keep-existing-dep-schemas\", false)\n\tviper.Set(\"value-files\", []string{\"values.yaml\"})\n\tviper.Set(\"skip-auto-generation\", []string{})\n\tviper.Set(\"log-level\", \"info\")\n\n\terr := exec(nil, nil)\n\tassert.NoError(t, err)\n\n\tdepSchemaPath := filepath.Join(tmpDir, \"dep\", \"values.schema.json\")\n\tdepSchemaBytes, err := os.ReadFile(depSchemaPath)\n\tassert.NoError(t, err)\n\tassert.NotEqual(t, preExistingDepSchema, string(depSchemaBytes),\n\t\t\"default mode must regenerate a dependency's values.schema.json\")\n}\n\nfunc TestExec_NoDependenciesSkipsDependencyCharts(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\twriteFile := func(relPath, content string) {\n\t\tpath := filepath.Join(tmpDir, relPath)\n\t\terr := os.MkdirAll(filepath.Dir(path), 0o755)\n\t\tassert.NoError(t, err)\n\t\terr = os.WriteFile(path, []byte(content), 0o644)\n\t\tassert.NoError(t, err)\n\t}\n\n\twriteFile(\"foo/Chart.yaml\", `\napiVersion: v2\nname: foo\nversion: 1.0.0\ndependencies:\n  - name: bar\n    version: 1.0.0\n    repository: file://./bar\n`)\n\twriteFile(\"foo/values.yaml\", `\ntop: 1\n`)\n\twriteFile(\"foo/bar/Chart.yaml\", `\napiVersion: v2\nname: bar\nversion: 1.0.0\n`)\n\twriteFile(\"foo/bar/values.yaml\", `\ninside: 2\n`)\n\n\tviper.Reset()\n\tviper.Set(\"chart-search-root\", filepath.Join(tmpDir, \"foo\"))\n\tviper.Set(\"dry-run\", false)\n\tviper.Set(\"no-dependencies\", true)\n\tviper.Set(\"add-schema-reference\", false)\n\tviper.Set(\"keep-full-comment\", false)\n\tviper.Set(\"helm-docs-compatibility-mode\", false)\n\tviper.Set(\"uncomment\", false)\n\tviper.Set(\"output-file\", \"values.schema.json\")\n\tviper.Set(\"dont-strip-helm-docs-prefix\", false)\n\tviper.Set(\"append-newline\", false)\n\tviper.Set(\"dependencies-filter\", []string{})\n\tviper.Set(\"dont-add-global\", true)\n\tviper.Set(\"skip-dependencies-schema-validation\", false)\n\tviper.Set(\"allow-circular-dependencies\", false)\n\tviper.Set(\"annotate\", false)\n\tviper.Set(\"keep-existing-dep-schemas\", false)\n\tviper.Set(\"value-files\", []string{\"values.yaml\"})\n\tviper.Set(\"skip-auto-generation\", []string{})\n\tviper.Set(\"log-level\", \"info\")\n\n\terr := exec(nil, nil)\n\tassert.NoError(t, err)\n\n\tparentSchemaPath := filepath.Join(tmpDir, \"foo\", \"values.schema.json\")\n\t_, err = os.Stat(parentSchemaPath)\n\tassert.NoError(t, err, \"parent chart schema must still be generated with --no-dependencies\")\n\n\tdepSchemaPath := filepath.Join(tmpDir, \"foo\", \"bar\", \"values.schema.json\")\n\t_, err = os.Stat(depSchemaPath)\n\tassert.True(t, os.IsNotExist(err),\n\t\t\"dependency chart schema must not be generated with --no-dependencies (issue #215)\")\n}\n"
  },
  {
    "path": "cmd/helm-schema/version.go",
    "content": "package main\n\nvar version string = \"0.23.2\"\n"
  },
  {
    "path": "examples/Chart.yaml",
    "content": "apiVersion: v2\nname: example\ndescription: A Helm chart for Kubernetes\n\n# A chart can be either an 'application' or a 'library' chart.\n#\n# Application charts are a collection of templates that can be packaged into versioned archives\n# to be deployed.\n#\n# Library charts provide useful utilities or functions for the chart developer. They're included as\n# a dependency of application charts to inject those utilities and functions into the rendering\n# pipeline. Library charts do not define any templates and therefore cannot be deployed.\ntype: application\n\n# This is the chart version. This version number should be incremented each time you make changes\n# to the chart and its templates, including the app version.\n# Versions are expected to follow Semantic Versioning (https://semver.org/)\nversion: 0.1.0\n\n# This is the version number of the application being deployed. This version number should be\n# incremented each time you make changes to the application. Versions are not expected to\n# follow Semantic Versioning. They should reflect the version the application is using.\n# It is recommended to use it with quotes.\nappVersion: \"1.16.0\"\n"
  },
  {
    "path": "examples/values.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"global\": {\n      \"description\": \"Global values are values that can be accessed from any chart or subchart by exactly the same name.\",\n      \"required\": [],\n      \"title\": \"global\",\n      \"type\": \"object\"\n    },\n    \"service\": {\n      \"additionalProperties\": true,\n      \"properties\": {\n        \"conf\": {\n          \"additionalProperties\": false,\n          \"examples\": [\n            \"API_PROVIDER_ONE: api-key-x\",\n            \"EMAIL_ADMIN: admin@example.org\"\n          ],\n          \"patternProperties\": {\n            \"^API_.*\": {\n              \"pattern\": \"^api-key\",\n              \"type\": \"string\"\n            },\n            \"^EMAIL_.*\": {\n              \"format\": \"idn-email\",\n              \"type\": \"string\"\n            }\n          },\n          \"required\": [],\n          \"title\": \"conf\",\n          \"type\": \"object\"\n        },\n        \"contact\": {\n          \"default\": \"\",\n          \"examples\": [\n            \"name@domain.tld\"\n          ],\n          \"format\": \"idn-email\",\n          \"required\": [],\n          \"title\": \"contact\"\n        },\n        \"enabled\": {\n          \"default\": true,\n          \"description\": \"Type will be parsed as boolean\",\n          \"title\": \"enabled\",\n          \"type\": \"boolean\"\n        },\n        \"env\": {\n          \"additionalProperties\": false,\n          \"description\": \"Environment variables. If you want to provide auto-completion to the user\",\n          \"properties\": {\n            \"ADMIN_EMAIL\": {\n              \"examples\": [\n                \"admin@example.org\"\n              ],\n              \"format\": \"idn-email\",\n              \"title\": \"ADMIN_EMAIL\",\n              \"type\": \"string\"\n            },\n            \"API_URL\": {\n              \"examples\": [\n                \"https://api.example.org\"\n              ],\n              \"format\": \"idn-hostname\",\n              \"title\": \"API_URL\",\n              \"type\": \"string\"\n            },\n            \"CONFIG_PATH\": {\n              \"description\": \"The local path to the service configuration file\",\n              \"examples\": [\n                \"/path/to/config\"\n              ],\n              \"title\": \"CONFIG_PATH\",\n              \"type\": \"string\"\n            }\n          },\n          \"required\": [],\n          \"title\": \"env\"\n        },\n        \"externalIP\": {\n          \"default\": \"443\",\n          \"enum\": [\n            443,\n            80\n          ],\n          \"required\": [],\n          \"title\": \"externalIP\"\n        },\n        \"hosts\": {\n          \"description\": \"Will give auto-completion for the below structure\\nhosts:\\n - name:\\n     url: my.example.org\",\n          \"items\": {\n            \"properties\": {\n              \"host\": {\n                \"properties\": {\n                  \"url\": {\n                    \"format\": \"idn-hostname\",\n                    \"type\": \"string\"\n                  }\n                },\n                \"required\": [],\n                \"type\": \"object\"\n              }\n            },\n            \"required\": [],\n            \"type\": \"object\"\n          },\n          \"title\": \"hosts\",\n          \"type\": \"array\"\n        },\n        \"maintainer\": {\n          \"const\": \"maintainer@example.org\",\n          \"default\": \"maintainer@example.org\",\n          \"required\": [],\n          \"title\": \"maintainer\"\n        },\n        \"name\": {\n          \"anyOf\": [\n            {\n              \"required\": []\n            },\n            {\n              \"pattern\": \"^foo-\",\n              \"required\": []\n            }\n          ],\n          \"default\": \"\",\n          \"description\": \"Name of the deployed service. Defined in the schema annotation\",\n          \"required\": [],\n          \"title\": \"name\"\n        },\n        \"otherExternalIP\": {\n          \"default\": 443,\n          \"examples\": [\n            443,\n            80\n          ],\n          \"title\": \"otherExternalIP\",\n          \"type\": \"integer\"\n        },\n        \"port\": {\n          \"default\": 80,\n          \"maximum\": 89,\n          \"minimum\": 80,\n          \"title\": \"port\",\n          \"type\": \"integer\"\n        },\n        \"storage\": {\n          \"default\": \"10Gib\",\n          \"examples\": [\n            \"5Gib\",\n            \"10Gib\",\n            \"20Gib\"\n          ],\n          \"pattern\": \"^[1-9][0-9]*Gib$\",\n          \"title\": \"storage\",\n          \"type\": \"string\"\n        },\n        \"telemetry\": {\n          \"default\": true,\n          \"title\": \"telemetry\",\n          \"type\": \"boolean\"\n        },\n        \"type\": {\n          \"default\": \"application\",\n          \"enum\": [\n            \"application\",\n            \"controller\",\n            \"api\"\n          ],\n          \"required\": [],\n          \"title\": \"type\"\n        }\n      },\n      \"required\": [\n        \"enabled\"\n      ],\n      \"title\": \"service\"\n    }\n  },\n  \"required\": [],\n  \"type\": \"object\"\n}"
  },
  {
    "path": "examples/values.yaml",
    "content": "# vim: set ft=yaml:\n# yaml-language-server: $schema=values.schema.json\n\n# This is an example values.yaml file, it aims to show how to annotate the keys, and it's its only purpose.\n# The corresponding values.schema.json has been generated with the below command.\n# helm-schema -n -k additionalProperties\n\n# @schema\n# additionalProperties: true\n# @schema\nservice:\n  # Type will be parsed as boolean\n  enabled: true\n\n  # @schema\n  # type: integer\n  # minimum: 80\n  # maximum: 89\n  # @schema\n  port: 80\n\n  # @schema\n  # enum: [443, 80]\n  # @schema\n  externalIP: 443\n\n  # @schema\n  # type: integer\n  # examples: [443, 80]\n  # @schema\n  otherExternalIP: 443\n\n  # @schema\n  # description: Name of the deployed service. Defined in the schema annotation\n  # anyOf:\n  #   - type: null\n  #   - pattern: ^foo-\n  # @schema\n  # This comment will not be parsed as 'description', the 'description' field take precedence over comments\n  name:\n\n  # @schema\n  # properties:\n  #   CONFIG_PATH:\n  #     title: CONFIG_PATH\n  #     type: string\n  #     description: The local path to the service configuration file\n  #     examples: [/path/to/config]\n  #   ADMIN_EMAIL:\n  #     title: ADMIN_EMAIL\n  #     type: string\n  #     format: idn-email\n  #     examples: [admin@example.org]\n  #   API_URL:\n  #     title: API_URL\n  #     type: string\n  #     format: idn-hostname\n  #     examples: [https://api.example.org]\n  # @schema\n  # -- Environment variables. If you want to provide auto-completion to the user\n  env: {}\n\n  # @schema\n  # type: object\n  # patternProperties:\n  #   \"^API_.*\":\n  #     type: string\n  #     pattern: ^api-key\n  #   \"^EMAIL_.*\":\n  #     type: string\n  #     format: idn-email\n  # examples: [\"API_PROVIDER_ONE: api-key-x\",\"EMAIL_ADMIN: admin@example.org\"]\n  # @schema\n  conf: {}\n\n  # @schema\n  # format: idn-email\n  # examples: [name@domain.tld]\n  # @schema\n  contact: \"\"\n\n  # @schema\n  # type: boolean\n  # default: true\n  # @schema\n  telemetry: true\n\n  # @schema\n  # type: array\n  # items:\n  #   type: object\n  #   properties:\n  #     host:\n  #       type: object\n  #       properties:\n  #         url:\n  #           type: string\n  #           format: idn-hostname\n  # @schema\n  # Will give auto-completion for the below structure\n  # hosts:\n  #  - name:\n  #      url: my.example.org\n  hosts: []\n\n  # @schema\n  # enum:\n  # - application\n  # - controller\n  # - api\n  # @schema\n  type: application\n\n  # @schema\n  # const: maintainer@example.org\n  # @schema\n  maintainer: maintainer@example.org\n\n  # @schema\n  # type: string\n  # pattern: ^[1-9][0-9]*Gib$\n  # examples: [\"5Gib\",\"10Gib\",\"20Gib\"]\n  # @schema\n  storage: \"10Gib\"\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/dadav/helm-schema\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/dadav/go-jsonpointer v0.0.0-20240918181927-335cbee8c279\n\tgithub.com/magiconair/properties v1.8.10\n\tgithub.com/norwoodj/helm-docs v1.14.2\n\tgithub.com/santhosh-tekuri/jsonschema/v6 v6.0.2\n\tgithub.com/sirupsen/logrus v1.9.4\n\tgithub.com/spf13/cobra v1.10.2\n\tgithub.com/spf13/viper v1.21.0\n\tgithub.com/stretchr/testify v1.11.1\n\tgopkg.in/yaml.v3 v3.0.1\n)\n\nrequire (\n\tdario.cat/mergo v1.0.2 // indirect\n\tgithub.com/Masterminds/goutils v1.1.1 // indirect\n\tgithub.com/Masterminds/semver/v3 v3.4.0 // indirect\n\tgithub.com/Masterminds/sprig/v3 v3.3.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/fsnotify/fsnotify v1.9.0 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.5.0 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/huandu/xstrings v1.5.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/mitchellh/copystructure v1.2.0 // indirect\n\tgithub.com/mitchellh/reflectwalk v1.0.2 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.3.0 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/sagikazarmark/locafero v0.12.0 // indirect\n\tgithub.com/shopspring/decimal v1.4.0 // indirect\n\tgithub.com/spf13/afero v1.15.0 // indirect\n\tgithub.com/spf13/cast v1.10.0 // indirect\n\tgithub.com/spf13/pflag v1.0.10 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/crypto v0.49.0 // indirect\n\tgolang.org/x/sys v0.42.0 // indirect\n\tgolang.org/x/text v0.35.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect\n\thelm.sh/helm/v3 v3.20.2 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=\ngithub.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=\ngithub.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=\ngithub.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=\ngithub.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=\ngithub.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=\ngithub.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/dadav/go-jsonpointer v0.0.0-20240918181927-335cbee8c279 h1:GxxxrAMM6YJK4Tf7c6APwkhXzzpJm2hGeWKpD3OKpHY=\ngithub.com/dadav/go-jsonpointer v0.0.0-20240918181927-335cbee8c279/go.mod h1:JoQhlwhuWFnmxzDNgByW0ONYJZjQGnUnRu9H7+C0lTU=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=\ngithub.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=\ngithub.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=\ngithub.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=\ngithub.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=\ngithub.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=\ngithub.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=\ngithub.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=\ngithub.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=\ngithub.com/norwoodj/helm-docs v1.14.2 h1:Ew3bCq1hZqMnnTopkk66Uy2mGwu/jAclAx+3JAVp1To=\ngithub.com/norwoodj/helm-docs v1.14.2/go.mod h1:qdo76rorOkPDme8nsV5e0JBAYrs56kzvZMYW83k1kgc=\ngithub.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=\ngithub.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=\ngithub.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=\ngithub.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=\ngithub.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=\ngithub.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=\ngithub.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=\ngithub.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=\ngithub.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=\ngithub.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=\ngithub.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=\ngithub.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=\ngithub.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=\ngithub.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=\ngolang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=\ngolang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhelm.sh/helm/v3 v3.20.2 h1:binM4rvPx5DcNsa1sIt7UZi55lRbu3pZUFmQkSoRh48=\nhelm.sh/helm/v3 v3.20.2/go.mod h1:Fl1kBaWCpkUrM6IYXPjQ3bdZQfFrogKArqptvueZ6Ww=\n"
  },
  {
    "path": "install-binary.sh",
    "content": "#!/usr/bin/env sh\n\n# Shamelessly copied from https://github.com/technosophos/helm-template/blob/master/install-binary.sh\n\nPROJECT_NAME=\"helm-schema\"\nBINARY_NAME=\"helm-schema\"\nPROJECT_GH=\"dadav/$PROJECT_NAME\"\nPLUGIN_MANIFEST=\"plugin.yaml\"\n\n# Convert HELM_BIN and HELM_PLUGIN_DIR to unix if cygpath is\n# available. This is the case when using MSYS2 or Cygwin\n# on Windows where helm returns a Windows path but we\n# need a Unix path\n\nif command -v cygpath >/dev/null 2>&1; then\n  HELM_BIN=\"$(cygpath -u \"${HELM_BIN}\")\"\n  HELM_PLUGIN_DIR=\"$(cygpath -u \"${HELM_PLUGIN_DIR}\")\"\nfi\n\n[ -z \"$HELM_BIN\" ] && HELM_BIN=$(command -v helm)\n\n[ -z \"$HELM_HOME\" ] && HELM_HOME=$(helm env | grep 'HELM_DATA_HOME' | cut -d '=' -f2 | tr -d '\"')\n\nmkdir -p \"$HELM_HOME\"\n\nif [ \"$SKIP_BIN_INSTALL\" = \"1\" ]; then\n  echo \"Skipping binary install\"\n  exit\nfi\n\n# which mode is the common installer script running in.\nSCRIPT_MODE=\"install\"\nif [ \"$1\" = \"-u\" ]; then\n  SCRIPT_MODE=\"update\"\nfi\n\n# initArch discovers the architecture for this system.\ninitArch() {\n  ARCH=$(uname -m)\n  case $ARCH in\n  armv6*) ARCH=\"armv6\" ;;\n  armv7*) ARCH=\"armv7\" ;;\n  aarch64 | arm64) ARCH=\"arm64\" ;;\n  x86_64 | amd64) ARCH=\"x86_64\" ;;\n  *)\n    echo \"Arch '$(uname -m)' not supported!\" >&2\n    exit 1\n    ;;\n  esac\n}\n\n# initOS discovers the operating system for this system.\ninitOS() {\n  OS=$(uname -s)\n\n  case \"$OS\" in\n  Windows_NT) OS='Windows' ;;\n  # Msys support\n  MSYS*) OS='Windows' ;;\n  # Minimalist GNU for Windows\n  MINGW*) OS='Windows' ;;\n  CYGWIN*) OS='Windows' ;;\n  Darwin) OS='Darwin' ;;\n  Linux) OS='Linux' ;;\n  *)\n    echo \"OS '$(uname)' not supported!\" >&2\n    exit 1\n    ;;\n  esac\n}\n\n# verifySupported checks that the os/arch combination is supported for binary builds.\nverifySupported() {\n  supported=\"Linux-x86_64\\nLinux-arm64\\nLinux-armv6\\nLinux-armv7\\nDarwin-x86_64\\nDarwin-arm64\\nWindows-x86_64\\nWindows-arm64\\nWindows-armv6\\nWindows-armv7\"\n  if ! echo \"${supported}\" | grep -q \"${OS}-${ARCH}\"; then\n    echo \"No prebuild binary for ${OS}-${ARCH}.\"\n    exit 1\n  fi\n\n  if\n    ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1\n  then\n    echo \"Either curl or wget is required\"\n    exit 1\n  fi\n}\n\n# getDownloadURL retrieves the download URL and checksum URL.\ngetDownloadURL() {\n  version=\"$(grep <\"$HELM_PLUGIN_DIR/$PLUGIN_MANIFEST\" \"version\" | cut -d '\"' -f 2)\"\n  ext=\"tar.gz\"\n  if [ \"$OS\" = \"Windows\" ]; then\n    ext=\"zip\"\n  fi\n  if [ \"$SCRIPT_MODE\" = \"install\" ] && [ -n \"$version\" ]; then\n    DOWNLOAD_URL=\"https://github.com/${PROJECT_GH}/releases/download/${version}/${PROJECT_NAME}_${version}_${OS}_${ARCH}.${ext}\"\n    CHECKSUM_URL=\"https://github.com/${PROJECT_GH}/releases/download/${version}/checksums.txt\"\n  else\n    DOWNLOAD_URL=\"https://github.com/${PROJECT_GH}/releases/latest/download/${PROJECT_NAME}_${version}_${OS}_${ARCH}.${ext}\"\n    CHECKSUM_URL=\"https://github.com/${PROJECT_GH}/releases/latest/download/checksums.txt\"\n  fi\n}\n\n# Temporary dir\nmkTempDir() {\n  HELM_TMP=\"$(mktemp -d -t \"${PROJECT_NAME}-XXXXXX\")\"\n}\n\nrmTempDir() {\n  if [ -d \"${HELM_TMP:-/tmp/helm-schema}\" ]; then\n    rm -rf \"${HELM_TMP:-/tmp/helm-schema}\"\n  fi\n}\n\n# downloadFile downloads the latest binary package and the checksum.\ndownloadFile() {\n  PLUGIN_TMP_FILE=\"${HELM_TMP}/${PROJECT_NAME}.tar.gz\"\n  PLUGIN_CHECKSUMS_FILE=\"${HELM_TMP}/${PROJECT_NAME}_checksums.txt\"\n  echo \"Downloading ...\"\n  echo \"$DOWNLOAD_URL\"\n  echo \"$CHECKSUM_URL\"\n  if\n    command -v curl >/dev/null 2>&1\n  then\n    curl -sSf -L \"$DOWNLOAD_URL\" >\"$PLUGIN_TMP_FILE\"\n    curl -sSf -L \"$CHECKSUM_URL\" >\"$PLUGIN_CHECKSUMS_FILE\"\n  elif\n    command -v wget >/dev/null 2>&1\n  then\n    wget -q -O - \"$DOWNLOAD_URL\" >\"$PLUGIN_TMP_FILE\"\n    wget -q -O - \"$CHECKSUM_URL\" >\"$PLUGIN_CHECKSUMS_FILE\"\n  fi\n}\n\nvalidateChecksum() {\n  if ! grep -q ${1} ${2}; then\n    echo \"Invalid checksum\" >/dev/stderr\n    exit 1\n  fi\n  echo \"Checksum is valid.\"\n}\n\n# installFile verifies the SHA256 for the file, then unpacks and installs it.\ninstallFile() {\n  if command -v sha256sum >/dev/null 2>&1; then\n    checksum=$(sha256sum ${PLUGIN_TMP_FILE} | awk '{ print $1 }')\n    validateChecksum ${checksum} ${PLUGIN_CHECKSUMS_FILE}\n  elif command -v openssl >/dev/null 2>&1; then\n    checksum=$(openssl dgst -sha256 ${PLUGIN_TMP_FILE} | awk '{ print $2 }')\n    validateChecksum ${checksum} ${PLUGIN_CHECKSUMS_FILE}\n  else\n    echo \"WARNING: no tool found to verify checksum\" >/dev/stderr\n  fi\n\n  HELM_TMP_BIN=\"$HELM_TMP/$BINARY_NAME\"\n  if [ \"${OS}\" = \"Windows\" ]; then\n    HELM_TMP_BIN=\"$HELM_TMP_BIN.exe\"\n    unzip \"$PLUGIN_TMP_FILE\" -d \"$HELM_TMP\"\n  else\n    tar xzf \"$PLUGIN_TMP_FILE\" -C \"$HELM_TMP\"\n  fi\n  echo \"Preparing to install into ${HELM_PLUGIN_DIR}\"\n  mkdir -p \"$HELM_PLUGIN_DIR/bin\"\n  cp \"$HELM_TMP_BIN\" \"$HELM_PLUGIN_DIR/bin\"\n}\n\n# exit_trap is executed if on exit (error or not).\nexit_trap() {\n  result=$?\n  rmTempDir\n  if [ \"$result\" != \"0\" ]; then\n    echo \"Failed to install $PROJECT_NAME\"\n    printf \"\\tFor support, go to https://github.com/%s.\\n\" \"$PROJECT_GH\"\n  fi\n  exit $result\n}\n\n# testVersion tests the installed client to make sure it is working.\ntestVersion() {\n  set +e\n  echo \"$PROJECT_NAME installed into $HELM_PLUGIN_DIR\"\n  \"${HELM_PLUGIN_DIR}/bin/$BINARY_NAME\" --version\n  set -e\n}\n\n# Execution\n\n#Stop execution on any error\ntrap \"exit_trap\" EXIT\nset -e\ninitArch\ninitOS\nverifySupported\ngetDownloadURL\nmkTempDir\ndownloadFile\ninstallFile\ntestVersion\n"
  },
  {
    "path": "pkg/chart/chart.go",
    "content": "package chart\n\nimport (\n\t\"io\"\n\n\t\"github.com/dadav/helm-schema/pkg/util\"\n\tyaml \"gopkg.in/yaml.v3\"\n)\n\ntype Dependency struct {\n\tName         string        `yaml:\"name\"`\n\tVersion      string        `yaml:\"version\"`\n\tCondition    string        `yaml:\"condition,omitempty\"`\n\tRepository   string        `yaml:\"repository,omitempty\"`\n\tAlias        string        `yaml:\"alias,omitempty\"`\n\tImportValues []interface{} `yaml:\"import-values,omitempty\"`\n\t// Tags         []string `yaml:\"tags,omitempty\"`\n}\n\n// Maintainer describes a Chart maintainer.\n// https://github.com/helm/helm/blob/main/pkg/chart/metadata.go#L26C1-L34C2\ntype Maintainer struct {\n\t// Name is a user name or organization name\n\tName string `json:\"name,omitempty\"`\n\t// Email is an optional email address to contact the named maintainer\n\tEmail string `json:\"email,omitempty\"`\n\t// URL is an optional URL to an address for the named maintainer\n\tURL string `json:\"url,omitempty\"`\n}\n\n// https://github.com/helm/helm/blob/main/pkg/chart/metadata.go#L48\ntype ChartFile struct {\n\t// The name of the chart. Required.\n\tName string `yaml:\"name,omitempty\"`\n\t// The URL to a relevant project page, git repo, or contact person\n\tHome string `yaml:\"home,omitempty\"`\n\t// Source is the URL to the source code of this chart\n\tSources []string `yaml:\"sources,omitempty\"`\n\t// A SemVer 2 conformant version string of the chart. Required.\n\tVersion string `yaml:\"version,omitempty\"`\n\t// A one-sentence description of the chart\n\tDescription string `yaml:\"description,omitempty\"`\n\t// A list of string keywords\n\tKeywords []string `yaml:\"keywords,omitempty\"`\n\t// A list of name and URL/email address combinations for the maintainer(s)\n\tMaintainers []*Maintainer `yaml:\"maintainers,omitempty\"`\n\t// The URL to an icon file.\n\tIcon string `yaml:\"icon,omitempty\"`\n\t// The API Version of this chart. Required.\n\tAPIVersion string `yaml:\"apiVersion,omitempty\"`\n\t// The condition to check to enable chart\n\tCondition string `yaml:\"condition,omitempty\"`\n\t// The tags to check to enable chart\n\tTags string `yaml:\"tags,omitempty\"`\n\t// The version of the application enclosed inside of this chart.\n\tAppVersion string `yaml:\"appVersion,omitempty\"`\n\t// Whether or not this chart is deprecated\n\tDeprecated bool `yaml:\"deprecated,omitempty\"`\n\t// Annotations are additional mappings uninterpreted by Helm,\n\t// made available for inspection by other applications.\n\tAnnotations map[string]string `yaml:\"annotations,omitempty\"`\n\t// KubeVersion is a SemVer constraint specifying the version of Kubernetes required.\n\tKubeVersion string `yaml:\"kubeVersion,omitempty\"`\n\t// Dependencies are a list of dependencies for a chart.\n\tDependencies []*Dependency `yaml:\"dependencies,omitempty\"`\n\t// Specifies the chart type: application or library\n\tType string `yaml:\"type,omitempty\"`\n}\n\n// ReadChart parses the given yaml into a ChartFile struct\nfunc ReadChart(reader io.Reader) (ChartFile, error) {\n\tvar chart ChartFile\n\n\tchartContent, err := util.ReadFileAndFixNewline(reader)\n\tif err != nil {\n\t\treturn chart, err\n\t}\n\n\terr = yaml.Unmarshal(chartContent, &chart)\n\tif err != nil {\n\t\treturn chart, err\n\t}\n\treturn chart, nil\n}\n"
  },
  {
    "path": "pkg/chart/chart_test.go",
    "content": "package chart\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc TestReadChartFile(t *testing.T) {\n\tdata := []byte(`\n  name: test\n  description: test\n  dependencies:\n    - name: test\n  `)\n\tr := bytes.NewReader(data)\n\tc, err := ReadChart(r)\n\tif err != nil {\n\t\tt.Errorf(\"Error while reading test data: %v\", err)\n\t}\n\tif c.Name != \"test\" {\n\t\tt.Errorf(\"Expected Name was test, but got %v\", c.Name)\n\t}\n\tif c.Description != \"test\" {\n\t\tt.Errorf(\"Expected Description was test, but got %v\", c.Description)\n\t}\n\tif len(c.Dependencies) != 1 {\n\t\tt.Errorf(\"Expected to find one dependency, but got %d\", len(c.Dependencies))\n\t}\n\tif c.Dependencies[0].Name != \"test\" {\n\t\tt.Errorf(\"Expected Dependency name was test, but got %v\", c.Dependencies[0].Name)\n\t}\n}\n\nfunc TestImportValuesParsing(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected []interface{}\n\t\twantErr  bool\n\t}{\n\t\t{\n\t\t\tname: \"simple string import-values\",\n\t\t\tinput: `\nname: dep1\nimport-values:\n  - defaults`,\n\t\t\texpected: []interface{}{\"defaults\"},\n\t\t\twantErr:  false,\n\t\t},\n\t\t{\n\t\t\tname: \"complex map import-values\",\n\t\t\tinput: `\nname: dep1\nimport-values:\n  - child: exports.data\n    parent: mydata`,\n\t\t\texpected: []interface{}{\n\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\"child\":  \"exports.data\",\n\t\t\t\t\t\"parent\": \"mydata\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"mixed import-values\",\n\t\t\tinput: `\nname: dep1\nimport-values:\n  - defaults\n  - child: exports.config\n    parent: config`,\n\t\t\texpected: []interface{}{\n\t\t\t\t\"defaults\",\n\t\t\t\tmap[string]interface{}{\n\t\t\t\t\t\"child\":  \"exports.config\",\n\t\t\t\t\t\"parent\": \"config\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar dep Dependency\n\t\t\terr := yaml.Unmarshal([]byte(tt.input), &dep)\n\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"yaml.Unmarshal() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif !tt.wantErr {\n\t\t\t\tif len(dep.ImportValues) != len(tt.expected) {\n\t\t\t\t\tt.Errorf(\"ImportValues length = %v, want %v\", len(dep.ImportValues), len(tt.expected))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tfor i, val := range dep.ImportValues {\n\t\t\t\t\tswitch expected := tt.expected[i].(type) {\n\t\t\t\t\tcase string:\n\t\t\t\t\t\tif val != expected {\n\t\t\t\t\t\t\tt.Errorf(\"ImportValues[%d] = %v, want %v\", i, val, expected)\n\t\t\t\t\t\t}\n\t\t\t\t\tcase map[string]interface{}:\n\t\t\t\t\t\tvalMap, ok := val.(map[string]interface{})\n\t\t\t\t\t\tif !ok {\n\t\t\t\t\t\t\tt.Errorf(\"ImportValues[%d] is not a map, got %T\", i, val)\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfor k, v := range expected {\n\t\t\t\t\t\t\tif valMap[k] != v {\n\t\t\t\t\t\t\t\tt.Errorf(\"ImportValues[%d][%s] = %v, want %v\", i, k, valMap[k], v)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestChartFileParsing(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected ChartFile\n\t\twantErr  bool\n\t}{\n\t\t{\n\t\t\tname: \"basic chart file\",\n\t\t\tinput: `\nname: mychart\ndescription: A test chart\ndependencies:\n  - name: dep1\n    alias: aliased-dep\n    condition: subchart.enabled`,\n\t\t\texpected: ChartFile{\n\t\t\t\tName:        \"mychart\",\n\t\t\t\tDescription: \"A test chart\",\n\t\t\t\tDependencies: []*Dependency{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:      \"dep1\",\n\t\t\t\t\t\tAlias:     \"aliased-dep\",\n\t\t\t\t\t\tCondition: \"subchart.enabled\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"chart file without dependencies\",\n\t\t\tinput: `\nname: standalone\ndescription: A standalone chart`,\n\t\t\texpected: ChartFile{\n\t\t\t\tName:        \"standalone\",\n\t\t\t\tDescription: \"A standalone chart\",\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid yaml\",\n\t\t\tinput: `\nname: broken\ndescription: [broken`,\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar got ChartFile\n\t\t\terr := yaml.Unmarshal([]byte(tt.input), &got)\n\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"yaml.Unmarshal() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif !tt.wantErr {\n\t\t\t\tif got.Name != tt.expected.Name {\n\t\t\t\t\tt.Errorf(\"Name = %v, want %v\", got.Name, tt.expected.Name)\n\t\t\t\t}\n\t\t\t\tif got.Description != tt.expected.Description {\n\t\t\t\t\tt.Errorf(\"Description = %v, want %v\", got.Description, tt.expected.Description)\n\t\t\t\t}\n\t\t\t\tif len(got.Dependencies) != len(tt.expected.Dependencies) {\n\t\t\t\t\tt.Errorf(\"Dependencies length = %v, want %v\", len(got.Dependencies), len(tt.expected.Dependencies))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tfor i, dep := range got.Dependencies {\n\t\t\t\t\tif dep.Name != tt.expected.Dependencies[i].Name {\n\t\t\t\t\t\tt.Errorf(\"Dependency[%d].Name = %v, want %v\", i, dep.Name, tt.expected.Dependencies[i].Name)\n\t\t\t\t\t}\n\t\t\t\t\tif dep.Alias != tt.expected.Dependencies[i].Alias {\n\t\t\t\t\t\tt.Errorf(\"Dependency[%d].Alias = %v, want %v\", i, dep.Alias, tt.expected.Dependencies[i].Alias)\n\t\t\t\t\t}\n\t\t\t\t\tif dep.Condition != tt.expected.Dependencies[i].Condition {\n\t\t\t\t\t\tt.Errorf(\"Dependency[%d].Condition = %v, want %v\", i, dep.Condition, tt.expected.Dependencies[i].Condition)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/chart/searching/dependency_charts.go",
    "content": "package searching\n\nimport (\n\t\"archive/tar\"\n\t\"compress/gzip\"\n\t\"fmt\"\n\t\"github.com/dadav/helm-schema/pkg/chart\"\n\t\"gopkg.in/yaml.v3\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\nfunc extractTGZ(src, dest string) error {\n\tfile, err := os.Open(src)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer file.Close()\n\n\t// Open gzip reader\n\tgzr, err := gzip.NewReader(file)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer gzr.Close()\n\n\t// Open tar reader\n\ttr := tar.NewReader(gzr)\n\n\t// Extract files\n\tfor {\n\t\theader, err := tr.Next()\n\t\tif err == io.EOF {\n\t\t\tbreak\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Resolve and sanitize file path\n\t\tcleanName := filepath.Clean(header.Name)\n\t\t// Prevent absolute paths\n\t\tif filepath.IsAbs(cleanName) {\n\t\t\treturn fmt.Errorf(\"tar entry has absolute path: %s\", cleanName)\n\t\t}\n\t\t// Prevent path traversal outside dest\n\t\ttarget := filepath.Join(dest, cleanName)\n\t\trel, err := filepath.Rel(dest, target)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get relative path: %v\", err)\n\t\t}\n\t\tif strings.HasPrefix(rel, \"..\") || rel == \"..\" {\n\t\t\treturn fmt.Errorf(\"tar entry attempts to write outside destination: %s\", cleanName)\n\t\t}\n\n\t\tswitch header.Typeflag {\n\t\tcase tar.TypeDir:\n\t\t\t// Create directory if not exists\n\t\t\tif err := os.MkdirAll(target, 0755); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase tar.TypeReg:\n\t\t\t// Ensure directory exists\n\t\t\tif err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\t// Create file\n\t\t\toutFile, err := os.Create(target)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t// Copy file content\n\t\t\tif _, err := io.Copy(outFile, tr); err != nil {\n\t\t\t\toutFile.Close()\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := outFile.Close(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc SearchFiles(chartSearchRoot, startPath, fileName string, dependenciesFilter map[string]bool, queue chan<- string, errs chan<- error) {\n\tdefer close(queue)\n\terr := filepath.Walk(startPath, func(path string, info os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\terrs <- err\n\t\t\treturn nil\n\t\t}\n\n\t\tif !info.IsDir() && info.Name() == fileName {\n\t\t\tif filepath.Dir(path) == chartSearchRoot {\n\t\t\t\tqueue <- path\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif len(dependenciesFilter) > 0 {\n\t\t\t\tchartData, err := os.ReadFile(path)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrs <- fmt.Errorf(\"failed to read Chart.yaml at %s: %w\", path, err)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tvar chartFile chart.ChartFile\n\t\t\t\tif err := yaml.Unmarshal(chartData, &chartFile); err != nil {\n\t\t\t\t\terrs <- fmt.Errorf(\"failed to parse Chart.yaml at %s: %w\", path, err)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tif dependenciesFilter[chartFile.Name] {\n\t\t\t\t\tqueue <- path\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tqueue <- path\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\terrs <- err\n\t}\n}\n\nfunc SearchArchivesOpenTemp(startPath string, errs chan<- error) string {\n\ttempDir := \"\"\n\ttempDirCreationFailed := false\n\terr := filepath.Walk(startPath, func(path string, info os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\terrs <- err\n\t\t\treturn nil\n\t\t}\n\t\tif strings.HasSuffix(info.Name(), \".tgz\") || strings.HasSuffix(info.Name(), \".tar.gz\") {\n\t\t\t// Skip extraction if temp dir creation previously failed\n\t\t\tif tempDirCreationFailed {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\t// Extract archived charts from deps\n\t\t\tif tempDir == \"\" {\n\t\t\t\trelativeDir := filepath.Dir(path)\n\t\t\t\tvar mkdirErr error\n\t\t\t\ttempDir, mkdirErr = os.MkdirTemp(relativeDir, \"tmp-*\")\n\t\t\t\tif mkdirErr != nil {\n\t\t\t\t\terrs <- fmt.Errorf(\"failed to create temp directory for chart extraction: %w\", mkdirErr)\n\t\t\t\t\ttempDirCreationFailed = true\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\t\t\tif extractErr := extractTGZ(path, tempDir); extractErr != nil {\n\t\t\t\terrs <- fmt.Errorf(\"failed to extract %s: %w\", path, extractErr)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\terrs <- err\n\t}\n\treturn tempDir\n}\n"
  },
  {
    "path": "pkg/schema/annotate.go",
    "content": "package schema\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gopkg.in/yaml.v3\"\n)\n\n// HasSchemaAnnotation checks if a HeadComment already contains a # @schema block.\n// It matches exact \"# @schema\" lines but not \"# @schema.root\" lines.\nfunc HasSchemaAnnotation(comment string) bool {\n\tfor _, line := range strings.Split(comment, \"\\n\") {\n\t\ttrimmed := strings.TrimSpace(line)\n\t\tif trimmed == \"# @schema\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// typeAnnotationFromTag maps a YAML tag to the annotation type string.\n// Uses the same mapping as typeFromTag.\nfunc typeAnnotationFromTag(tag string) string {\n\tswitch tag {\n\tcase nullTag:\n\t\treturn `\"null\"`\n\tcase boolTag:\n\t\treturn \"boolean\"\n\tcase strTag:\n\t\treturn \"string\"\n\tcase intTag:\n\t\treturn \"integer\"\n\tcase floatTag:\n\t\treturn \"number\"\n\tcase timestampTag:\n\t\treturn \"string\"\n\tcase arrayTag:\n\t\treturn \"array\"\n\tcase mapTag:\n\t\treturn \"object\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// InsertionPoint represents where to insert an annotation block in the file.\ntype InsertionPoint struct {\n\tLine    int    // 1-based line number of the key node\n\tIndent  string // indentation string (spaces) derived from keyNode.Column\n\tTypeStr string // type annotation value\n}\n\n// collectInsertionPoints walks the yaml.Node tree and collects InsertionPoints\n// for keys that don't already have @schema annotations.\nfunc collectInsertionPoints(node *yaml.Node) []InsertionPoint {\n\tvar points []InsertionPoint\n\tcollectInsertionPointsRecursive(node, &points)\n\treturn points\n}\n\nfunc collectInsertionPointsRecursive(node *yaml.Node, points *[]InsertionPoint) {\n\tif node == nil {\n\t\treturn\n\t}\n\n\tswitch node.Kind {\n\tcase yaml.DocumentNode:\n\t\tfor _, child := range node.Content {\n\t\t\tcollectInsertionPointsRecursive(child, points)\n\t\t}\n\tcase yaml.MappingNode:\n\t\tfor i := 0; i < len(node.Content)-1; i += 2 {\n\t\t\tkeyNode := node.Content[i]\n\t\t\tvalueNode := node.Content[i+1]\n\n\t\t\tif !HasSchemaAnnotation(keyNode.HeadComment) {\n\t\t\t\ttypeStr := typeAnnotationFromTag(valueNode.Tag)\n\t\t\t\tif typeStr != \"\" {\n\t\t\t\t\tindent := strings.Repeat(\" \", keyNode.Column-1)\n\t\t\t\t\t*points = append(*points, InsertionPoint{\n\t\t\t\t\t\tLine:    keyNode.Line,\n\t\t\t\t\t\tIndent:  indent,\n\t\t\t\t\t\tTypeStr: typeStr,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Recurse into mapping values for nested keys\n\t\t\tif valueNode.Kind == yaml.MappingNode {\n\t\t\t\tcollectInsertionPointsRecursive(valueNode, points)\n\t\t\t}\n\n\t\t\t// Handle alias nodes that point to mappings\n\t\t\tif valueNode.Kind == yaml.AliasNode && valueNode.Alias != nil && valueNode.Alias.Kind == yaml.MappingNode {\n\t\t\t\tcollectInsertionPointsRecursive(valueNode.Alias, points)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// AnnotateContent parses YAML content, collects insertion points for keys\n// that lack @schema annotations, and inserts type annotation blocks.\n// Returns the modified content.\nfunc AnnotateContent(content []byte) ([]byte, error) {\n\tvar doc yaml.Node\n\tif err := yaml.Unmarshal(content, &doc); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse YAML: %w\", err)\n\t}\n\n\tpoints := collectInsertionPoints(&doc)\n\tif len(points) == 0 {\n\t\treturn content, nil\n\t}\n\n\tlines := strings.Split(string(content), \"\\n\")\n\n\t// Sort by line number descending so insertions don't shift earlier line numbers\n\tsort.Slice(points, func(i, j int) bool {\n\t\treturn points[i].Line > points[j].Line\n\t})\n\n\tfor _, pt := range points {\n\t\t// pt.Line is 1-based; convert to 0-based index\n\t\ttargetIdx := pt.Line - 1\n\t\tif targetIdx < 0 || targetIdx >= len(lines) {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Walk backwards past any existing HeadComment lines (lines starting with #)\n\t\t// to insert the annotation above existing comments\n\t\tinsertIdx := targetIdx\n\t\tfor insertIdx > 0 {\n\t\t\tcandidate := strings.TrimSpace(lines[insertIdx-1])\n\t\t\tif strings.HasPrefix(candidate, \"#\") {\n\t\t\t\tinsertIdx--\n\t\t\t} else {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\t// Build the 3 annotation lines\n\t\tannotationLines := []string{\n\t\t\tpt.Indent + \"# @schema\",\n\t\t\tpt.Indent + \"# type: \" + pt.TypeStr,\n\t\t\tpt.Indent + \"# @schema\",\n\t\t}\n\n\t\t// Insert at insertIdx\n\t\tnewLines := make([]string, 0, len(lines)+3)\n\t\tnewLines = append(newLines, lines[:insertIdx]...)\n\t\tnewLines = append(newLines, annotationLines...)\n\t\tnewLines = append(newLines, lines[insertIdx:]...)\n\t\tlines = newLines\n\t}\n\n\treturn []byte(strings.Join(lines, \"\\n\")), nil\n}\n\n// AnnotateValuesFile reads a values.yaml file, annotates unannotated keys\n// with @schema type blocks, and writes the result back (or prints to stdout if dryRun).\nfunc AnnotateValuesFile(valuesPath string, dryRun bool) error {\n\tfileInfo, err := os.Stat(valuesPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to stat %s: %w\", valuesPath, err)\n\t}\n\tperm := fileInfo.Mode().Perm()\n\n\tcontent, err := os.ReadFile(valuesPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read %s: %w\", valuesPath, err)\n\t}\n\n\tannotated, err := AnnotateContent(content)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to annotate %s: %w\", valuesPath, err)\n\t}\n\n\tif dryRun {\n\t\tlog.Infof(\"Annotated values for %s\", valuesPath)\n\t\tfmt.Print(string(annotated))\n\t\treturn nil\n\t}\n\n\tif err := os.WriteFile(valuesPath, annotated, perm); err != nil {\n\t\treturn fmt.Errorf(\"failed to write %s: %w\", valuesPath, err)\n\t}\n\n\tlog.Infof(\"Annotated %s\", valuesPath)\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/schema/annotate_test.go",
    "content": "package schema\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc TestHasSchemaAnnotation(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tcomment string\n\t\twant    bool\n\t}{\n\t\t{\n\t\t\tname:    \"empty comment\",\n\t\t\tcomment: \"\",\n\t\t\twant:    false,\n\t\t},\n\t\t{\n\t\t\tname:    \"no schema annotation\",\n\t\t\tcomment: \"# This is a normal comment\",\n\t\t\twant:    false,\n\t\t},\n\t\t{\n\t\t\tname:    \"has schema annotation\",\n\t\t\tcomment: \"# @schema\\n# type: string\\n# @schema\",\n\t\t\twant:    true,\n\t\t},\n\t\t{\n\t\t\tname:    \"has schema.root only\",\n\t\t\tcomment: \"# @schema.root\\n# title: foo\\n# @schema.root\",\n\t\t\twant:    false,\n\t\t},\n\t\t{\n\t\t\tname:    \"has both schema and schema.root\",\n\t\t\tcomment: \"# @schema.root\\n# title: foo\\n# @schema.root\\n# @schema\\n# type: string\\n# @schema\",\n\t\t\twant:    true,\n\t\t},\n\t\t{\n\t\t\tname:    \"schema prefix but not exact\",\n\t\t\tcomment: \"# @schema.something\",\n\t\t\twant:    false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := HasSchemaAnnotation(tt.comment)\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"HasSchemaAnnotation(%q) = %v, want %v\", tt.comment, got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTypeAnnotationFromTag(t *testing.T) {\n\ttests := []struct {\n\t\ttag  string\n\t\twant string\n\t}{\n\t\t{\"!!null\", `\"null\"`},\n\t\t{\"!!bool\", \"boolean\"},\n\t\t{\"!!str\", \"string\"},\n\t\t{\"!!int\", \"integer\"},\n\t\t{\"!!float\", \"number\"},\n\t\t{\"!!timestamp\", \"string\"},\n\t\t{\"!!seq\", \"array\"},\n\t\t{\"!!map\", \"object\"},\n\t\t{\"!!unknown\", \"\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.tag, func(t *testing.T) {\n\t\t\tgot := typeAnnotationFromTag(tt.tag)\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"typeAnnotationFromTag(%q) = %q, want %q\", tt.tag, got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCollectInsertionPoints(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tyaml       string\n\t\twantCount  int\n\t\twantTypes  []string\n\t}{\n\t\t{\n\t\t\tname:      \"simple flat yaml\",\n\t\t\tyaml:      \"name: hello\\nport: 80\\nenabled: true\\n\",\n\t\t\twantCount: 3,\n\t\t\twantTypes: []string{\"string\", \"integer\", \"boolean\"},\n\t\t},\n\t\t{\n\t\t\tname:      \"nested objects\",\n\t\t\tyaml:      \"service:\\n  type: ClusterIP\\n  port: 80\\n\",\n\t\t\twantCount: 3,\n\t\t\twantTypes: []string{\"object\", \"string\", \"integer\"},\n\t\t},\n\t\t{\n\t\t\tname:      \"already annotated key\",\n\t\t\tyaml:      \"# @schema\\n# type: string\\n# @schema\\nname: hello\\nport: 80\\n\",\n\t\t\twantCount: 1,\n\t\t\twantTypes: []string{\"integer\"},\n\t\t},\n\t\t{\n\t\t\tname:      \"null value\",\n\t\t\tyaml:      \"key:\\n\",\n\t\t\twantCount: 1,\n\t\t\twantTypes: []string{`\"null\"`},\n\t\t},\n\t\t{\n\t\t\tname:      \"array value\",\n\t\t\tyaml:      \"items: []\\n\",\n\t\t\twantCount: 1,\n\t\t\twantTypes: []string{\"array\"},\n\t\t},\n\t\t{\n\t\t\tname:      \"empty map value\",\n\t\t\tyaml:      \"config: {}\\n\",\n\t\t\twantCount: 1,\n\t\t\twantTypes: []string{\"object\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar doc yaml.Node\n\t\t\tif err := yaml.Unmarshal([]byte(tt.yaml), &doc); err != nil {\n\t\t\t\tt.Fatalf(\"failed to parse YAML: %v\", err)\n\t\t\t}\n\t\t\tpoints := collectInsertionPoints(&doc)\n\t\t\tif len(points) != tt.wantCount {\n\t\t\t\tt.Errorf(\"got %d insertion points, want %d\", len(points), tt.wantCount)\n\t\t\t\tfor _, p := range points {\n\t\t\t\t\tt.Logf(\"  line=%d type=%s\", p.Line, p.TypeStr)\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor i, wantType := range tt.wantTypes {\n\t\t\t\tif i >= len(points) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tif points[i].TypeStr != wantType {\n\t\t\t\t\tt.Errorf(\"point[%d].TypeStr = %q, want %q\", i, points[i].TypeStr, wantType)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAnnotateContent(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tinput   string\n\t\twant    string\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:  \"simple unannotated file\",\n\t\t\tinput: \"port: 80\\n\",\n\t\t\twant:  \"# @schema\\n# type: integer\\n# @schema\\nport: 80\\n\",\n\t\t},\n\t\t{\n\t\t\tname:  \"multiple keys\",\n\t\t\tinput: \"name: hello\\nport: 80\\n\",\n\t\t\twant:  \"# @schema\\n# type: string\\n# @schema\\nname: hello\\n# @schema\\n# type: integer\\n# @schema\\nport: 80\\n\",\n\t\t},\n\t\t{\n\t\t\tname:  \"already fully annotated\",\n\t\t\tinput: \"# @schema\\n# type: integer\\n# @schema\\nport: 80\\n\",\n\t\t\twant:  \"# @schema\\n# type: integer\\n# @schema\\nport: 80\\n\",\n\t\t},\n\t\t{\n\t\t\tname:  \"partially annotated\",\n\t\t\tinput: \"# @schema\\n# type: string\\n# @schema\\nname: hello\\nport: 80\\n\",\n\t\t\twant:  \"# @schema\\n# type: string\\n# @schema\\nname: hello\\n# @schema\\n# type: integer\\n# @schema\\nport: 80\\n\",\n\t\t},\n\t\t{\n\t\t\tname:  \"nested objects with indentation\",\n\t\t\tinput: \"service:\\n  type: ClusterIP\\n  port: 80\\n\",\n\t\t\twant:  \"# @schema\\n# type: object\\n# @schema\\nservice:\\n  # @schema\\n  # type: string\\n  # @schema\\n  type: ClusterIP\\n  # @schema\\n  # type: integer\\n  # @schema\\n  port: 80\\n\",\n\t\t},\n\t\t{\n\t\t\tname:  \"key with existing comment - annotation goes above comment\",\n\t\t\tinput: \"# This is the port\\nport: 80\\n\",\n\t\t\twant:  \"# @schema\\n# type: integer\\n# @schema\\n# This is the port\\nport: 80\\n\",\n\t\t},\n\t\t{\n\t\t\tname:  \"empty file\",\n\t\t\tinput: \"\",\n\t\t\twant:  \"\",\n\t\t},\n\t\t{\n\t\t\tname:  \"document separator preserved\",\n\t\t\tinput: \"---\\nport: 80\\n\",\n\t\t\twant:  \"---\\n# @schema\\n# type: integer\\n# @schema\\nport: 80\\n\",\n\t\t},\n\t\t{\n\t\t\tname:  \"boolean value\",\n\t\t\tinput: \"enabled: true\\n\",\n\t\t\twant:  \"# @schema\\n# type: boolean\\n# @schema\\nenabled: true\\n\",\n\t\t},\n\t\t{\n\t\t\tname:  \"null value\",\n\t\t\tinput: \"key:\\n\",\n\t\t\twant:  \"# @schema\\n# type: \\\"null\\\"\\n# @schema\\nkey:\\n\",\n\t\t},\n\t\t{\n\t\t\tname:  \"array value\",\n\t\t\tinput: \"items: []\\n\",\n\t\t\twant:  \"# @schema\\n# type: array\\n# @schema\\nitems: []\\n\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := AnnotateContent([]byte(tt.input))\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Fatalf(\"AnnotateContent() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif string(got) != tt.want {\n\t\t\t\tt.Errorf(\"AnnotateContent() mismatch\\ngot:\\n%s\\nwant:\\n%s\", string(got), tt.want)\n\t\t\t\t// Show diff line by line\n\t\t\t\tgotLines := strings.Split(string(got), \"\\n\")\n\t\t\t\twantLines := strings.Split(tt.want, \"\\n\")\n\t\t\t\tmaxLen := len(gotLines)\n\t\t\t\tif len(wantLines) > maxLen {\n\t\t\t\t\tmaxLen = len(wantLines)\n\t\t\t\t}\n\t\t\t\tfor i := 0; i < maxLen; i++ {\n\t\t\t\t\tvar g, w string\n\t\t\t\t\tif i < len(gotLines) {\n\t\t\t\t\t\tg = gotLines[i]\n\t\t\t\t\t}\n\t\t\t\t\tif i < len(wantLines) {\n\t\t\t\t\t\tw = wantLines[i]\n\t\t\t\t\t}\n\t\t\t\t\tif g != w {\n\t\t\t\t\t\tt.Errorf(\"  line %d: got=%q want=%q\", i, g, w)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/schema/err.go",
    "content": "package schema\n\ntype CircularError struct {\n\tmsg string\n}\n\nfunc (e *CircularError) Error() string { return e.msg }\n"
  },
  {
    "path": "pkg/schema/err_test.go",
    "content": "package schema\n\nimport \"testing\"\n\nfunc TestCircularError(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tmessage string\n\t\twant    string\n\t}{\n\t\t{\n\t\t\tname:    \"basic error message\",\n\t\t\tmessage: \"circular dependency detected\",\n\t\t\twant:    \"circular dependency detected\",\n\t\t},\n\t\t{\n\t\t\tname:    \"empty message\",\n\t\t\tmessage: \"\",\n\t\t\twant:    \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := &CircularError{msg: tt.message}\n\t\t\tif got := err.Error(); got != tt.want {\n\t\t\t\tt.Errorf(\"CircularError.Error() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/schema/root_schema_test.go",
    "content": "package schema\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc TestGetRootSchemaFromComment(t *testing.T) {\n\ttests := []struct {\n\t\tname                string\n\t\tcomment             string\n\t\texpectedHasData     bool\n\t\texpectedTitle       string\n\t\texpectedDescription string\n\t\texpectedRemaining   string\n\t\texpectError         bool\n\t}{\n\t\t{\n\t\t\tname: \"simple root schema\",\n\t\t\tcomment: `# @schema.root\n# title: My Chart\n# description: A chart for testing\n# @schema.root\n# This is a key comment\nkey: value`,\n\t\t\texpectedHasData:     true,\n\t\t\texpectedTitle:       \"My Chart\",\n\t\t\texpectedDescription: \"A chart for testing\",\n\t\t\texpectedRemaining: `# This is a key comment\nkey: value`,\n\t\t},\n\t\t{\n\t\t\tname: \"root schema with custom annotations\",\n\t\t\tcomment: `# @schema.root\n# title: Advanced Chart\n# x-custom-field: custom-value\n# additionalProperties: true\n# @schema.root\n# Key description`,\n\t\t\texpectedHasData:     true,\n\t\t\texpectedTitle:       \"Advanced Chart\",\n\t\t\texpectedDescription: \"\",\n\t\t\texpectedRemaining:   `# Key description`,\n\t\t},\n\t\t{\n\t\t\tname: \"no root schema\",\n\t\t\tcomment: `# @schema\n# type: string\n# @schema\n# Just a regular comment`,\n\t\t\texpectedHasData: false,\n\t\t\texpectedRemaining: `# @schema\n# type: string\n# @schema\n# Just a regular comment`,\n\t\t},\n\t\t{\n\t\t\tname: \"unclosed root schema block\",\n\t\t\tcomment: `# @schema.root\n# title: Unclosed\n# This will fail`,\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tschema, remaining, err := GetRootSchemaFromComment(tt.comment)\n\n\t\t\tif tt.expectError {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Errorf(\"Expected an error but got none\")\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Unexpected error: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif schema.HasData != tt.expectedHasData {\n\t\t\t\tt.Errorf(\"Expected HasData=%v, got %v\", tt.expectedHasData, schema.HasData)\n\t\t\t}\n\n\t\t\tif tt.expectedHasData {\n\t\t\t\tif schema.Title != tt.expectedTitle {\n\t\t\t\t\tt.Errorf(\"Expected Title=%q, got %q\", tt.expectedTitle, schema.Title)\n\t\t\t\t}\n\t\t\t\tif schema.Description != tt.expectedDescription {\n\t\t\t\t\tt.Errorf(\"Expected Description=%q, got %q\", tt.expectedDescription, schema.Description)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif strings.TrimSpace(remaining) != strings.TrimSpace(tt.expectedRemaining) {\n\t\t\t\tt.Errorf(\"Expected remaining=%q, got %q\", tt.expectedRemaining, remaining)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestYamlToSchemaWithRootAnnotations(t *testing.T) {\n\ttests := []struct {\n\t\tname                   string\n\t\tyamlContent            string\n\t\texpectedTitle          string\n\t\texpectedDescription    string\n\t\texpectedAdditionalProp interface{}\n\t\texpectedCustomField    interface{}\n\t}{\n\t\t{\n\t\t\tname: \"basic root schema\",\n\t\t\tyamlContent: `# @schema.root\n# title: Test Chart Values\n# description: Test description\n# @schema.root\nfoo: bar`,\n\t\t\texpectedTitle:       \"Test Chart Values\",\n\t\t\texpectedDescription: \"Test description\",\n\t\t},\n\t\t{\n\t\t\tname: \"root schema with additionalProperties\",\n\t\t\tyamlContent: `# @schema.root\n# title: Flexible Chart\n# additionalProperties: true\n# @schema.root\nservice:\n  port: 8080`,\n\t\t\texpectedTitle:          \"Flexible Chart\",\n\t\t\texpectedAdditionalProp: true,\n\t\t},\n\t\t{\n\t\t\tname: \"root schema with custom annotations\",\n\t\t\tyamlContent: `# @schema.root\n# title: Custom Chart\n# x-helm-version: \"3.0\"\n# @schema.root\napp: myapp`,\n\t\t\texpectedTitle:       \"Custom Chart\",\n\t\t\texpectedCustomField: \"3.0\",\n\t\t},\n\t\t{\n\t\t\tname: \"root schema with required array\",\n\t\t\tyamlContent: `# @schema.root\n# required: [keycloak, apps]\n# @schema.root\nkeycloak:\n  url: \"\"\napps:\n  decide:\n    baseUrl: \"\"`,\n\t\t},\n\t\t{\n\t\t\tname: \"root schema separated by blank lines\",\n\t\t\tyamlContent: `# @schema.root\n# additionalProperties: true\n# @schema.root\n\n# @schema\n# type: object\n# @schema\n_: {}`,\n\t\t\texpectedAdditionalProp: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar node yaml.Node\n\t\t\terr := yaml.Unmarshal([]byte(tt.yamlContent), &node)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Failed to unmarshal YAML: %v\", err)\n\t\t\t}\n\n\t\t\tskipConfig := &SkipAutoGenerationConfig{}\n\t\t\tschema, err := YamlToSchema(\"\", &node, false, false, false, true, skipConfig, nil)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"YamlToSchema failed: %v\", err)\n\t\t\t}\n\n\t\t\tif schema.Title != tt.expectedTitle {\n\t\t\t\tt.Errorf(\"Expected Title=%q, got %q\", tt.expectedTitle, schema.Title)\n\t\t\t}\n\n\t\t\tif tt.expectedDescription != \"\" && schema.Description != tt.expectedDescription {\n\t\t\t\tt.Errorf(\"Expected Description=%q, got %q\", tt.expectedDescription, schema.Description)\n\t\t\t}\n\n\t\t\tif tt.expectedAdditionalProp != nil {\n\t\t\t\tif schema.AdditionalProperties == nil {\n\t\t\t\t\tt.Errorf(\"Expected AdditionalProperties=%v, got nil\", tt.expectedAdditionalProp)\n\t\t\t\t} else if schema.AdditionalProperties != tt.expectedAdditionalProp {\n\t\t\t\t\tt.Errorf(\"Expected AdditionalProperties=%v, got %v\", tt.expectedAdditionalProp, schema.AdditionalProperties)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif tt.expectedCustomField != nil {\n\t\t\t\tif schema.CustomAnnotations == nil {\n\t\t\t\t\tt.Errorf(\"Expected CustomAnnotations to contain x-helm-version, but CustomAnnotations is nil\")\n\t\t\t\t} else if val, ok := schema.CustomAnnotations[\"x-helm-version\"]; !ok {\n\t\t\t\t\tt.Errorf(\"Expected CustomAnnotations to contain x-helm-version\")\n\t\t\t\t} else if val != tt.expectedCustomField {\n\t\t\t\t\tt.Errorf(\"Expected x-helm-version=%v, got %v\", tt.expectedCustomField, val)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif tt.name == \"root schema with required array\" {\n\t\t\t\texpectedRequired := []string{\"keycloak\", \"apps\"}\n\t\t\t\tif len(schema.Required.Strings) != len(expectedRequired) {\n\t\t\t\t\tt.Fatalf(\"Expected required=%v, got %v\", expectedRequired, schema.Required.Strings)\n\t\t\t\t}\n\t\t\t\tfor i, item := range expectedRequired {\n\t\t\t\t\tif schema.Required.Strings[i] != item {\n\t\t\t\t\t\tt.Fatalf(\"Expected required[%d]=%q, got %q\", i, item, schema.Required.Strings[i])\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRootSchemaDoesNotAffectKeyAnnotations(t *testing.T) {\n\tyamlContent := `# @schema.root\n# title: Root Title\n# description: Root description\n# @schema.root\n# @schema\n# enum: [dev, prod]\n# @schema\n# -- Environment setting\nenvironment: dev\n\nservice:\n  port: 8080`\n\n\tvar node yaml.Node\n\terr := yaml.Unmarshal([]byte(yamlContent), &node)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to unmarshal YAML: %v\", err)\n\t}\n\n\tskipConfig := &SkipAutoGenerationConfig{}\n\tschema, err := YamlToSchema(\"\", &node, false, false, false, true, skipConfig, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"YamlToSchema failed: %v\", err)\n\t}\n\n\t// Check root schema\n\tif schema.Title != \"Root Title\" {\n\t\tt.Errorf(\"Expected root Title=%q, got %q\", \"Root Title\", schema.Title)\n\t}\n\tif schema.Description != \"Root description\" {\n\t\tt.Errorf(\"Expected root Description=%q, got %q\", \"Root description\", schema.Description)\n\t}\n\n\t// Check that environment key still has its own annotations\n\tif schema.Properties == nil {\n\t\tt.Fatal(\"Expected Properties to be set\")\n\t}\n\n\tenvSchema, ok := schema.Properties[\"environment\"]\n\tif !ok {\n\t\tt.Fatal(\"Expected environment property to exist\")\n\t}\n\n\tif len(envSchema.Enum) != 2 {\n\t\tt.Errorf(\"Expected 2 enum values, got %d\", len(envSchema.Enum))\n\t}\n\tif envSchema.Description != \"Environment setting\" {\n\t\tt.Errorf(\"Expected environment Description=%q, got %q\", \"Environment setting\", envSchema.Description)\n\t}\n}\n"
  },
  {
    "path": "pkg/schema/schema.go",
    "content": "package schema\n\nimport (\n\t\"bufio\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/dadav/go-jsonpointer\"\n\t\"github.com/dadav/helm-schema/pkg/util\"\n\t\"github.com/norwoodj/helm-docs/pkg/helm\"\n\t\"github.com/santhosh-tekuri/jsonschema/v6\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gopkg.in/yaml.v3\"\n)\n\n// SchemaPrefix and CommentPrefix define the markers used for schema annotations in comments\nconst (\n\tSchemaPrefix     = \"# @schema\"\n\tSchemaRootPrefix = \"# @schema.root\"\n\tCommentPrefix    = \"#\"\n\n\t// CustomAnnotationPrefix marks custom annotations.\n\t// Custom annotations are extensions to the JSON Schema specification\n\t// See: https://json-schema.org/blog/posts/custom-annotations-will-continue\n\tCustomAnnotationPrefix = \"x-\"\n)\n\n// YAML tag constants used for type inference\nconst (\n\tnullTag      = \"!!null\"\n\tboolTag      = \"!!bool\"\n\tstrTag       = \"!!str\"\n\tintTag       = \"!!int\"\n\tfloatTag     = \"!!float\"\n\ttimestampTag = \"!!timestamp\"\n\tarrayTag     = \"!!seq\"\n\tmapTag       = \"!!map\"\n)\n\n// Precompiled regex patterns for better performance\nvar (\n\tleadingCommentsRemover = regexp.MustCompile(`(?s)(?m)(?:.*\\n{2,})+`)\n\thelmDocsTagsRemover    = regexp.MustCompile(`(?ms)(\\r\\n|\\r|\\n)?\\s*@\\w+(\\s+--\\s)?[^\\n\\r]*`)\n\thelmDocsPrefixRemover  = regexp.MustCompile(`(?m)^--\\s?`)\n)\n\n// SchemaOrBool represents a JSON Schema field that can be either a boolean or a Schema object\ntype SchemaOrBool interface{}\n\n// BoolOrArrayOfString represents a JSON Schema field that can be either a boolean or an array of strings\n// Used primarily for the \"required\" field which can be either true/false or an array of required property names\ntype BoolOrArrayOfString struct {\n\tStrings []string\n\tBool    bool\n}\n\nfunc NewBoolOrArrayOfString(arr []string, b bool) BoolOrArrayOfString {\n\treturn BoolOrArrayOfString{\n\t\tStrings: arr,\n\t\tBool:    b,\n\t}\n}\n\nfunc (s *BoolOrArrayOfString) UnmarshalJSON(value []byte) error {\n\tvar multi []string\n\tvar single bool\n\n\tif err := json.Unmarshal(value, &multi); err == nil {\n\t\ts.Strings = multi\n\t} else if err := json.Unmarshal(value, &single); err == nil {\n\t\ts.Bool = single\n\t}\n\treturn nil\n}\n\nfunc (s *BoolOrArrayOfString) MarshalJSON() ([]byte, error) {\n\tif len(s.Strings) > 0 {\n\t\treturn json.Marshal(s.Strings)\n\t}\n\t// Return empty array - the Bool field is only used internally\n\t// to signal that the property should be added to parent's required list\n\treturn json.Marshal([]string{})\n}\n\nfunc (s *BoolOrArrayOfString) UnmarshalYAML(value *yaml.Node) error {\n\tvar multi []string\n\tif value.ShortTag() == arrayTag {\n\t\tfor _, v := range value.Content {\n\t\t\tvar typeStr string\n\t\t\terr := v.Decode(&typeStr)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tmulti = append(multi, typeStr)\n\t\t}\n\t\ts.Strings = multi\n\t} else if value.ShortTag() == boolTag {\n\t\tvar single bool\n\t\terr := value.Decode(&single)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ts.Bool = single\n\t} else {\n\t\treturn fmt.Errorf(\"could not unmarshal %v to slice of string or bool\", value.Content)\n\t}\n\treturn nil\n}\n\ntype StringOrArrayOfString []string\n\nfunc (s *StringOrArrayOfString) UnmarshalYAML(value *yaml.Node) error {\n\tvar multi []string\n\tif value.ShortTag() == arrayTag {\n\t\tfor _, v := range value.Content {\n\t\t\tif v.ShortTag() == nullTag {\n\t\t\t\tmulti = append(multi, \"null\")\n\t\t\t} else {\n\t\t\t\tvar typeStr string\n\t\t\t\terr := v.Decode(&typeStr)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tmulti = append(multi, typeStr)\n\t\t\t}\n\t\t}\n\t\t*s = multi\n\t} else {\n\t\tvar single string\n\t\terr := value.Decode(&single)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*s = []string{single}\n\t}\n\treturn nil\n}\n\nfunc (s *StringOrArrayOfString) UnmarshalJSON(value []byte) error {\n\tvar multi []string\n\tvar single string\n\n\tif err := json.Unmarshal(value, &multi); err == nil {\n\t\t*s = multi\n\t} else if err := json.Unmarshal(value, &single); err == nil {\n\t\t*s = []string{single}\n\t}\n\treturn nil\n}\n\nfunc (s *StringOrArrayOfString) MarshalJSON() ([]byte, error) {\n\tif len(*s) == 1 {\n\t\treturn json.Marshal([]string(*s)[0])\n\t}\n\treturn json.Marshal([]string(*s))\n}\n\nfunc (s *StringOrArrayOfString) Validate() error {\n\t// Check if type is valid\n\tfor _, t := range []string(*s) {\n\t\tif t != \"\" &&\n\t\t\tt != \"object\" &&\n\t\t\tt != \"string\" &&\n\t\t\tt != \"integer\" &&\n\t\t\tt != \"number\" &&\n\t\t\tt != \"array\" &&\n\t\t\tt != \"null\" &&\n\t\t\tt != \"boolean\" {\n\t\t\treturn fmt.Errorf(\"unsupported type %s\", t)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *StringOrArrayOfString) IsEmpty() bool {\n\tfor _, t := range []string(*s) {\n\t\tif t == \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn len(*s) == 0\n}\n\nfunc (s *StringOrArrayOfString) canDropRequired() bool {\n\tss := *s\n\treturn len(ss) == 1 && (ss[0] == \"string\" ||\n\t\tss[0] == \"number\" || ss[0] == \"boolean\" ||\n\t\tss[0] == \"integer\" || ss[0] == \"null\" ||\n\t\tss[0] == \"array\")\n}\n\nfunc (s *StringOrArrayOfString) Matches(typeString string) bool {\n\tfor _, t := range []string(*s) {\n\t\tif t == typeString {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// MarshalJSON custom marshal method for Schema. It inlines the CustomAnnotations fields\nfunc (s *Schema) MarshalJSON() ([]byte, error) {\n\t// Create a map to hold all the fields\n\ttype Alias Schema\n\tdata := make(map[string]interface{})\n\n\t// Marshal the Schema struct (excluding CustomAnnotations)\n\talias := (*Alias)(s)\n\taliasJSON, err := json.Marshal(alias)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Unmarshal the JSON back into the map\n\tif err := json.Unmarshal(aliasJSON, &data); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// inline the CustomAnnotations fields\n\tfor key, value := range s.CustomAnnotations {\n\t\tdata[key] = value\n\t}\n\n\tdelete(data, \"CustomAnnotations\")\n\n\t// Remove \"required\" if the schema type is not object\n\tif s.Type.canDropRequired() {\n\t\tdelete(data, \"required\")\n\t}\n\n\t// Explicitly include const field when it was set to null\n\t// This handles the case where const: null in YAML should appear as \"const\": null in JSON\n\tif s.constWasSet && s.Const == nil {\n\t\tdata[\"const\"] = nil\n\t}\n\n\t// Marshal the final map into JSON\n\treturn json.Marshal(data)\n}\n\n// Schema struct contains yaml tags for reading, json for writing (creating the jsonschema)\ntype Schema struct {\n\tAdditionalProperties SchemaOrBool           `yaml:\"additionalProperties,omitempty\" json:\"additionalProperties,omitempty\"`\n\tDefault              interface{}            `yaml:\"default,omitempty\"              json:\"default,omitempty\"`\n\tThen                 *Schema                `yaml:\"then,omitempty\"                 json:\"then,omitempty\"`\n\tPatternProperties    map[string]*Schema     `yaml:\"patternProperties,omitempty\"    json:\"patternProperties,omitempty\"`\n\tProperties           map[string]*Schema     `yaml:\"properties,omitempty\"           json:\"properties,omitempty\"`\n\tIf                   *Schema                `yaml:\"if,omitempty\"                   json:\"if,omitempty\"`\n\tMinimum              *float64               `yaml:\"minimum,omitempty\"              json:\"minimum,omitempty\"`\n\tMultipleOf           *float64               `yaml:\"multipleOf,omitempty\"           json:\"multipleOf,omitempty\"`\n\tExclusiveMaximum     *float64               `yaml:\"exclusiveMaximum,omitempty\"     json:\"exclusiveMaximum,omitempty\"`\n\tItems                *Schema                `yaml:\"items,omitempty\"                json:\"items,omitempty\"`\n\tExclusiveMinimum     *float64               `yaml:\"exclusiveMinimum,omitempty\"     json:\"exclusiveMinimum,omitempty\"`\n\tMaximum              *float64               `yaml:\"maximum,omitempty\"              json:\"maximum,omitempty\"`\n\tElse                 *Schema                `yaml:\"else,omitempty\"                 json:\"else,omitempty\"`\n\tPattern              string                 `yaml:\"pattern,omitempty\"              json:\"pattern,omitempty\"`\n\tConst                interface{}            `yaml:\"const,omitempty\"                json:\"const,omitempty\"`\n\tConstFromValue       bool                   `yaml:\"const-from-value,omitempty\"     json:\"-\"`\n\tRef                  string                 `yaml:\"$ref,omitempty\"                 json:\"$ref,omitempty\"`\n\tSchema               string                 `yaml:\"$schema,omitempty\"              json:\"$schema,omitempty\"`\n\tId                   string                 `yaml:\"$id,omitempty\"                  json:\"$id,omitempty\"`\n\tComment              string                 `yaml:\"$comment,omitempty\"             json:\"$comment,omitempty\"`\n\tFormat               string                 `yaml:\"format,omitempty\"               json:\"format,omitempty\"`\n\tDescription          string                 `yaml:\"description,omitempty\"          json:\"description,omitempty\"`\n\tTitle                string                 `yaml:\"title,omitempty\"                json:\"title,omitempty\"`\n\tContentEncoding      string                 `yaml:\"contentEncoding,omitempty\"      json:\"contentEncoding,omitempty\"`\n\tContentMediaType     string                 `yaml:\"contentMediaType,omitempty\"     json:\"contentMediaType,omitempty\"`\n\tType                 StringOrArrayOfString  `yaml:\"type,omitempty\"                 json:\"type,omitempty\"`\n\tAnyOf                []*Schema              `yaml:\"anyOf,omitempty\"                json:\"anyOf,omitempty\"`\n\tAllOf                []*Schema              `yaml:\"allOf,omitempty\"                json:\"allOf,omitempty\"`\n\tOneOf                []*Schema              `yaml:\"oneOf,omitempty\"                json:\"oneOf,omitempty\"`\n\tNot                  *Schema                `yaml:\"not,omitempty\"                  json:\"not,omitempty\"`\n\tExamples             []interface{}          `yaml:\"examples,omitempty\"             json:\"examples,omitempty\"`\n\tEnum                 []interface{}          `yaml:\"enum,omitempty\"                 json:\"enum,omitempty\"`\n\tDefinitions          map[string]*Schema     `yaml:\"definitions,omitempty\"          json:\"definitions,omitempty\"`\n\tHasData              bool                   `yaml:\"-\"                              json:\"-\"`\n\tDeprecated           bool                   `yaml:\"deprecated,omitempty\"           json:\"deprecated,omitempty\"`\n\tReadOnly             bool                   `yaml:\"readOnly,omitempty\"             json:\"readOnly,omitempty\"`\n\tWriteOnly            bool                   `yaml:\"writeOnly,omitempty\"            json:\"writeOnly,omitempty\"`\n\tRequired             BoolOrArrayOfString    `yaml:\"required,omitempty\"             json:\"required,omitempty\"`\n\tCustomAnnotations    map[string]interface{} `yaml:\"-\"                              json:\",omitempty\"`\n\tMinLength            *int                   `yaml:\"minLength,omitempty\"            json:\"minLength,omitempty\"`\n\tMaxLength            *int                   `yaml:\"maxLength,omitempty\"            json:\"maxLength,omitempty\"`\n\tMinItems             *int                   `yaml:\"minItems,omitempty\"             json:\"minItems,omitempty\"`\n\tMaxItems             *int                   `yaml:\"maxItems,omitempty\"             json:\"maxItems,omitempty\"`\n\tUniqueItems          bool                   `yaml:\"uniqueItems,omitempty\"          json:\"uniqueItems,omitempty\"`\n\tContains             *Schema                `yaml:\"contains,omitempty\"             json:\"contains,omitempty\"`\n\tAdditionalItems      SchemaOrBool           `yaml:\"additionalItems,omitempty\"      json:\"additionalItems,omitempty\"`\n\tMinProperties        *int                   `yaml:\"minProperties,omitempty\"        json:\"minProperties,omitempty\"`\n\tMaxProperties        *int                   `yaml:\"maxProperties,omitempty\"        json:\"maxProperties,omitempty\"`\n\tPropertyNames        *Schema                `yaml:\"propertyNames,omitempty\"        json:\"propertyNames,omitempty\"`\n\tDependencies         map[string]interface{} `yaml:\"dependencies,omitempty\"         json:\"dependencies,omitempty\"`\n\tconstWasSet          bool                   `yaml:\"-\"                              json:\"-\"`\n}\n\nfunc NewSchema(schemaType string) *Schema {\n\tif schemaType == \"\" {\n\t\treturn &Schema{}\n\t}\n\n\treturn &Schema{\n\t\tType:     []string{schemaType},\n\t\tRequired: NewBoolOrArrayOfString([]string{}, false),\n\t}\n}\n\n// getJsonKeys returns a slice of all JSON tag values from the Schema struct fields.\n// This is used to identify known fields during YAML unmarshaling to separate them\n// from custom annotations.\nfunc (s Schema) getJsonKeys() []string {\n\tresult := []string{}\n\tt := reflect.TypeOf(s)\n\n\tfor i := 0; i < t.NumField(); i++ {\n\t\tfield := t.Field(i)\n\t\tresult = append(result, field.Tag.Get(\"json\"))\n\t}\n\treturn result\n}\n\n// UnmarshalYAML implements custom YAML unmarshaling for Schema objects.\n// It handles both standard schema fields and custom annotations (prefixed with \"x-\").\n// Custom annotations are stored in the CustomAnnotations map while standard fields\n// are unmarshaled directly into the Schema struct.\nfunc (s *Schema) UnmarshalYAML(node *yaml.Node) error {\n\t// Create an alias type to avoid recursion\n\ttype schemaAlias Schema\n\talias := new(schemaAlias)\n\t// copy all existing fields\n\t*alias = schemaAlias(*s)\n\n\t// Unmarshal known fields into alias\n\tif err := node.Decode(alias); err != nil {\n\t\treturn err\n\t}\n\n\t// Initialize CustomAnnotations map\n\talias.CustomAnnotations = make(map[string]interface{})\n\n\tknownKeys := s.getJsonKeys()\n\n\t// Iterate through all node fields\n\tfor i := 0; i < len(node.Content)-1; i += 2 {\n\t\tkeyNode := node.Content[i]\n\t\tvalueNode := node.Content[i+1]\n\t\tkey := keyNode.Value\n\n\t\t// Track if const field was explicitly set (even to null)\n\t\tif key == \"const\" {\n\t\t\talias.constWasSet = true\n\t\t}\n\n\t\tif slices.Contains(knownKeys, key) {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Unmarshal unknown fields into the CustomAnnotations map\n\t\tif !strings.HasPrefix(key, CustomAnnotationPrefix) {\n\t\t\tcontinue\n\t\t}\n\t\tvar value interface{}\n\t\tif err := valueNode.Decode(&value); err != nil {\n\t\t\treturn err\n\t\t}\n\t\talias.CustomAnnotations[key] = value\n\t}\n\n\t// Copy alias to the main struct\n\t*s = Schema(*alias)\n\treturn nil\n}\n\n// UnmarshalJSON implements custom JSON unmarshaling for Schema objects.\n// It handles \"definitions\" (Draft 7), \"$defs\" (Draft 2019-09+), custom\n// annotations (prefixed with \"x-\"), and tracks explicit \"const\" fields.\nfunc (s *Schema) UnmarshalJSON(data []byte) error {\n\t// Create an alias type to avoid recursion\n\ttype schemaAlias Schema\n\talias := new(schemaAlias)\n\n\t// Unmarshal known fields into alias\n\tif err := json.Unmarshal(data, alias); err != nil {\n\t\treturn err\n\t}\n\n\t// Parse raw JSON to check for $defs, custom annotations, and const\n\tvar raw map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &raw); err != nil {\n\t\treturn err\n\t}\n\n\t// Handle $defs (Draft 2019-09+) - merge into Definitions\n\tif defsRaw, ok := raw[\"$defs\"]; ok {\n\t\tvar defs map[string]*Schema\n\t\tif err := json.Unmarshal(defsRaw, &defs); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to unmarshal $defs: %w\", err)\n\t\t}\n\t\t// Merge $defs into Definitions\n\t\tif alias.Definitions == nil {\n\t\t\talias.Definitions = make(map[string]*Schema)\n\t\t}\n\t\tfor k, v := range defs {\n\t\t\tif _, exists := alias.Definitions[k]; !exists {\n\t\t\t\talias.Definitions[k] = v\n\t\t\t}\n\t\t}\n\t}\n\n\t// Track if const field was explicitly set (even to null)\n\tif _, ok := raw[\"const\"]; ok {\n\t\talias.constWasSet = true\n\t}\n\n\t// Extract custom annotations (x-* prefixed keys)\n\tknownKeys := s.getJsonKeys()\n\talias.CustomAnnotations = make(map[string]interface{})\n\tfor key, rawValue := range raw {\n\t\tif !strings.HasPrefix(key, CustomAnnotationPrefix) {\n\t\t\tcontinue\n\t\t}\n\t\tif slices.Contains(knownKeys, key) {\n\t\t\tcontinue\n\t\t}\n\t\tvar value interface{}\n\t\tif err := json.Unmarshal(rawValue, &value); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to unmarshal custom annotation %s: %w\", key, err)\n\t\t}\n\t\talias.CustomAnnotations[key] = value\n\t}\n\n\t// Copy alias to the main struct\n\t*s = Schema(*alias)\n\n\t// Rewrite $ref paths from $defs to definitions for Draft 7 compatibility\n\ts.rewriteDefsRefs()\n\n\treturn nil\n}\n\n// HoistDefinitions collects all definitions from nested schemas and hoists them\n// to the root level. This is necessary because $ref paths like \"#/definitions/X\"\n// always reference the document root, not the local schema.\nfunc (s *Schema) HoistDefinitions() {\n\tif s == nil {\n\t\treturn\n\t}\n\n\t// Initialize root definitions if needed\n\tif s.Definitions == nil {\n\t\ts.Definitions = make(map[string]*Schema)\n\t}\n\n\t// Collect definitions from all nested schemas\n\ts.collectAndHoistDefinitions(s.Definitions)\n}\n\n// collectAndHoistDefinitions recursively collects definitions from nested schemas\n// and adds them to the root definitions map, then removes them from nested positions.\nfunc (s *Schema) collectAndHoistDefinitions(rootDefs map[string]*Schema) {\n\tif s == nil {\n\t\treturn\n\t}\n\n\t// Process nested properties\n\tfor _, prop := range s.Properties {\n\t\tprop.collectAndHoistDefinitions(rootDefs)\n\t\t// Hoist definitions from this property\n\t\tif prop.Definitions != nil {\n\t\t\tfor name, def := range prop.Definitions {\n\t\t\t\tif _, exists := rootDefs[name]; !exists {\n\t\t\t\t\trootDefs[name] = def\n\t\t\t\t}\n\t\t\t}\n\t\t\tprop.Definitions = nil // Remove from nested position\n\t\t}\n\t}\n\n\t// Process pattern properties\n\tfor _, prop := range s.PatternProperties {\n\t\tprop.collectAndHoistDefinitions(rootDefs)\n\t\tif prop.Definitions != nil {\n\t\t\tfor name, def := range prop.Definitions {\n\t\t\t\tif _, exists := rootDefs[name]; !exists {\n\t\t\t\t\trootDefs[name] = def\n\t\t\t\t}\n\t\t\t}\n\t\t\tprop.Definitions = nil\n\t\t}\n\t}\n\n\t// Process items\n\tif s.Items != nil {\n\t\ts.Items.collectAndHoistDefinitions(rootDefs)\n\t\tif s.Items.Definitions != nil {\n\t\t\tfor name, def := range s.Items.Definitions {\n\t\t\t\tif _, exists := rootDefs[name]; !exists {\n\t\t\t\t\trootDefs[name] = def\n\t\t\t\t}\n\t\t\t}\n\t\t\ts.Items.Definitions = nil\n\t\t}\n\t}\n\n\t// Process composition schemas\n\tfor _, schema := range s.AllOf {\n\t\tschema.collectAndHoistDefinitions(rootDefs)\n\t\tif schema.Definitions != nil {\n\t\t\tfor name, def := range schema.Definitions {\n\t\t\t\tif _, exists := rootDefs[name]; !exists {\n\t\t\t\t\trootDefs[name] = def\n\t\t\t\t}\n\t\t\t}\n\t\t\tschema.Definitions = nil\n\t\t}\n\t}\n\tfor _, schema := range s.AnyOf {\n\t\tschema.collectAndHoistDefinitions(rootDefs)\n\t\tif schema.Definitions != nil {\n\t\t\tfor name, def := range schema.Definitions {\n\t\t\t\tif _, exists := rootDefs[name]; !exists {\n\t\t\t\t\trootDefs[name] = def\n\t\t\t\t}\n\t\t\t}\n\t\t\tschema.Definitions = nil\n\t\t}\n\t}\n\tfor _, schema := range s.OneOf {\n\t\tschema.collectAndHoistDefinitions(rootDefs)\n\t\tif schema.Definitions != nil {\n\t\t\tfor name, def := range schema.Definitions {\n\t\t\t\tif _, exists := rootDefs[name]; !exists {\n\t\t\t\t\trootDefs[name] = def\n\t\t\t\t}\n\t\t\t}\n\t\t\tschema.Definitions = nil\n\t\t}\n\t}\n\n\t// Process conditional schemas\n\tif s.If != nil {\n\t\ts.If.collectAndHoistDefinitions(rootDefs)\n\t\tif s.If.Definitions != nil {\n\t\t\tfor name, def := range s.If.Definitions {\n\t\t\t\tif _, exists := rootDefs[name]; !exists {\n\t\t\t\t\trootDefs[name] = def\n\t\t\t\t}\n\t\t\t}\n\t\t\ts.If.Definitions = nil\n\t\t}\n\t}\n\tif s.Then != nil {\n\t\ts.Then.collectAndHoistDefinitions(rootDefs)\n\t\tif s.Then.Definitions != nil {\n\t\t\tfor name, def := range s.Then.Definitions {\n\t\t\t\tif _, exists := rootDefs[name]; !exists {\n\t\t\t\t\trootDefs[name] = def\n\t\t\t\t}\n\t\t\t}\n\t\t\ts.Then.Definitions = nil\n\t\t}\n\t}\n\tif s.Else != nil {\n\t\ts.Else.collectAndHoistDefinitions(rootDefs)\n\t\tif s.Else.Definitions != nil {\n\t\t\tfor name, def := range s.Else.Definitions {\n\t\t\t\tif _, exists := rootDefs[name]; !exists {\n\t\t\t\t\trootDefs[name] = def\n\t\t\t\t}\n\t\t\t}\n\t\t\ts.Else.Definitions = nil\n\t\t}\n\t}\n\tif s.Not != nil {\n\t\ts.Not.collectAndHoistDefinitions(rootDefs)\n\t\tif s.Not.Definitions != nil {\n\t\t\tfor name, def := range s.Not.Definitions {\n\t\t\t\tif _, exists := rootDefs[name]; !exists {\n\t\t\t\t\trootDefs[name] = def\n\t\t\t\t}\n\t\t\t}\n\t\t\ts.Not.Definitions = nil\n\t\t}\n\t}\n\n\t// Process AdditionalProperties when it's a schema\n\tif s.AdditionalProperties != nil {\n\t\tswitch v := s.AdditionalProperties.(type) {\n\t\tcase *Schema:\n\t\t\tv.collectAndHoistDefinitions(rootDefs)\n\t\t\tif v.Definitions != nil {\n\t\t\t\tfor name, def := range v.Definitions {\n\t\t\t\t\tif _, exists := rootDefs[name]; !exists {\n\t\t\t\t\t\trootDefs[name] = def\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tv.Definitions = nil\n\t\t\t}\n\t\t}\n\t}\n}\n\n// rewriteDefsRefs recursively rewrites $ref paths from \"#/$defs/\" to \"#/definitions/\"\n// for JSON Schema Draft 7 compatibility.\nfunc (s *Schema) rewriteDefsRefs() {\n\tif s == nil {\n\t\treturn\n\t}\n\n\t// Rewrite main $ref\n\tif strings.HasPrefix(s.Ref, \"#/$defs/\") {\n\t\ts.Ref = strings.Replace(s.Ref, \"#/$defs/\", \"#/definitions/\", 1)\n\t}\n\n\t// Recursively process all nested schemas\n\tfor _, prop := range s.Properties {\n\t\tprop.rewriteDefsRefs()\n\t}\n\tfor _, prop := range s.PatternProperties {\n\t\tprop.rewriteDefsRefs()\n\t}\n\tfor _, def := range s.Definitions {\n\t\tdef.rewriteDefsRefs()\n\t}\n\tif s.Items != nil {\n\t\ts.Items.rewriteDefsRefs()\n\t}\n\tif s.Contains != nil {\n\t\ts.Contains.rewriteDefsRefs()\n\t}\n\tif s.PropertyNames != nil {\n\t\ts.PropertyNames.rewriteDefsRefs()\n\t}\n\tif s.If != nil {\n\t\ts.If.rewriteDefsRefs()\n\t}\n\tif s.Then != nil {\n\t\ts.Then.rewriteDefsRefs()\n\t}\n\tif s.Else != nil {\n\t\ts.Else.rewriteDefsRefs()\n\t}\n\tif s.Not != nil {\n\t\ts.Not.rewriteDefsRefs()\n\t}\n\tfor _, schema := range s.AllOf {\n\t\tschema.rewriteDefsRefs()\n\t}\n\tfor _, schema := range s.AnyOf {\n\t\tschema.rewriteDefsRefs()\n\t}\n\tfor _, schema := range s.OneOf {\n\t\tschema.rewriteDefsRefs()\n\t}\n\n\t// Handle AdditionalProperties and AdditionalItems when they are schemas\n\tif s.AdditionalProperties != nil {\n\t\tswitch v := s.AdditionalProperties.(type) {\n\t\tcase *Schema:\n\t\t\tv.rewriteDefsRefs()\n\t\tcase Schema:\n\t\t\tv.rewriteDefsRefs()\n\t\t\ts.AdditionalProperties = v\n\t\t}\n\t}\n\tif s.AdditionalItems != nil {\n\t\tswitch v := s.AdditionalItems.(type) {\n\t\tcase *Schema:\n\t\t\tv.rewriteDefsRefs()\n\t\tcase Schema:\n\t\t\tv.rewriteDefsRefs()\n\t\t\ts.AdditionalItems = v\n\t\t}\n\t}\n}\n\n// Set sets the HasData field to true\nfunc (s *Schema) Set() {\n\ts.HasData = true\n}\n\n// DisableRequiredProperties recursively disables all required property validations throughout the schema.\n// This includes:\n// - Setting the root schema's required field to an empty array\n// - Recursively disabling required properties in all nested schemas (properties, items, etc.)\n// - Handling all conditional schemas (if/then/else)\n// - Processing all composition schemas (anyOf/oneOf/allOf)\nfunc (s *Schema) DisableRequiredProperties() {\n\ts.Required = NewBoolOrArrayOfString([]string{}, false)\n\tfor _, v := range s.Properties {\n\t\tv.DisableRequiredProperties()\n\t}\n\tif s.Items != nil {\n\t\ts.Items.DisableRequiredProperties()\n\t}\n\n\tif s.AnyOf != nil {\n\t\tfor _, v := range s.AnyOf {\n\t\t\tv.DisableRequiredProperties()\n\t\t}\n\t}\n\tif s.OneOf != nil {\n\t\tfor _, v := range s.OneOf {\n\t\t\tv.DisableRequiredProperties()\n\t\t}\n\t}\n\tif s.AllOf != nil {\n\t\tfor _, v := range s.AllOf {\n\t\t\tv.DisableRequiredProperties()\n\t\t}\n\t}\n\tif s.If != nil {\n\t\ts.If.DisableRequiredProperties()\n\t}\n\tif s.Else != nil {\n\t\ts.Else.DisableRequiredProperties()\n\t}\n\tif s.Then != nil {\n\t\ts.Then.DisableRequiredProperties()\n\t}\n\tif s.Not != nil {\n\t\ts.Not.DisableRequiredProperties()\n\t}\n\n\t// Add handling for AdditionalProperties when it's a Schema\n\tif s.AdditionalProperties != nil {\n\t\tswitch v := s.AdditionalProperties.(type) {\n\t\tcase *Schema:\n\t\t\tv.DisableRequiredProperties()\n\t\tcase Schema:\n\t\t\tv.DisableRequiredProperties()\n\t\t\ts.AdditionalProperties = v\n\t\t}\n\t}\n\n\t// Handle Contains schema\n\tif s.Contains != nil {\n\t\ts.Contains.DisableRequiredProperties()\n\t}\n\n\t// Handle PropertyNames schema\n\tif s.PropertyNames != nil {\n\t\ts.PropertyNames.DisableRequiredProperties()\n\t}\n\n\t// Handle AdditionalItems when it's a Schema\n\tif s.AdditionalItems != nil {\n\t\tswitch v := s.AdditionalItems.(type) {\n\t\tcase *Schema:\n\t\t\tv.DisableRequiredProperties()\n\t\tcase Schema:\n\t\t\tv.DisableRequiredProperties()\n\t\t\ts.AdditionalItems = v\n\t\t}\n\t}\n\n\t// Handle Definitions\n\tfor _, v := range s.Definitions {\n\t\tv.DisableRequiredProperties()\n\t}\n}\n\n// GetPropertyAtPath navigates a dot-separated path and returns the schema at that location.\n// Returns nil if any part of the path doesn't exist. Empty path segments are skipped.\nfunc (s *Schema) GetPropertyAtPath(path string) *Schema {\n\tif s == nil || path == \"\" {\n\t\treturn s\n\t}\n\n\tparts := strings.Split(path, \".\")\n\tcurrent := s\n\n\tfor _, part := range parts {\n\t\tif part == \"\" {\n\t\t\tcontinue // Skip empty segments (e.g., \"foo..bar\" or \".foo\")\n\t\t}\n\t\tif current.Properties == nil {\n\t\t\treturn nil\n\t\t}\n\t\tprop, ok := current.Properties[part]\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\t\tcurrent = prop\n\t}\n\n\treturn current\n}\n\n// SetPropertyAtPath navigates a dot-separated path and ensures all intermediate schemas exist.\n// Creates intermediate object schemas as needed. Returns the schema at the final path location.\n// If the path is empty, returns the current schema. Empty path segments are skipped.\nfunc (s *Schema) SetPropertyAtPath(path string) *Schema {\n\tif s == nil || path == \"\" {\n\t\treturn s\n\t}\n\n\tparts := strings.Split(path, \".\")\n\tcurrent := s\n\n\tfor _, part := range parts {\n\t\tif part == \"\" {\n\t\t\tcontinue // Skip empty segments (e.g., \"foo..bar\" or \".foo\")\n\t\t}\n\t\tif current.Properties == nil {\n\t\t\tcurrent.Properties = make(map[string]*Schema)\n\t\t}\n\t\tif _, ok := current.Properties[part]; !ok {\n\t\t\tcurrent.Properties[part] = &Schema{\n\t\t\t\tType:       []string{\"object\"},\n\t\t\t\tTitle:      part,\n\t\t\t\tProperties: make(map[string]*Schema),\n\t\t\t}\n\t\t}\n\t\tcurrent = current.Properties[part]\n\t}\n\n\treturn current\n}\n\n// ToJson converts the data to raw json\nfunc (s Schema) ToJson() ([]byte, error) {\n\tres, err := json.MarshalIndent(&s, \"\", \"  \")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn res, nil\n}\n\n// Supported format values according to JSON Schema specification\nconst (\n\tFormatDateTime       = \"date-time\"\n\tFormatTime           = \"time\"\n\tFormatDate           = \"date\"\n\tFormatDuration       = \"duration\"\n\tFormatEmail          = \"email\"\n\tFormatIDNEmail       = \"idn-email\"\n\tFormatHostname       = \"hostname\"\n\tFormatIDNHostname    = \"idn-hostname\"\n\tFormatIPv4           = \"ipv4\"\n\tFormatIPv6           = \"ipv6\"\n\tFormatUUID           = \"uuid\"\n\tFormatURI            = \"uri\"\n\tFormatURIReference   = \"uri-reference\"\n\tFormatIRI            = \"iri\"\n\tFormatIRIReference   = \"iri-reference\"\n\tFormatURITemplate    = \"uri-template\"\n\tFormatJSONPointer    = \"json-pointer\"\n\tFormatRelJSONPointer = \"relative-json-pointer\"\n\tFormatRegex          = \"regex\"\n)\n\nvar supportedFormats = map[string]bool{\n\tFormatDateTime: true, FormatTime: true, FormatDate: true,\n\tFormatDuration: true, FormatEmail: true, FormatIDNEmail: true,\n\tFormatHostname: true, FormatIDNHostname: true, FormatIPv4: true,\n\tFormatIPv6: true, FormatUUID: true, FormatURI: true,\n\tFormatURIReference: true, FormatIRI: true, FormatIRIReference: true,\n\tFormatURITemplate: true, FormatJSONPointer: true,\n\tFormatRelJSONPointer: true, FormatRegex: true,\n}\n\n// Validate performs comprehensive validation of the schema\nfunc (s Schema) Validate() error {\n\t// Validate schema syntax\n\tif err := s.validateSchemaSyntax(); err != nil {\n\t\treturn err\n\t}\n\n\t// Validate type constraints\n\tif err := s.validateTypeConstraints(); err != nil {\n\t\treturn err\n\t}\n\n\t// Validate numeric constraints\n\tif err := s.validateNumericConstraints(); err != nil {\n\t\treturn err\n\t}\n\n\t// Validate string constraints\n\tif err := s.validateStringConstraints(); err != nil {\n\t\treturn err\n\t}\n\n\t// Validate array constraints\n\tif err := s.validateArrayConstraints(); err != nil {\n\t\treturn err\n\t}\n\n\t// Validate object constraints\n\tif err := s.validateObjectConstraints(); err != nil {\n\t\treturn err\n\t}\n\n\t// Validate nested schemas\n\tif err := s.validateNestedSchemas(); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (s Schema) validateSchemaSyntax() error {\n\tjsonStr, err := s.ToJson()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to convert schema to JSON: %w\", err)\n\t}\n\n\tc := jsonschema.NewCompiler()\n\tif err := c.AddResource(\"schema.json\", jsonStr); err != nil {\n\t\treturn fmt.Errorf(\"invalid schema syntax: %w\", err)\n\t}\n\n\treturn s.Type.Validate()\n}\n\nfunc (s Schema) validateTypeConstraints() error {\n\treturn nil\n}\n\nfunc (s Schema) validateNumericConstraints() error {\n\tif !s.hasNumericConstraints() {\n\t\treturn nil\n\t}\n\n\tif !s.Type.IsEmpty() && !s.Type.Matches(\"number\") && !s.Type.Matches(\"integer\") {\n\t\treturn fmt.Errorf(\"numeric constraints can only be used with number or integer types, got %v\", s.Type)\n\t}\n\n\tif s.MultipleOf != nil && *s.MultipleOf <= 0 {\n\t\treturn errors.New(\"multipleOf must be greater than 0\")\n\t}\n\n\tif s.Minimum != nil && s.ExclusiveMinimum != nil {\n\t\treturn errors.New(\"cannot use both minimum and exclusiveMinimum\")\n\t}\n\n\tif s.Maximum != nil && s.ExclusiveMaximum != nil {\n\t\treturn errors.New(\"cannot use both maximum and exclusiveMaximum\")\n\t}\n\n\t// Validate min <= max when both are specified\n\tif s.Minimum != nil && s.Maximum != nil && *s.Minimum > *s.Maximum {\n\t\treturn fmt.Errorf(\"minimum (%v) cannot be greater than maximum (%v)\", *s.Minimum, *s.Maximum)\n\t}\n\n\tif s.ExclusiveMinimum != nil && s.ExclusiveMaximum != nil && *s.ExclusiveMinimum >= *s.ExclusiveMaximum {\n\t\treturn fmt.Errorf(\"exclusiveMinimum (%v) must be less than exclusiveMaximum (%v)\", *s.ExclusiveMinimum, *s.ExclusiveMaximum)\n\t}\n\n\treturn nil\n}\n\nfunc (s Schema) validateStringConstraints() error {\n\tif s.Format != \"\" {\n\t\tif !s.Type.IsEmpty() && !s.Type.Matches(\"string\") {\n\t\t\treturn fmt.Errorf(\"format can only be used with string type, got %v\", s.Type)\n\t\t}\n\n\t\tif !supportedFormats[s.Format] {\n\t\t\treturn fmt.Errorf(\"unsupported format: %s\", s.Format)\n\t\t}\n\t}\n\n\tif s.Pattern != \"\" {\n\t\tif !s.Type.IsEmpty() && !s.Type.Matches(\"string\") {\n\t\t\treturn fmt.Errorf(\"pattern can only be used with string type, got %v\", s.Type)\n\t\t}\n\t\t// Validate that pattern is a valid regex\n\t\tif _, err := regexp.Compile(s.Pattern); err != nil {\n\t\t\treturn fmt.Errorf(\"invalid pattern regex: %w\", err)\n\t\t}\n\t}\n\n\tif s.Format != \"\" && s.Pattern != \"\" {\n\t\treturn errors.New(\"cannot use both format and pattern in the same schema\")\n\t}\n\n\t// Validate minLength/maxLength are only used with string type\n\tif s.MinLength != nil || s.MaxLength != nil {\n\t\tif !s.Type.IsEmpty() && !s.Type.Matches(\"string\") {\n\t\t\treturn fmt.Errorf(\"minLength/maxLength can only be used with string type, got %v\", s.Type)\n\t\t}\n\t}\n\n\tif s.MaxLength != nil && s.MinLength != nil && *s.MinLength > *s.MaxLength {\n\t\treturn fmt.Errorf(\"minLength (%d) cannot be greater than maxLength (%d)\", *s.MinLength, *s.MaxLength)\n\t}\n\n\tif s.MinLength != nil && *s.MinLength < 0 {\n\t\treturn errors.New(\"minLength must be non-negative\")\n\t}\n\n\tif s.MaxLength != nil && *s.MaxLength < 0 {\n\t\treturn errors.New(\"maxLength must be non-negative\")\n\t}\n\n\t// Validate contentEncoding and contentMediaType are only used with string type\n\tif s.ContentEncoding != \"\" || s.ContentMediaType != \"\" {\n\t\tif !s.Type.IsEmpty() && !s.Type.Matches(\"string\") {\n\t\t\treturn fmt.Errorf(\"contentEncoding/contentMediaType can only be used with string type, got %v\", s.Type)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (s Schema) validateArrayConstraints() error {\n\tif s.Items != nil {\n\t\tif !s.Type.IsEmpty() && !s.Type.Matches(\"array\") {\n\t\t\treturn fmt.Errorf(\"items can only be used with array type, got %v\", s.Type)\n\t\t}\n\n\t\tif err := s.Items.Validate(); err != nil {\n\t\t\treturn fmt.Errorf(\"invalid items schema: %w\", err)\n\t\t}\n\t}\n\n\tif s.MinItems != nil || s.MaxItems != nil {\n\t\tif !s.Type.IsEmpty() && !s.Type.Matches(\"array\") {\n\t\t\treturn fmt.Errorf(\"minItems/maxItems can only be used with array type, got %v\", s.Type)\n\t\t}\n\n\t\tif s.MinItems != nil && s.MaxItems != nil && *s.MaxItems < *s.MinItems {\n\t\t\treturn fmt.Errorf(\"maxItems (%d) cannot be less than minItems (%d)\", *s.MaxItems, *s.MinItems)\n\t\t}\n\t}\n\n\tif s.MinItems != nil && *s.MinItems < 0 {\n\t\treturn errors.New(\"minItems must be non-negative\")\n\t}\n\n\tif s.MaxItems != nil && *s.MaxItems < 0 {\n\t\treturn errors.New(\"maxItems must be non-negative\")\n\t}\n\n\t// Note: uniqueItems is a boolean that doesn't require type validation.\n\t// Per JSON Schema spec, keywords are ignored if the type doesn't match.\n\n\t// Validate contains\n\tif s.Contains != nil {\n\t\tif !s.Type.IsEmpty() && !s.Type.Matches(\"array\") {\n\t\t\treturn fmt.Errorf(\"contains can only be used with array type, got %v\", s.Type)\n\t\t}\n\t\tif err := s.Contains.Validate(); err != nil {\n\t\t\treturn fmt.Errorf(\"invalid contains schema: %w\", err)\n\t\t}\n\t}\n\n\t// Validate additionalItems\n\tif s.AdditionalItems != nil {\n\t\tif !s.Type.IsEmpty() && !s.Type.Matches(\"array\") {\n\t\t\treturn fmt.Errorf(\"additionalItems can only be used with array type, got %v\", s.Type)\n\t\t}\n\t\tswitch v := s.AdditionalItems.(type) {\n\t\tcase *Schema:\n\t\t\tif err := v.Validate(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid additionalItems schema: %w\", err)\n\t\t\t}\n\t\tcase Schema:\n\t\t\tif err := v.Validate(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid additionalItems schema: %w\", err)\n\t\t\t}\n\t\tcase bool:\n\t\t\t// Boolean is valid\n\t\tcase map[string]interface{}:\n\t\t\t// When unmarshaled from YAML, a schema object becomes map[string]interface{}\n\t\t\t// Convert and validate it\n\t\t\tschemaBytes, err := json.Marshal(v)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid additionalItems schema: %w\", err)\n\t\t\t}\n\t\t\tvar itemsSchema Schema\n\t\t\tif err := json.Unmarshal(schemaBytes, &itemsSchema); err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid additionalItems schema: %w\", err)\n\t\t\t}\n\t\t\tif err := itemsSchema.Validate(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid additionalItems schema: %w\", err)\n\t\t\t}\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"additionalItems must be a boolean or schema, got %T\", s.AdditionalItems)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (s Schema) validateObjectConstraints() error {\n\t// Validate minProperties/maxProperties\n\tif s.MinProperties != nil || s.MaxProperties != nil {\n\t\tif !s.Type.IsEmpty() && !s.Type.Matches(\"object\") {\n\t\t\treturn fmt.Errorf(\"minProperties/maxProperties can only be used with object type, got %v\", s.Type)\n\t\t}\n\n\t\tif s.MinProperties != nil && *s.MinProperties < 0 {\n\t\t\treturn errors.New(\"minProperties must be non-negative\")\n\t\t}\n\n\t\tif s.MaxProperties != nil && *s.MaxProperties < 0 {\n\t\t\treturn errors.New(\"maxProperties must be non-negative\")\n\t\t}\n\n\t\tif s.MinProperties != nil && s.MaxProperties != nil && *s.MaxProperties < *s.MinProperties {\n\t\t\treturn fmt.Errorf(\"maxProperties (%d) cannot be less than minProperties (%d)\", *s.MaxProperties, *s.MinProperties)\n\t\t}\n\t}\n\n\t// Validate propertyNames\n\tif s.PropertyNames != nil {\n\t\tif !s.Type.IsEmpty() && !s.Type.Matches(\"object\") {\n\t\t\treturn fmt.Errorf(\"propertyNames can only be used with object type, got %v\", s.Type)\n\t\t}\n\t\tif err := s.PropertyNames.Validate(); err != nil {\n\t\t\treturn fmt.Errorf(\"invalid propertyNames schema: %w\", err)\n\t\t}\n\t}\n\n\t// Validate additionalProperties type check\n\tif s.AdditionalProperties != nil {\n\t\tif !s.Type.IsEmpty() && !s.Type.Matches(\"object\") {\n\t\t\treturn fmt.Errorf(\"additionalProperties can only be used with object type, got %v\", s.Type)\n\t\t}\n\t\tswitch v := s.AdditionalProperties.(type) {\n\t\tcase *Schema:\n\t\t\tif err := v.Validate(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid additionalProperties schema: %w\", err)\n\t\t\t}\n\t\tcase Schema:\n\t\t\tif err := v.Validate(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid additionalProperties schema: %w\", err)\n\t\t\t}\n\t\tcase bool:\n\t\t\t// Boolean is valid\n\t\tcase map[string]interface{}:\n\t\t\t// When unmarshaled from YAML, a schema object becomes map[string]interface{}\n\t\t\t// Convert and validate it\n\t\t\tschemaBytes, err := json.Marshal(v)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid additionalProperties schema: %w\", err)\n\t\t\t}\n\t\t\tvar propsSchema Schema\n\t\t\tif err := json.Unmarshal(schemaBytes, &propsSchema); err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid additionalProperties schema: %w\", err)\n\t\t\t}\n\t\t\tif err := propsSchema.Validate(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid additionalProperties schema: %w\", err)\n\t\t\t}\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"additionalProperties must be a boolean or schema, got %T\", s.AdditionalProperties)\n\t\t}\n\t}\n\n\t// Validate patternProperties patterns\n\tfor pattern, patternSchema := range s.PatternProperties {\n\t\tif _, err := regexp.Compile(pattern); err != nil {\n\t\t\treturn fmt.Errorf(\"invalid pattern in patternProperties '%s': %w\", pattern, err)\n\t\t}\n\t\tif patternSchema != nil {\n\t\t\tif err := patternSchema.Validate(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid schema in patternProperties[%s]: %w\", pattern, err)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Validate dependencies\n\tfor depKey, depValue := range s.Dependencies {\n\t\tswitch v := depValue.(type) {\n\t\tcase []interface{}:\n\t\t\t// Array of property names - validate they are strings\n\t\t\tfor i, item := range v {\n\t\t\t\tif _, ok := item.(string); !ok {\n\t\t\t\t\treturn fmt.Errorf(\"dependencies[%s][%d] must be a string, got %T\", depKey, i, item)\n\t\t\t\t}\n\t\t\t}\n\t\tcase map[string]interface{}:\n\t\t\t// Schema - convert and validate\n\t\t\tschemaBytes, err := json.Marshal(v)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid schema in dependencies[%s]: %w\", depKey, err)\n\t\t\t}\n\t\t\tvar depSchema Schema\n\t\t\tif err := json.Unmarshal(schemaBytes, &depSchema); err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid schema in dependencies[%s]: %w\", depKey, err)\n\t\t\t}\n\t\t\tif err := depSchema.Validate(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid schema in dependencies[%s]: %w\", depKey, err)\n\t\t\t}\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"dependencies[%s] must be an array of strings or a schema, got %T\", depKey, depValue)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (s Schema) validateNestedSchemas() error {\n\t// Validate combinatorial schemas\n\tfor _, schemas := range [][]*Schema{s.AllOf, s.AnyOf, s.OneOf} {\n\t\tfor _, schema := range schemas {\n\t\t\tif err := schema.Validate(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\t// Validate conditional schemas\n\tfor _, schema := range []*Schema{s.If, s.Then, s.Else, s.Not} {\n\t\tif schema != nil {\n\t\t\tif err := schema.Validate(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\t// Validate definitions\n\tfor name, defSchema := range s.Definitions {\n\t\tif defSchema != nil {\n\t\t\tif err := defSchema.Validate(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid schema in definitions[%s]: %w\", name, err)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Validate nested properties\n\tfor name, propSchema := range s.Properties {\n\t\tif propSchema != nil {\n\t\t\tif err := propSchema.Validate(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid schema in properties[%s]: %w\", name, err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (s Schema) hasNumericConstraints() bool {\n\treturn s.Minimum != nil || s.Maximum != nil ||\n\t\ts.ExclusiveMinimum != nil || s.ExclusiveMaximum != nil ||\n\t\ts.MultipleOf != nil\n}\n\nvar possibleSkipFields = []string{\"type\", \"title\", \"description\", \"required\", \"default\", \"additionalProperties\"}\n\ntype SkipAutoGenerationConfig struct {\n\tType, Title, Description, Required, Default, AdditionalProperties bool\n}\n\nfunc NewSkipAutoGenerationConfig(flag []string) (*SkipAutoGenerationConfig, error) {\n\tvar config SkipAutoGenerationConfig\n\n\tvar invalidFlags []string\n\n\tfor _, fieldName := range flag {\n\t\tif !slices.Contains(possibleSkipFields, fieldName) {\n\t\t\tinvalidFlags = append(invalidFlags, fieldName)\n\t\t}\n\t\tif fieldName == \"type\" {\n\t\t\tconfig.Type = true\n\t\t}\n\t\tif fieldName == \"title\" {\n\t\t\tconfig.Title = true\n\t\t}\n\t\tif fieldName == \"description\" {\n\t\t\tconfig.Description = true\n\t\t}\n\t\tif fieldName == \"required\" {\n\t\t\tconfig.Required = true\n\t\t}\n\t\tif fieldName == \"default\" {\n\t\t\tconfig.Default = true\n\t\t}\n\t\tif fieldName == \"additionalProperties\" {\n\t\t\tconfig.AdditionalProperties = true\n\t\t}\n\t}\n\n\tif len(invalidFlags) != 0 {\n\t\treturn nil, fmt.Errorf(\"unsupported field names '%s' for skipping auto-generation\", strings.Join(invalidFlags, \"', '\"))\n\t}\n\n\treturn &config, nil\n}\n\nfunc typeFromTag(tag string) ([]string, error) {\n\tswitch tag {\n\tcase nullTag:\n\t\treturn []string{\"null\"}, nil\n\tcase boolTag:\n\t\treturn []string{\"boolean\"}, nil\n\tcase strTag:\n\t\treturn []string{\"string\"}, nil\n\tcase intTag:\n\t\treturn []string{\"integer\"}, nil\n\tcase floatTag:\n\t\treturn []string{\"number\"}, nil\n\tcase timestampTag:\n\t\treturn []string{\"string\"}, nil\n\tcase arrayTag:\n\t\treturn []string{\"array\"}, nil\n\tcase mapTag:\n\t\treturn []string{\"object\"}, nil\n\t}\n\treturn []string{}, fmt.Errorf(\"unsupported yaml tag found: %s\", tag)\n}\n\n// FixRequiredProperties iterates over the properties and checks if required has a boolean value.\n// Then the property is added to the parents required property list\nfunc FixRequiredProperties(schema *Schema) error {\n\tif schema.Properties != nil {\n\t\tfor propName, propValue := range schema.Properties {\n\t\t\tFixRequiredProperties(propValue)\n\t\t\tif propValue.Required.Bool && !slices.Contains(schema.Required.Strings, propName) {\n\t\t\t\tschema.Required.Strings = append(schema.Required.Strings, propName)\n\t\t\t}\n\t\t}\n\t\tif !slices.Contains(schema.Type, \"object\") {\n\t\t\t// If .Properties is set, type must be object\n\t\t\tif len(schema.Type) == 0 {\n\t\t\t\tschema.Type = []string{\"object\"}\n\t\t\t} else {\n\t\t\t\tschema.Type = append(schema.Type, \"object\")\n\t\t\t}\n\t\t}\n\t}\n\n\tif schema.Then != nil {\n\t\tFixRequiredProperties(schema.Then)\n\t}\n\n\tif schema.If != nil {\n\t\tFixRequiredProperties(schema.If)\n\t}\n\n\tif schema.Else != nil {\n\t\tFixRequiredProperties(schema.Else)\n\t}\n\n\tif schema.Items != nil {\n\t\tFixRequiredProperties(schema.Items)\n\t}\n\n\tif schema.AdditionalProperties != nil {\n\t\tswitch v := schema.AdditionalProperties.(type) {\n\t\tcase *Schema:\n\t\t\tFixRequiredProperties(v)\n\t\tcase Schema:\n\t\t\tFixRequiredProperties(&v)\n\t\t\tschema.AdditionalProperties = v\n\t\t}\n\t}\n\n\tif len(schema.AnyOf) > 0 {\n\t\tfor _, subSchema := range schema.AnyOf {\n\t\t\tFixRequiredProperties(subSchema)\n\t\t}\n\t}\n\n\tif len(schema.AllOf) > 0 {\n\t\tfor _, subSchema := range schema.AllOf {\n\t\t\tFixRequiredProperties(subSchema)\n\t\t}\n\t}\n\n\tif len(schema.OneOf) > 0 {\n\t\tfor _, subSchema := range schema.OneOf {\n\t\t\tFixRequiredProperties(subSchema)\n\t\t}\n\t}\n\n\tif schema.Not != nil {\n\t\tFixRequiredProperties(schema.Not)\n\t}\n\n\t// Handle Contains schema\n\tif schema.Contains != nil {\n\t\tFixRequiredProperties(schema.Contains)\n\t}\n\n\t// Handle PropertyNames schema\n\tif schema.PropertyNames != nil {\n\t\tFixRequiredProperties(schema.PropertyNames)\n\t}\n\n\t// Handle AdditionalItems when it's a Schema\n\tif schema.AdditionalItems != nil {\n\t\tswitch v := schema.AdditionalItems.(type) {\n\t\tcase *Schema:\n\t\t\tFixRequiredProperties(v)\n\t\tcase Schema:\n\t\t\tFixRequiredProperties(&v)\n\t\t\tschema.AdditionalItems = v\n\t\t}\n\t}\n\n\t// Handle Definitions\n\tfor _, defSchema := range schema.Definitions {\n\t\tFixRequiredProperties(defSchema)\n\t}\n\n\t// Handle PatternProperties\n\tfor _, patternSchema := range schema.PatternProperties {\n\t\tFixRequiredProperties(patternSchema)\n\t}\n\n\treturn nil\n}\n\n// applyRootSchemaProperties copies root-level schema properties from source to target.\n// Used for applying @schema.root annotations.\nfunc (s *Schema) applyRootSchemaProperties(source *Schema, valuesPath string) error {\n\tif source.Title != \"\" {\n\t\ts.Title = source.Title\n\t}\n\tif source.Description != \"\" {\n\t\ts.Description = source.Description\n\t}\n\tif source.Ref != \"\" {\n\t\tif err := handleSchemaRefs(source, valuesPath); err != nil {\n\t\t\treturn err\n\t\t}\n\t\ts.Ref = source.Ref\n\t}\n\tif len(source.Examples) > 0 {\n\t\ts.Examples = source.Examples\n\t}\n\tif source.Deprecated {\n\t\ts.Deprecated = source.Deprecated\n\t}\n\tif source.ReadOnly {\n\t\ts.ReadOnly = source.ReadOnly\n\t}\n\tif source.WriteOnly {\n\t\ts.WriteOnly = source.WriteOnly\n\t}\n\tif source.AdditionalProperties != nil {\n\t\ts.AdditionalProperties = source.AdditionalProperties\n\t}\n\tif len(source.Required.Strings) > 0 || source.Required.Bool {\n\t\ts.Required = source.Required\n\t}\n\tif len(source.PatternProperties) > 0 {\n\t\tif s.PatternProperties == nil {\n\t\t\ts.PatternProperties = make(map[string]*Schema)\n\t\t}\n\t\tfor k, v := range source.PatternProperties {\n\t\t\ts.PatternProperties[k] = v\n\t\t}\n\t}\n\tif len(source.Definitions) > 0 {\n\t\tif s.Definitions == nil {\n\t\t\ts.Definitions = make(map[string]*Schema)\n\t\t}\n\t\tfor k, v := range source.Definitions {\n\t\t\ts.Definitions[k] = v\n\t\t}\n\t}\n\tif len(source.AllOf) > 0 {\n\t\ts.AllOf = source.AllOf\n\t}\n\tif len(source.AnyOf) > 0 {\n\t\ts.AnyOf = source.AnyOf\n\t}\n\tif len(source.OneOf) > 0 {\n\t\ts.OneOf = source.OneOf\n\t}\n\tif source.Not != nil {\n\t\ts.Not = source.Not\n\t}\n\tif len(source.CustomAnnotations) > 0 {\n\t\tif s.CustomAnnotations == nil {\n\t\t\ts.CustomAnnotations = make(map[string]interface{})\n\t\t}\n\t\tfor k, v := range source.CustomAnnotations {\n\t\t\ts.CustomAnnotations[k] = v\n\t\t}\n\t}\n\treturn nil\n}\n\n// GetRootSchemaFromComment parses root-level schema annotations (marked with @schema.root)\n// from a comment and returns the schema, the remaining comment (without root annotations),\n// and any error. Root schema annotations are useful for applying schema properties to the\n// entire values file rather than individual keys.\nfunc GetRootSchemaFromComment(comment string) (Schema, string, error) {\n\tvar result Schema\n\tscanner := bufio.NewScanner(strings.NewReader(comment))\n\trootSchemaLines := []string{}\n\tremainingCommentLines := []string{}\n\tinsideRootSchemaBlock := false\n\tfoundRootSchema := false\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tif strings.HasPrefix(line, SchemaRootPrefix) {\n\t\t\tinsideRootSchemaBlock = !insideRootSchemaBlock\n\t\t\tfoundRootSchema = true\n\t\t\tcontinue\n\t\t}\n\t\tif insideRootSchemaBlock {\n\t\t\tcontent := strings.TrimPrefix(line, CommentPrefix)\n\t\t\trootSchemaLines = append(rootSchemaLines, strings.TrimPrefix(strings.TrimPrefix(content, CommentPrefix), \" \"))\n\t\t\tresult.Set()\n\t\t} else {\n\t\t\tremainingCommentLines = append(remainingCommentLines, line)\n\t\t}\n\t}\n\n\tif insideRootSchemaBlock {\n\t\treturn result, \"\",\n\t\t\tfmt.Errorf(\"unclosed root schema block found in comment: %s\", comment)\n\t}\n\n\tif foundRootSchema {\n\t\terr := yaml.Unmarshal([]byte(strings.Join(rootSchemaLines, \"\\n\")), &result)\n\t\tif err != nil {\n\t\t\treturn result, \"\", err\n\t\t}\n\t}\n\n\treturn result, strings.Join(remainingCommentLines, \"\\n\"), nil\n}\n\n// GetSchemaFromComment parses the annotations from the given comment\nfunc GetSchemaFromComment(comment string) (Schema, string, error) {\n\tvar result Schema\n\tscanner := bufio.NewScanner(strings.NewReader(comment))\n\tdescription := []string{}\n\trawSchema := []string{}\n\tinsideSchemaBlock := false\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tif strings.HasPrefix(line, SchemaPrefix) {\n\t\t\tinsideSchemaBlock = !insideSchemaBlock\n\t\t\tcontinue\n\t\t}\n\t\tif insideSchemaBlock {\n\t\t\tcontent := strings.TrimPrefix(line, CommentPrefix)\n\t\t\trawSchema = append(rawSchema, strings.TrimPrefix(strings.TrimPrefix(content, CommentPrefix), \" \"))\n\t\t\tresult.Set()\n\t\t} else {\n\t\t\tdescription = append(description, strings.TrimPrefix(strings.TrimPrefix(line, CommentPrefix), \" \"))\n\t\t}\n\t}\n\n\tif insideSchemaBlock {\n\t\treturn result, \"\",\n\t\t\tfmt.Errorf(\"unclosed schema block found in comment: %s\", comment)\n\t}\n\n\terr := yaml.Unmarshal([]byte(strings.Join(rawSchema, \"\\n\")), &result)\n\tif err != nil {\n\t\treturn result, \"\", err\n\t}\n\n\treturn result, strings.Join(description, \"\\n\"), nil\n}\n\n// YamlToSchema recursively parses a YAML node and creates a JSON Schema from it\n// Parameters:\n//   - valuesPath: path to the values file being processed\n//   - node: current YAML node being processed\n//   - keepFullComment: whether to preserve all comment text\n//   - helmDocsCompatibilityMode: whether to parse helm-docs annotations\n//   - dontRemoveHelmDocsPrefix: whether to keep helm-docs prefixes in comments\n//   - skipAutoGeneration: configuration for which fields should not be auto-generated\n//   - parentRequiredProperties: list of required properties to populate in parent\n//\n// Returns:\n//   - The generated Schema and any error encountered during parsing\nfunc YamlToSchema(\n\tvaluesPath string,\n\tnode *yaml.Node,\n\tkeepFullComment bool,\n\thelmDocsCompatibilityMode bool,\n\tdontRemoveHelmDocsPrefix bool,\n\tdontAddGlobal bool,\n\tskipAutoGeneration *SkipAutoGenerationConfig,\n\tparentRequiredProperties *[]string,\n) (*Schema, error) {\n\tschema := NewSchema(\"object\")\n\n\tswitch node.Kind {\n\tcase yaml.DocumentNode:\n\t\tif len(node.Content) != 1 {\n\t\t\treturn nil, fmt.Errorf(\"unexpected yaml document structure: expected 1 content node, got %d\", len(node.Content))\n\t\t}\n\n\t\tschema.Schema = \"http://json-schema.org/draft-07/schema#\"\n\n\t\t// Check document-level HeadComment for @schema.root (handles blank-line separated case)\n\t\tif node.HeadComment != \"\" {\n\t\t\tif docRootSchema, _, err := GetRootSchemaFromComment(node.HeadComment); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"error parsing root schema from document comment: %w\", err)\n\t\t\t} else if docRootSchema.HasData {\n\t\t\t\tif err := schema.applyRootSchemaProperties(&docRootSchema, valuesPath); err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"error applying root schema from document comment: %w\", err)\n\t\t\t\t}\n\t\t\t\tif err := docRootSchema.Validate(); err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"error validating root schema from document comment: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tchildSchema, err := YamlToSchema(\n\t\t\tvaluesPath,\n\t\t\tnode.Content[0],\n\t\t\tkeepFullComment,\n\t\t\thelmDocsCompatibilityMode,\n\t\t\tdontRemoveHelmDocsPrefix,\n\t\t\tdontAddGlobal,\n\t\t\tskipAutoGeneration,\n\t\t\t&schema.Required.Strings,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tschema.Properties = childSchema.Properties\n\n\t\t// Apply root schema properties from child if they were set\n\t\tif err := schema.applyRootSchemaProperties(childSchema, valuesPath); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error applying root schema properties from child: %w\", err)\n\t\t}\n\n\t\tif _, ok := schema.Properties[\"global\"]; !ok && !dontAddGlobal {\n\t\t\t// global key must be present, otherwise helm lint will fail\n\t\t\tif schema.Properties == nil {\n\t\t\t\tschema.Properties = make(map[string]*Schema)\n\t\t\t}\n\t\t\tschema.Properties[\"global\"] = NewSchema(\n\t\t\t\t\"object\",\n\t\t\t)\n\t\t\tif !skipAutoGeneration.Title {\n\t\t\t\tschema.Properties[\"global\"].Title = \"global\"\n\t\t\t}\n\t\t\tif !skipAutoGeneration.Description {\n\t\t\t\tschema.Properties[\"global\"].Description = \"Global values are values that can be accessed from any chart or subchart by exactly the same name.\"\n\t\t\t}\n\t\t}\n\n\t\t// always disable on top level (unless root schema specifies otherwise)\n\t\tif !skipAutoGeneration.AdditionalProperties && schema.AdditionalProperties == nil {\n\t\t\tschema.AdditionalProperties = new(bool)\n\t\t}\n\tcase yaml.MappingNode:\n\t\t// Check if the first key has root schema annotations (only for root-level mappings)\n\t\tif len(node.Content) > 0 && parentRequiredProperties != nil {\n\t\t\tfirstKeyNode := node.Content[0]\n\n\t\t\t// Try to extract root schema annotations (adjacent to first key)\n\t\t\trootSchema, remainingComment, err := GetRootSchemaFromComment(firstKeyNode.HeadComment)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"error parsing root schema comment: %w\", err)\n\t\t\t}\n\n\t\t\tif rootSchema.HasData {\n\t\t\t\tif err := schema.applyRootSchemaProperties(&rootSchema, valuesPath); err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"error applying root schema: %w\", err)\n\t\t\t\t}\n\t\t\t\tif err := rootSchema.Validate(); err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"error validating root schema: %w\", err)\n\t\t\t\t}\n\t\t\t\t// Update the first key's comment to exclude the root schema annotations\n\t\t\t\tfirstKeyNode.HeadComment = remainingComment\n\t\t\t}\n\t\t}\n\n\t\tfor i := 0; i < len(node.Content); i += 2 {\n\t\t\tkeyNode := node.Content[i]\n\t\t\tvalueNode := node.Content[i+1]\n\n\t\t\tif valueNode.Kind == yaml.AliasNode {\n\t\t\t\tvalueNode = valueNode.Alias\n\t\t\t}\n\n\t\t\tcomment := keyNode.HeadComment\n\t\t\tif !keepFullComment {\n\t\t\t\tcomment = leadingCommentsRemover.ReplaceAllString(comment, \"\")\n\t\t\t}\n\n\t\t\tkeyNodeSchema, description, err := GetSchemaFromComment(comment)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"error parsing comment of key %s: %w\", keyNode.Value, err)\n\t\t\t}\n\n\t\t\tif helmDocsCompatibilityMode {\n\t\t\t\t_, helmDocsValue := helm.ParseComment(strings.Split(keyNode.HeadComment, \"\\n\"))\n\t\t\t\tif helmDocsValue.Default != \"\" {\n\t\t\t\t\tkeyNodeSchema.Set()\n\t\t\t\t\tkeyNodeSchema.Default = helmDocsValue.Default\n\t\t\t\t}\n\t\t\t\tif helmDocsValue.Description != \"\" {\n\t\t\t\t\tkeyNodeSchema.Set()\n\t\t\t\t\tkeyNodeSchema.Description = helmDocsValue.Description\n\t\t\t\t}\n\t\t\t\tif helmDocsValue.ValueType != \"\" {\n\t\t\t\t\thelmDocsType, err := helmDocsTypeToSchemaType(helmDocsValue.ValueType)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.Warnln(err)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tkeyNodeSchema.Set()\n\t\t\t\t\t\tkeyNodeSchema.Type = StringOrArrayOfString{helmDocsType}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !dontRemoveHelmDocsPrefix {\n\t\t\t\t// remove all lines containing helm-docs @tags, like @ignored, or one of those:\n\t\t\t\t// https://github.com/norwoodj/helm-docs/blob/v1.14.2/pkg/helm/chart_info.go#L18-L24\n\t\t\t\tdescription = helmDocsTagsRemover.ReplaceAllString(description, \"\")\n\t\t\t\tdescription = helmDocsPrefixRemover.ReplaceAllString(description, \"\")\n\t\t\t}\n\n\t\t\tif keyNodeSchema.Ref != \"\" || len(keyNodeSchema.PatternProperties) > 0 {\n\t\t\t\t// Handle $ref in main schema and pattern properties\n\t\t\t\tif err := handleSchemaRefs(&keyNodeSchema, valuesPath); err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"error resolving $ref for key %s: %w\", keyNode.Value, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif keyNodeSchema.ConstFromValue {\n\t\t\t\tif keyNodeSchema.constWasSet {\n\t\t\t\t\treturn nil, fmt.Errorf(\"error validating schema of key %s: const and const-from-value cannot be used together\", keyNode.Value)\n\t\t\t\t}\n\n\t\t\t\tdecodedValue, err := decodeNodeValue(valueNode)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"error decoding value for const-from-value on key %s: %w\", keyNode.Value, err)\n\t\t\t\t}\n\n\t\t\t\tkeyNodeSchema.Const = decodedValue\n\t\t\t\tkeyNodeSchema.constWasSet = true\n\t\t\t}\n\n\t\t\tif keyNodeSchema.HasData {\n\t\t\t\tif err := keyNodeSchema.Validate(); err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"error validating schema of key %s: %w\", keyNode.Value, err)\n\t\t\t\t}\n\t\t\t} else if !skipAutoGeneration.Type {\n\t\t\t\tnodeType, err := typeFromTag(valueNode.Tag)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"error inferring type for key %s: %w\", keyNode.Value, err)\n\t\t\t\t}\n\t\t\t\tkeyNodeSchema.Type = nodeType\n\t\t\t}\n\n\t\t\t// only validate or default if $ref is not set\n\t\t\tif keyNodeSchema.Ref == \"\" {\n\n\t\t\t\t// Add key to required array of parent\n\t\t\t\tif keyNodeSchema.Required.Bool || (len(keyNodeSchema.Required.Strings) == 0 && !skipAutoGeneration.Required && !keyNodeSchema.HasData) {\n\t\t\t\t\tif !slices.Contains(*parentRequiredProperties, keyNode.Value) {\n\t\t\t\t\t\t*parentRequiredProperties = append(*parentRequiredProperties, keyNode.Value)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif !skipAutoGeneration.AdditionalProperties && valueNode.Kind == yaml.MappingNode &&\n\t\t\t\t\t(!keyNodeSchema.HasData || keyNodeSchema.AdditionalProperties == nil) {\n\t\t\t\t\tkeyNodeSchema.AdditionalProperties = new(bool)\n\t\t\t\t}\n\n\t\t\t\t// If no title was set, use the key value\n\t\t\t\tif keyNodeSchema.Title == \"\" && !skipAutoGeneration.Title {\n\t\t\t\t\tkeyNodeSchema.Title = keyNode.Value\n\t\t\t\t}\n\n\t\t\t\t// If no description was set, use the rest of the comment as description\n\t\t\t\tif keyNodeSchema.Description == \"\" && !skipAutoGeneration.Description {\n\t\t\t\t\tkeyNodeSchema.Description = description\n\t\t\t\t}\n\n\t\t\t\t// If no default value was set, use the values node value as default\n\t\t\t\tif !skipAutoGeneration.Default && keyNodeSchema.Default == nil && valueNode.Kind == yaml.ScalarNode {\n\t\t\t\t\tkeyNodeSchema.Default = castNodeValueByType(valueNode.Value, keyNodeSchema.Type)\n\t\t\t\t}\n\n\t\t\t\t// If the value is another map and no properties are set, get them from default values\n\t\t\t\tif valueNode.Kind == yaml.MappingNode && keyNodeSchema.Properties == nil {\n\t\t\t\t\tkeyNodeSchema.Properties = make(map[string]*Schema)\n\n\t\t\t\t\tgeneratedSchema, err := YamlToSchema(\n\t\t\t\t\t\tvaluesPath,\n\t\t\t\t\t\tvalueNode,\n\t\t\t\t\t\tkeepFullComment,\n\t\t\t\t\t\thelmDocsCompatibilityMode,\n\t\t\t\t\t\tdontRemoveHelmDocsPrefix,\n\t\t\t\t\t\tdontAddGlobal,\n\t\t\t\t\t\tskipAutoGeneration,\n\t\t\t\t\t\t&keyNodeSchema.Required.Strings,\n\t\t\t\t\t)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t\tgeneratedProperties := generatedSchema.Properties\n\n\t\t\t\t\t// Process each property\n\t\t\t\t\tfor i := 0; i < len(valueNode.Content); i += 2 {\n\t\t\t\t\t\tpropKeyNode := valueNode.Content[i]\n\n\t\t\t\t\t\t// Check if this specific property matches any pattern\n\t\t\t\t\t\tskipProperty := false\n\t\t\t\t\t\tfor pattern := range keyNodeSchema.PatternProperties {\n\t\t\t\t\t\t\tmatched, err := regexp.MatchString(pattern, propKeyNode.Value)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn nil, fmt.Errorf(\"invalid pattern '%s' in patternProperties: %w\", pattern, err)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif matched {\n\t\t\t\t\t\t\t\tskipProperty = true\n\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Only add schema for non-skipped properties\n\t\t\t\t\t\tif !skipProperty {\n\t\t\t\t\t\t\tif prop, exists := generatedProperties[propKeyNode.Value]; exists {\n\t\t\t\t\t\t\t\tkeyNodeSchema.Properties[propKeyNode.Value] = prop\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if valueNode.Kind == yaml.SequenceNode && keyNodeSchema.Items == nil {\n\t\t\t\t\t// If the value is a sequence, but no items are predefined\n\t\t\t\t\tseqSchema := NewSchema(\"\")\n\n\t\t\t\t\tfor _, itemNode := range valueNode.Content {\n\t\t\t\t\t\tif itemNode.Kind == yaml.ScalarNode {\n\t\t\t\t\t\t\titemNodeType, err := typeFromTag(itemNode.Tag)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn nil, fmt.Errorf(\"error inferring type for array item: %w\", err)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tseqSchema.AnyOf = append(seqSchema.AnyOf, NewSchema(itemNodeType[0]))\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\titemRequiredProperties := []string{}\n\t\t\t\t\t\t\titemSchema, err := YamlToSchema(valuesPath, itemNode, keepFullComment, helmDocsCompatibilityMode, dontRemoveHelmDocsPrefix, dontAddGlobal, skipAutoGeneration, &itemRequiredProperties)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\titemSchema.Required.Strings = append(itemSchema.Required.Strings, itemRequiredProperties...)\n\n\t\t\t\t\t\t\tif !skipAutoGeneration.AdditionalProperties && itemNode.Kind == yaml.MappingNode && (!itemSchema.HasData || itemSchema.AdditionalProperties == nil) {\n\t\t\t\t\t\t\t\titemSchema.AdditionalProperties = new(bool)\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tseqSchema.AnyOf = append(seqSchema.AnyOf, itemSchema)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tkeyNodeSchema.Items = seqSchema\n\n\t\t\t\t\t// Because the `required` field isn't valid jsonschema (but just a helper boolean)\n\t\t\t\t\t// we must convert them to valid requiredProperties fields\n\t\t\t\t\tFixRequiredProperties(&keyNodeSchema)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif schema.Properties == nil {\n\t\t\t\tschema.Properties = make(map[string]*Schema)\n\t\t\t}\n\t\t\tschema.Properties[keyNode.Value] = &keyNodeSchema\n\t\t}\n\t}\n\n\treturn schema, nil\n}\n\nfunc helmDocsTypeToSchemaType(helmDocsType string) (string, error) {\n\tswitch helmDocsType {\n\tcase \"int\":\n\t\treturn \"integer\", nil\n\tcase \"bool\":\n\t\treturn \"boolean\", nil\n\tcase \"float\":\n\t\treturn \"number\", nil\n\tcase \"list\":\n\t\treturn \"array\", nil\n\tcase \"map\":\n\t\treturn \"object\", nil\n\tcase \"tpl\":\n\t\treturn \"string\", nil\n\tcase \"string\", \"object\":\n\t\treturn helmDocsType, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"cant translate helm-docs type (%s) to helm-schema type\", helmDocsType)\n}\n\n// castNodeValueByType attempts to convert a raw string value into the appropriate type based on\n// the provided fieldType. It handles boolean, integer, and number conversions. If the conversion\n// fails or the type is not supported (e.g., string), it returns the original raw value.\n//\n// Parameters:\n//   - rawValue: The string value to be converted\n//   - fieldType: Array of allowed JSON Schema types for this field\n//\n// Returns:\n//   - The converted value as interface{}, or the original string if conversion fails/isn't needed\nfunc castNodeValueByType(rawValue string, fieldType StringOrArrayOfString) any {\n\tif len(fieldType) == 0 {\n\t\treturn rawValue\n\t}\n\n\t// rawValue must be one of fielTypes\n\tfor _, t := range fieldType {\n\t\tswitch t {\n\t\tcase \"boolean\":\n\t\t\tswitch rawValue {\n\t\t\tcase \"true\":\n\t\t\t\treturn true\n\t\t\tcase \"false\":\n\t\t\t\treturn false\n\t\t\t}\n\t\tcase \"integer\":\n\t\t\tv, err := strconv.Atoi(rawValue)\n\t\t\tif err == nil {\n\t\t\t\treturn v\n\t\t\t}\n\t\tcase \"number\":\n\t\t\tv, err := strconv.ParseFloat(rawValue, 64)\n\t\t\tif err == nil {\n\t\t\t\treturn v\n\t\t\t}\n\t\t}\n\t}\n\n\treturn rawValue\n}\n\n// decodeNodeValue converts a yaml.Node into a plain Go value suitable for JSON Schema keywords.\n// Aliases are expanded by yaml.v3 during Decode.\nfunc decodeNodeValue(node *yaml.Node) (interface{}, error) {\n\tvar value interface{}\n\tif err := node.Decode(&value); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn value, nil\n}\n\n// handleSchemaRefs processes and resolves JSON Schema references ($ref) within a schema.\n// It handles both direct schema references and references within patternProperties.\n// For each reference:\n// - If it's a relative file path, it attempts to load and parse the referenced schema\n// - If it includes a JSON pointer (#/path/to/schema), it extracts the specific schema section\n// - The resolved schema replaces the original reference\n//\n// Parameters:\n//   - schema: Pointer to the Schema object containing the references to resolve\n//   - valuesPath: Path to the current values file, used for resolving relative paths\n//\n// Returns:\n//   - An error if the reference cannot be resolved, or nil on success\nfunc handleSchemaRefs(schema *Schema, valuesPath string) error {\n\t// Handle main schema $ref\n\tif schema.Ref != \"\" {\n\t\tfileRef, jsonPointer, hasJSONPointer := strings.Cut(schema.Ref, \"#\")\n\t\tif fileRef == \"\" {\n\t\t\treturn nil\n\t\t}\n\n\t\trelFilePath, err := util.IsRelativeFile(valuesPath, fileRef)\n\t\tif err != nil {\n\t\t\t// Not a relative file path, may be handled elsewhere\n\t\t\tlog.Debug(err)\n\t\t\treturn nil\n\t\t}\n\n\t\tvar relSchema Schema\n\t\tfile, err := os.Open(relFilePath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to open referenced schema file %s: %w\", relFilePath, err)\n\t\t}\n\t\tdefer file.Close()\n\n\t\tbyteValue, err := io.ReadAll(file)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to read referenced schema file %s: %w\", relFilePath, err)\n\t\t}\n\n\t\tif hasJSONPointer && jsonPointer != \"\" {\n\t\t\t// Found json-pointer\n\t\t\tvar obj interface{}\n\t\t\tif err := json.Unmarshal(byteValue, &obj); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to unmarshal JSON from %s: %w\", relFilePath, err)\n\t\t\t}\n\t\t\tjsonPointerResultRaw, err := jsonpointer.Get(obj, jsonPointer)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to resolve JSON pointer %s in %s: %w\", jsonPointer, relFilePath, err)\n\t\t\t}\n\t\t\tjsonPointerResultMarshaled, err := json.Marshal(jsonPointerResultRaw)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to marshal JSON pointer result from %s: %w\", relFilePath, err)\n\t\t\t}\n\t\t\tif err := json.Unmarshal(jsonPointerResultMarshaled, &relSchema); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to unmarshal JSON pointer result from %s: %w\", relFilePath, err)\n\t\t\t}\n\t\t} else {\n\t\t\t// No json-pointer\n\t\t\tif err := json.Unmarshal(byteValue, &relSchema); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to unmarshal schema from %s: %w\", relFilePath, err)\n\t\t\t}\n\t\t}\n\t\t*schema = relSchema\n\t\tschema.HasData = true\n\t}\n\n\t// Handle $ref in pattern properties\n\tif schema.PatternProperties != nil {\n\t\tfor pattern, subSchema := range schema.PatternProperties {\n\t\t\tif subSchema.Ref != \"\" {\n\t\t\t\tif err := handleSchemaRefs(subSchema, valuesPath); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to resolve $ref in patternProperties[%s]: %w\", pattern, err)\n\t\t\t\t}\n\t\t\t\tschema.PatternProperties[pattern] = subSchema // Update the original schema in the map\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/schema/schema_test.go",
    "content": "package schema\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/magiconair/properties/assert\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc TestValidate(t *testing.T) {\n\ttests := []struct {\n\t\tcomment       string\n\t\texpectedValid bool\n\t}{\n\t\t{\n\t\t\tcomment: `\n# @schema\n# multipleOf: 0\n# @schema`,\n\t\t\texpectedValid: false,\n\t\t},\n\t\t{\n\t\t\tcomment: `\n# @schema\n# type: doesnotexist\n# @schema`,\n\t\t\texpectedValid: false,\n\t\t},\n\t\t{\n\t\t\tcomment: `\n# @schema\n# type: [doesnotexist, string]\n# @schema`,\n\t\t\texpectedValid: false,\n\t\t},\n\t\t{\n\t\t\tcomment: `\n# @schema\n# type: [string, integer]\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tcomment: `\n# @schema\n# type: string\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tcomment: `\n# @schema\n# const: \"hello\"\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tcomment: `\n# @schema\n# const: true\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tcomment: `\n# @schema\n# const: null\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tcomment: `\n# @schema\n# format: ipv4\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tcomment: `\n# @schema\n# pattern: ^foo\n# format: ipv4\n# @schema`,\n\t\t\texpectedValid: false,\n\t\t},\n\t\t{\n\t\t\tcomment: `\n# @schema\n# readOnly: true\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tcomment: `\n# @schema\n# writeOnly: true\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tcomment: `\n# @schema\n# anyOf:\n#   - type: \"null\"\n#   - format: date-time\n#   - format: date\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tcomment: `\n# @schema\n# not:\n#   type: \"null\"\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tcomment: `\n# @schema\n# anyOf:\n#   - type: \"null\"\n#   - format: date-time\n# if:\n#   type: \"null\"\n# then:\n#   description: If set to null, this will do nothing\n# else:\n#   description: Here goes the description for date-time\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tcomment: `\n# @schema\n# $ref: https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.29.2/affinity-v1.json\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tcomment: `\n# @schema\n# minLength: 1\n# maxLength: 0\n# @schema`,\n\t\t\texpectedValid: false,\n\t\t},\n\t\t{\n\t\t\tcomment: `\n# @schema\n# minLength: 1\n# maxLength: 2\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tcomment: `\n# @schema\n# minItems: 1\n# maxItems: 2\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tcomment: `\n# @schema\n# minItems: 2\n# maxItems: 1\n# @schema`,\n\t\t\texpectedValid: false,\n\t\t},\n\t\t{\n\t\t\tcomment: `\n# @schema\n# type: string\n# minItems: 1\n# @schema`,\n\t\t\texpectedValid: false,\n\t\t},\n\t\t{\n\t\t\tcomment: `\n# @schema\n# type: boolean\n# uniqueItems: true\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tschema, _, err := GetSchemaFromComment(test.comment)\n\t\tif err != nil && test.expectedValid {\n\t\t\tt.Errorf(\n\t\t\t\t\"Expected the schema %s to be valid=%t, but can't even parse it: %v\",\n\t\t\t\ttest.comment,\n\t\t\t\ttest.expectedValid,\n\t\t\t\terr,\n\t\t\t)\n\t\t}\n\t\terr = schema.Validate()\n\t\tvalid := err == nil\n\t\tif valid != test.expectedValid {\n\t\t\tt.Errorf(\n\t\t\t\t\"Expected schema\\n%s\\n\\n to be valid=%t, but it's %t\",\n\t\t\t\ttest.comment,\n\t\t\t\ttest.expectedValid,\n\t\t\t\tvalid,\n\t\t\t)\n\t\t}\n\t}\n}\n\nfunc TestUnmarshalYAML(t *testing.T) {\n\tyamlData := `\ntype: string\nx-custom-foo: bar\n`\n\n\tvar schema Schema\n\tif err := yaml.Unmarshal([]byte(yamlData), &schema); err != nil {\n\t\tfmt.Println(\"Error unmarshaling YAML:\", err)\n\t\treturn\n\t}\n\tassert.Equal(t, schema.Type, StringOrArrayOfString{\"string\"})\n\tassert.Equal(t, schema.CustomAnnotations[\"x-custom-foo\"], \"bar\")\n}\n\nfunc TestUnmarshalJSON(t *testing.T) {\n\tjsonData := `{\n\t\t\"type\": \"object\",\n\t\t\"x-custom-foo\": \"bar\",\n\t\t\"x-kubernetes-preserve-unknown-fields\": true,\n\t\t\"const\": null,\n\t\t\"properties\": {\n\t\t\t\"nested\": {\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"x-nested-annotation\": 42\n\t\t\t}\n\t\t}\n\t}`\n\n\tvar schema Schema\n\terr := json.Unmarshal([]byte(jsonData), &schema)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tassert.Equal(t, schema.Type, StringOrArrayOfString{\"object\"})\n\tassert.Equal(t, schema.CustomAnnotations[\"x-custom-foo\"], \"bar\")\n\tassert.Equal(t, schema.CustomAnnotations[\"x-kubernetes-preserve-unknown-fields\"], true)\n\tassert.Equal(t, schema.constWasSet, true)\n\n\t// Nested schema should also preserve custom annotations\n\tif schema.Properties[\"nested\"] == nil {\n\t\tt.Fatal(\"expected nested property to exist\")\n\t}\n\tassert.Equal(t, schema.Properties[\"nested\"].CustomAnnotations[\"x-nested-annotation\"], float64(42))\n}\n\nfunc TestNewDraft7Keywords(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tcomment       string\n\t\texpectedValid bool\n\t}{\n\t\t// Float numeric constraints tests\n\t\t{\n\t\t\tname: \"minimum with float value\",\n\t\t\tcomment: `\n# @schema\n# type: number\n# minimum: 1.5\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname: \"maximum with float value\",\n\t\t\tcomment: `\n# @schema\n# type: number\n# maximum: 99.9\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname: \"exclusiveMinimum with float value\",\n\t\t\tcomment: `\n# @schema\n# type: number\n# exclusiveMinimum: 0.1\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname: \"exclusiveMaximum with float value\",\n\t\t\tcomment: `\n# @schema\n# type: number\n# exclusiveMaximum: 100.5\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname: \"multipleOf with float value\",\n\t\t\tcomment: `\n# @schema\n# type: number\n# multipleOf: 0.1\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname: \"minimum greater than maximum should fail\",\n\t\t\tcomment: `\n# @schema\n# type: number\n# minimum: 10.5\n# maximum: 5.5\n# @schema`,\n\t\t\texpectedValid: false,\n\t\t},\n\t\t// $comment keyword\n\t\t{\n\t\t\tname: \"$comment keyword\",\n\t\t\tcomment: `\n# @schema\n# type: string\n# $comment: This is a schema comment for developers\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t// contentEncoding and contentMediaType\n\t\t{\n\t\t\tname: \"contentEncoding keyword\",\n\t\t\tcomment: `\n# @schema\n# type: string\n# contentEncoding: base64\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname: \"contentMediaType keyword\",\n\t\t\tcomment: `\n# @schema\n# type: string\n# contentMediaType: application/json\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname: \"contentEncoding with non-string type should fail\",\n\t\t\tcomment: `\n# @schema\n# type: integer\n# contentEncoding: base64\n# @schema`,\n\t\t\texpectedValid: false,\n\t\t},\n\t\t// contains keyword\n\t\t{\n\t\t\tname: \"contains keyword with array type\",\n\t\t\tcomment: `\n# @schema\n# type: array\n# contains:\n#   type: string\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname: \"contains with non-array type should fail\",\n\t\t\tcomment: `\n# @schema\n# type: object\n# contains:\n#   type: string\n# @schema`,\n\t\t\texpectedValid: false,\n\t\t},\n\t\t// additionalItems keyword\n\t\t{\n\t\t\tname: \"additionalItems as boolean\",\n\t\t\tcomment: `\n# @schema\n# type: array\n# additionalItems: false\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname: \"additionalItems as schema\",\n\t\t\tcomment: `\n# @schema\n# type: array\n# additionalItems:\n#   type: string\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname: \"additionalItems with non-array type should fail\",\n\t\t\tcomment: `\n# @schema\n# type: object\n# additionalItems: false\n# @schema`,\n\t\t\texpectedValid: false,\n\t\t},\n\t\t// minProperties and maxProperties\n\t\t{\n\t\t\tname: \"minProperties keyword\",\n\t\t\tcomment: `\n# @schema\n# type: object\n# minProperties: 1\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname: \"maxProperties keyword\",\n\t\t\tcomment: `\n# @schema\n# type: object\n# maxProperties: 10\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname: \"minProperties greater than maxProperties should fail\",\n\t\t\tcomment: `\n# @schema\n# type: object\n# minProperties: 10\n# maxProperties: 5\n# @schema`,\n\t\t\texpectedValid: false,\n\t\t},\n\t\t{\n\t\t\tname: \"minProperties with non-object type should fail\",\n\t\t\tcomment: `\n# @schema\n# type: string\n# minProperties: 1\n# @schema`,\n\t\t\texpectedValid: false,\n\t\t},\n\t\t// propertyNames keyword\n\t\t{\n\t\t\tname: \"propertyNames keyword\",\n\t\t\tcomment: `\n# @schema\n# type: object\n# propertyNames:\n#   pattern: ^[a-z]+$\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname: \"propertyNames with non-object type should fail\",\n\t\t\tcomment: `\n# @schema\n# type: array\n# propertyNames:\n#   pattern: ^[a-z]+$\n# @schema`,\n\t\t\texpectedValid: false,\n\t\t},\n\t\t// dependencies keyword\n\t\t{\n\t\t\tname: \"dependencies with array of property names\",\n\t\t\tcomment: `\n# @schema\n# type: object\n# dependencies:\n#   bar: [\"foo\"]\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname: \"dependencies with schema\",\n\t\t\tcomment: `\n# @schema\n# type: object\n# dependencies:\n#   bar:\n#     properties:\n#       foo:\n#         type: string\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t// definitions keyword\n\t\t{\n\t\t\tname: \"definitions keyword\",\n\t\t\tcomment: `\n# @schema\n# definitions:\n#   address:\n#     type: object\n#     properties:\n#       street:\n#         type: string\n# @schema`,\n\t\t\texpectedValid: true,\n\t\t},\n\t\t// Invalid pattern regex\n\t\t{\n\t\t\tname: \"invalid pattern regex should fail\",\n\t\t\tcomment: `\n# @schema\n# type: string\n# pattern: \"[invalid\"\n# @schema`,\n\t\t\texpectedValid: false,\n\t\t},\n\t\t// additionalProperties type check\n\t\t{\n\t\t\tname: \"additionalProperties with non-object type should fail\",\n\t\t\tcomment: `\n# @schema\n# type: string\n# additionalProperties: false\n# @schema`,\n\t\t\texpectedValid: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tschema, _, err := GetSchemaFromComment(tt.comment)\n\t\t\tif err != nil {\n\t\t\t\tif tt.expectedValid {\n\t\t\t\t\tt.Errorf(\"Expected the schema to be valid, but can't even parse it: %v\", err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\terr = schema.Validate()\n\t\t\tvalid := err == nil\n\t\t\tif valid != tt.expectedValid {\n\t\t\t\tt.Errorf(\"Expected schema to be valid=%t, but got valid=%t (error: %v)\", tt.expectedValid, valid, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFloatNumericConstraintsMarshaling(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tyamlData     string\n\t\texpectedJSON string\n\t}{\n\t\t{\n\t\t\tname:         \"minimum with float\",\n\t\t\tyamlData:     \"type: number\\nminimum: 1.5\",\n\t\t\texpectedJSON: `\"minimum\": 1.5`,\n\t\t},\n\t\t{\n\t\t\tname:         \"maximum with float\",\n\t\t\tyamlData:     \"type: number\\nmaximum: 99.9\",\n\t\t\texpectedJSON: `\"maximum\": 99.9`,\n\t\t},\n\t\t{\n\t\t\tname:         \"multipleOf with float\",\n\t\t\tyamlData:     \"type: number\\nmultipleOf: 0.01\",\n\t\t\texpectedJSON: `\"multipleOf\": 0.01`,\n\t\t},\n\t\t{\n\t\t\tname:         \"exclusiveMinimum with float\",\n\t\t\tyamlData:     \"type: number\\nexclusiveMinimum: 0.5\",\n\t\t\texpectedJSON: `\"exclusiveMinimum\": 0.5`,\n\t\t},\n\t\t{\n\t\t\tname:         \"exclusiveMaximum with float\",\n\t\t\tyamlData:     \"type: number\\nexclusiveMaximum: 100.5\",\n\t\t\texpectedJSON: `\"exclusiveMaximum\": 100.5`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar schema Schema\n\t\t\tif err := yaml.Unmarshal([]byte(tt.yamlData), &schema); err != nil {\n\t\t\t\tt.Fatalf(\"Error unmarshaling YAML: %v\", err)\n\t\t\t}\n\n\t\t\tjsonData, err := schema.ToJson()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Error marshaling to JSON: %v\", err)\n\t\t\t}\n\n\t\t\tjsonStr := string(jsonData)\n\t\t\tif !strings.Contains(jsonStr, tt.expectedJSON) {\n\t\t\t\tt.Errorf(\"Expected JSON to contain %q, but got:\\n%s\", tt.expectedJSON, jsonStr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewKeywordsMarshaling(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tyamlData     string\n\t\texpectedJSON string\n\t}{\n\t\t{\n\t\t\tname:         \"$comment keyword\",\n\t\t\tyamlData:     \"$comment: Test comment\",\n\t\t\texpectedJSON: `\"$comment\": \"Test comment\"`,\n\t\t},\n\t\t{\n\t\t\tname:         \"contentEncoding keyword\",\n\t\t\tyamlData:     \"contentEncoding: base64\",\n\t\t\texpectedJSON: `\"contentEncoding\": \"base64\"`,\n\t\t},\n\t\t{\n\t\t\tname:         \"contentMediaType keyword\",\n\t\t\tyamlData:     \"contentMediaType: application/json\",\n\t\t\texpectedJSON: `\"contentMediaType\": \"application/json\"`,\n\t\t},\n\t\t{\n\t\t\tname:         \"minProperties keyword\",\n\t\t\tyamlData:     \"minProperties: 2\",\n\t\t\texpectedJSON: `\"minProperties\": 2`,\n\t\t},\n\t\t{\n\t\t\tname:         \"maxProperties keyword\",\n\t\t\tyamlData:     \"maxProperties: 10\",\n\t\t\texpectedJSON: `\"maxProperties\": 10`,\n\t\t},\n\t\t{\n\t\t\tname:         \"contains keyword\",\n\t\t\tyamlData:     \"type: array\\ncontains:\\n  type: string\",\n\t\t\texpectedJSON: `\"contains\"`,\n\t\t},\n\t\t{\n\t\t\tname:         \"propertyNames keyword\",\n\t\t\tyamlData:     \"type: object\\npropertyNames:\\n  pattern: ^[a-z]+$\",\n\t\t\texpectedJSON: `\"propertyNames\"`,\n\t\t},\n\t\t{\n\t\t\tname:         \"additionalItems as boolean\",\n\t\t\tyamlData:     \"type: array\\nadditionalItems: false\",\n\t\t\texpectedJSON: `\"additionalItems\": false`,\n\t\t},\n\t\t{\n\t\t\tname:         \"definitions keyword\",\n\t\t\tyamlData:     \"definitions:\\n  myDef:\\n    type: string\",\n\t\t\texpectedJSON: `\"definitions\"`,\n\t\t},\n\t\t{\n\t\t\tname:         \"dependencies keyword\",\n\t\t\tyamlData:     \"dependencies:\\n  bar: [\\\"foo\\\"]\",\n\t\t\texpectedJSON: `\"dependencies\"`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar schema Schema\n\t\t\tif err := yaml.Unmarshal([]byte(tt.yamlData), &schema); err != nil {\n\t\t\t\tt.Fatalf(\"Error unmarshaling YAML: %v\", err)\n\t\t\t}\n\n\t\t\tjsonData, err := schema.ToJson()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Error marshaling to JSON: %v\", err)\n\t\t\t}\n\n\t\t\tjsonStr := string(jsonData)\n\t\t\tif !strings.Contains(jsonStr, tt.expectedJSON) {\n\t\t\t\tt.Errorf(\"Expected JSON to contain %q, but got:\\n%s\", tt.expectedJSON, jsonStr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDisableRequiredPropertiesWithNewFields(t *testing.T) {\n\t// Test that DisableRequiredProperties works with all new nested schema fields\n\tschema := Schema{\n\t\tType: StringOrArrayOfString{\"object\"},\n\t\tRequired: BoolOrArrayOfString{\n\t\t\tStrings: []string{\"foo\"},\n\t\t},\n\t\tContains: &Schema{\n\t\t\tRequired: BoolOrArrayOfString{Strings: []string{\"inner\"}},\n\t\t},\n\t\tPropertyNames: &Schema{\n\t\t\tRequired: BoolOrArrayOfString{Strings: []string{\"name\"}},\n\t\t},\n\t\tDefinitions: map[string]*Schema{\n\t\t\t\"myDef\": {\n\t\t\t\tRequired: BoolOrArrayOfString{Strings: []string{\"defProp\"}},\n\t\t\t},\n\t\t},\n\t}\n\n\tschema.DisableRequiredProperties()\n\n\tif len(schema.Required.Strings) != 0 {\n\t\tt.Error(\"Expected root required to be empty\")\n\t}\n\tif len(schema.Contains.Required.Strings) != 0 {\n\t\tt.Error(\"Expected Contains required to be empty\")\n\t}\n\tif len(schema.PropertyNames.Required.Strings) != 0 {\n\t\tt.Error(\"Expected PropertyNames required to be empty\")\n\t}\n\tif len(schema.Definitions[\"myDef\"].Required.Strings) != 0 {\n\t\tt.Error(\"Expected Definitions[myDef] required to be empty\")\n\t}\n}\n\nfunc TestDefsToDefinitionsConversion(t *testing.T) {\n\t// Test that $defs is converted to definitions when unmarshaling JSON\n\ttests := []struct {\n\t\tname         string\n\t\tjsonInput    string\n\t\texpectDefs   bool\n\t\texpectedKeys []string\n\t}{\n\t\t{\n\t\t\tname: \"$defs is converted to definitions\",\n\t\t\tjsonInput: `{\n\t\t\t\t\"$defs\": {\n\t\t\t\t\t\"MyType\": {\"type\": \"string\"}\n\t\t\t\t}\n\t\t\t}`,\n\t\t\texpectDefs:   true,\n\t\t\texpectedKeys: []string{\"MyType\"},\n\t\t},\n\t\t{\n\t\t\tname: \"definitions is preserved\",\n\t\t\tjsonInput: `{\n\t\t\t\t\"definitions\": {\n\t\t\t\t\t\"OtherType\": {\"type\": \"integer\"}\n\t\t\t\t}\n\t\t\t}`,\n\t\t\texpectDefs:   true,\n\t\t\texpectedKeys: []string{\"OtherType\"},\n\t\t},\n\t\t{\n\t\t\tname: \"$defs and definitions are merged\",\n\t\t\tjsonInput: `{\n\t\t\t\t\"$defs\": {\n\t\t\t\t\t\"FromDefs\": {\"type\": \"string\"}\n\t\t\t\t},\n\t\t\t\t\"definitions\": {\n\t\t\t\t\t\"FromDefinitions\": {\"type\": \"integer\"}\n\t\t\t\t}\n\t\t\t}`,\n\t\t\texpectDefs:   true,\n\t\t\texpectedKeys: []string{\"FromDefs\", \"FromDefinitions\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar schema Schema\n\t\t\tif err := json.Unmarshal([]byte(tt.jsonInput), &schema); err != nil {\n\t\t\t\tt.Fatalf(\"Failed to unmarshal JSON: %v\", err)\n\t\t\t}\n\n\t\t\tif tt.expectDefs && schema.Definitions == nil {\n\t\t\t\tt.Error(\"Expected Definitions to be non-nil\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfor _, key := range tt.expectedKeys {\n\t\t\t\tif _, ok := schema.Definitions[key]; !ok {\n\t\t\t\t\tt.Errorf(\"Expected key %q in Definitions\", key)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRefPathRewriting(t *testing.T) {\n\t// Test that $ref paths are rewritten from #/$defs/ to #/definitions/\n\ttests := []struct {\n\t\tname        string\n\t\tjsonInput   string\n\t\texpectedRef string\n\t}{\n\t\t{\n\t\t\tname: \"$ref with $defs path is rewritten\",\n\t\t\tjsonInput: `{\n\t\t\t\t\"$ref\": \"#/$defs/MyType\"\n\t\t\t}`,\n\t\t\texpectedRef: \"#/definitions/MyType\",\n\t\t},\n\t\t{\n\t\t\tname: \"$ref with definitions path is preserved\",\n\t\t\tjsonInput: `{\n\t\t\t\t\"$ref\": \"#/definitions/MyType\"\n\t\t\t}`,\n\t\t\texpectedRef: \"#/definitions/MyType\",\n\t\t},\n\t\t{\n\t\t\tname: \"nested $ref paths are rewritten\",\n\t\t\tjsonInput: `{\n\t\t\t\t\"properties\": {\n\t\t\t\t\t\"foo\": {\n\t\t\t\t\t\t\"$ref\": \"#/$defs/FooType\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}`,\n\t\t\texpectedRef: \"#/definitions/FooType\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar schema Schema\n\t\t\tif err := json.Unmarshal([]byte(tt.jsonInput), &schema); err != nil {\n\t\t\t\tt.Fatalf(\"Failed to unmarshal JSON: %v\", err)\n\t\t\t}\n\n\t\t\t// Check main ref\n\t\t\tif schema.Ref != \"\" && schema.Ref != tt.expectedRef {\n\t\t\t\tt.Errorf(\"Expected Ref to be %q, got %q\", tt.expectedRef, schema.Ref)\n\t\t\t}\n\n\t\t\t// Check nested ref in properties\n\t\t\tif schema.Properties != nil {\n\t\t\t\tfor _, prop := range schema.Properties {\n\t\t\t\t\tif prop.Ref != \"\" && prop.Ref != tt.expectedRef {\n\t\t\t\t\t\tt.Errorf(\"Expected nested Ref to be %q, got %q\", tt.expectedRef, prop.Ref)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestConstNullMarshaling(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tyamlData      string\n\t\texpectedJSON  string\n\t\tshouldContain bool\n\t}{\n\t\t{\n\t\t\tname:          \"const with null value should be preserved\",\n\t\t\tyamlData:      \"const: null\",\n\t\t\texpectedJSON:  `\"const\": null`,\n\t\t\tshouldContain: true,\n\t\t},\n\t\t{\n\t\t\tname:          \"const with false value should be preserved\",\n\t\t\tyamlData:      \"const: false\",\n\t\t\texpectedJSON:  `\"const\": false`,\n\t\t\tshouldContain: true,\n\t\t},\n\t\t{\n\t\t\tname:          \"const with true value should be preserved\",\n\t\t\tyamlData:      \"const: true\",\n\t\t\texpectedJSON:  `\"const\": true`,\n\t\t\tshouldContain: true,\n\t\t},\n\t\t{\n\t\t\tname:          \"const with string value should be preserved\",\n\t\t\tyamlData:      `const: \"test\"`,\n\t\t\texpectedJSON:  `\"const\": \"test\"`,\n\t\t\tshouldContain: true,\n\t\t},\n\t\t{\n\t\t\tname:          \"schema without const should not have const field\",\n\t\t\tyamlData:      \"type: string\",\n\t\t\texpectedJSON:  `\"const\"`,\n\t\t\tshouldContain: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar schema Schema\n\t\t\tif err := yaml.Unmarshal([]byte(tt.yamlData), &schema); err != nil {\n\t\t\t\tt.Fatalf(\"Error unmarshaling YAML: %v\", err)\n\t\t\t}\n\n\t\t\tjsonData, err := schema.ToJson()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Error marshaling to JSON: %v\", err)\n\t\t\t}\n\n\t\t\tjsonStr := string(jsonData)\n\t\t\tcontains := strings.Contains(jsonStr, tt.expectedJSON)\n\n\t\t\tif tt.shouldContain && !contains {\n\t\t\t\tt.Errorf(\"Expected JSON to contain %q, but got:\\n%s\", tt.expectedJSON, jsonStr)\n\t\t\t}\n\t\t\tif !tt.shouldContain && contains {\n\t\t\t\tt.Errorf(\"Expected JSON to NOT contain %q, but got:\\n%s\", tt.expectedJSON, jsonStr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestYamlToSchemaConstFromValue(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tyamlContent   string\n\t\texpectedConst interface{}\n\t\texpectedErr   string\n\t}{\n\t\t{\n\t\t\tname: \"scalar string value\",\n\t\t\tyamlContent: `# @schema\n# const-from-value: true\n# @schema\nmessage: |\n  long message with {{ .gotemplate }}`,\n\t\t\texpectedConst: \"long message with {{ .gotemplate }}\",\n\t\t},\n\t\t{\n\t\t\tname: \"null value\",\n\t\t\tyamlContent: `# @schema\n# const-from-value: true\n# @schema\nmessage: null`,\n\t\t\texpectedConst: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"mapping value\",\n\t\t\tyamlContent: `# @schema\n# const-from-value: true\n# @schema\nmessage:\n  enabled: true\n  retries: 2`,\n\t\t\texpectedConst: map[string]interface{}{\n\t\t\t\t\"enabled\": true,\n\t\t\t\t\"retries\": 2,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"conflicts with explicit const\",\n\t\t\tyamlContent: `# @schema\n# const: fixed\n# const-from-value: true\n# @schema\nmessage: fixed`,\n\t\t\texpectedErr: \"const and const-from-value cannot be used together\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar node yaml.Node\n\t\t\tif err := yaml.Unmarshal([]byte(tt.yamlContent), &node); err != nil {\n\t\t\t\tt.Fatalf(\"Failed to unmarshal YAML: %v\", err)\n\t\t\t}\n\n\t\t\tskipConfig := &SkipAutoGenerationConfig{}\n\t\t\tschema, err := YamlToSchema(\"\", &node, false, false, false, true, skipConfig, nil)\n\t\t\tif tt.expectedErr != \"\" {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Fatalf(\"Expected error containing %q, got nil\", tt.expectedErr)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(err.Error(), tt.expectedErr) {\n\t\t\t\t\tt.Fatalf(\"Expected error containing %q, got %v\", tt.expectedErr, err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"YamlToSchema failed: %v\", err)\n\t\t\t}\n\n\t\t\tproperty, ok := schema.Properties[\"message\"]\n\t\t\tif !ok {\n\t\t\t\tt.Fatal(\"Expected schema to contain message property\")\n\t\t\t}\n\n\t\t\tif !reflect.DeepEqual(property.Const, tt.expectedConst) {\n\t\t\t\tt.Fatalf(\"Expected const %#v, got %#v\", tt.expectedConst, property.Const)\n\t\t\t}\n\n\t\t\tjsonData, err := property.ToJson()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Failed to marshal property schema to JSON: %v\", err)\n\t\t\t}\n\n\t\t\tif !strings.Contains(string(jsonData), `\"const\"`) {\n\t\t\t\tt.Fatalf(\"Expected JSON to contain const, got %s\", string(jsonData))\n\t\t\t}\n\t\t\tif strings.Contains(string(jsonData), \"const-from-value\") {\n\t\t\t\tt.Fatalf(\"Did not expect JSON to contain const-from-value, got %s\", string(jsonData))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestYamlToSchemaPreservesDocumentLocalRootRef(t *testing.T) {\n\tyamlContent := `# @schema\n# definitions:\n#   toplevel:\n#     description: \"Top Level\"\n# $ref: \"#/definitions/toplevel\"\n# @schema\ntoplevel:\n`\n\n\tvar node yaml.Node\n\tif err := yaml.Unmarshal([]byte(yamlContent), &node); err != nil {\n\t\tt.Fatalf(\"Failed to unmarshal YAML: %v\", err)\n\t}\n\n\tskipConfig := &SkipAutoGenerationConfig{}\n\tschema, err := YamlToSchema(\"/tmp/values.yaml\", &node, false, false, false, true, skipConfig, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"YamlToSchema failed: %v\", err)\n\t}\n\n\tproperty, ok := schema.Properties[\"toplevel\"]\n\tif !ok {\n\t\tt.Fatal(\"Expected schema to contain toplevel property\")\n\t}\n\n\tif property.Ref != \"#/definitions/toplevel\" {\n\t\tt.Fatalf(\"Expected ref to be preserved, got %q\", property.Ref)\n\t}\n\n\tschema.HoistDefinitions()\n\n\tif schema.Definitions == nil {\n\t\tt.Fatal(\"Expected root definitions to be preserved\")\n\t}\n\n\tdefinition, ok := schema.Definitions[\"toplevel\"]\n\tif !ok {\n\t\tt.Fatal(\"Expected toplevel definition to be present\")\n\t}\n\n\tif definition.Description != \"Top Level\" {\n\t\tt.Fatalf(\"Expected toplevel definition description %q, got %q\", \"Top Level\", definition.Description)\n\t}\n}\n\nfunc TestGetPropertyAtPath(t *testing.T) {\n\t// Create a nested schema structure\n\tschema := &Schema{\n\t\tType: StringOrArrayOfString{\"object\"},\n\t\tProperties: map[string]*Schema{\n\t\t\t\"exports\": {\n\t\t\t\tType: StringOrArrayOfString{\"object\"},\n\t\t\t\tProperties: map[string]*Schema{\n\t\t\t\t\t\"defaults\": {\n\t\t\t\t\t\tType: StringOrArrayOfString{\"object\"},\n\t\t\t\t\t\tProperties: map[string]*Schema{\n\t\t\t\t\t\t\t\"foo\": {\n\t\t\t\t\t\t\t\tType:  StringOrArrayOfString{\"string\"},\n\t\t\t\t\t\t\t\tTitle: \"Foo\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"bar\": {\n\t\t\t\t\t\t\t\tType:  StringOrArrayOfString{\"integer\"},\n\t\t\t\t\t\t\t\tTitle: \"Bar\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"config\": {\n\t\t\t\tType:  StringOrArrayOfString{\"object\"},\n\t\t\t\tTitle: \"Config\",\n\t\t\t},\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname          string\n\t\tpath          string\n\t\texpectedTitle string\n\t\texpectNil     bool\n\t}{\n\t\t{\n\t\t\tname:          \"empty path returns self\",\n\t\t\tpath:          \"\",\n\t\t\texpectedTitle: \"\",\n\t\t\texpectNil:     false,\n\t\t},\n\t\t{\n\t\t\tname:          \"single level path\",\n\t\t\tpath:          \"config\",\n\t\t\texpectedTitle: \"Config\",\n\t\t\texpectNil:     false,\n\t\t},\n\t\t{\n\t\t\tname:          \"nested path\",\n\t\t\tpath:          \"exports.defaults.foo\",\n\t\t\texpectedTitle: \"Foo\",\n\t\t\texpectNil:     false,\n\t\t},\n\t\t{\n\t\t\tname:      \"non-existent path\",\n\t\t\tpath:      \"exports.nonexistent\",\n\t\t\texpectNil: true,\n\t\t},\n\t\t{\n\t\t\tname:      \"path through non-object\",\n\t\t\tpath:      \"config.deeper\",\n\t\t\texpectNil: true,\n\t\t},\n\t\t{\n\t\t\tname:          \"path with leading dot\",\n\t\t\tpath:          \".config\",\n\t\t\texpectedTitle: \"Config\",\n\t\t\texpectNil:     false,\n\t\t},\n\t\t{\n\t\t\tname:          \"path with trailing dot\",\n\t\t\tpath:          \"config.\",\n\t\t\texpectedTitle: \"Config\",\n\t\t\texpectNil:     false,\n\t\t},\n\t\t{\n\t\t\tname:          \"path with consecutive dots\",\n\t\t\tpath:          \"exports..defaults.foo\",\n\t\t\texpectedTitle: \"Foo\",\n\t\t\texpectNil:     false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := schema.GetPropertyAtPath(tt.path)\n\n\t\t\tif tt.expectNil {\n\t\t\t\tif result != nil {\n\t\t\t\t\tt.Errorf(\"Expected nil result, got %+v\", result)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif result == nil {\n\t\t\t\tt.Errorf(\"Expected non-nil result\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif tt.path == \"\" {\n\t\t\t\tif result != schema {\n\t\t\t\t\tt.Errorf(\"Empty path should return self\")\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif result.Title != tt.expectedTitle {\n\t\t\t\tt.Errorf(\"Expected Title %q, got %q\", tt.expectedTitle, result.Title)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetPropertyAtPath(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tpath         string\n\t\texpectedPath []string\n\t}{\n\t\t{\n\t\t\tname:         \"empty path returns self\",\n\t\t\tpath:         \"\",\n\t\t\texpectedPath: []string{},\n\t\t},\n\t\t{\n\t\t\tname:         \"single level path\",\n\t\t\tpath:         \"config\",\n\t\t\texpectedPath: []string{\"config\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"nested path\",\n\t\t\tpath:         \"exports.defaults.settings\",\n\t\t\texpectedPath: []string{\"exports\", \"defaults\", \"settings\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"path with leading dot\",\n\t\t\tpath:         \".config\",\n\t\t\texpectedPath: []string{\"config\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"path with trailing dot\",\n\t\t\tpath:         \"config.\",\n\t\t\texpectedPath: []string{\"config\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"path with consecutive dots\",\n\t\t\tpath:         \"exports..defaults\",\n\t\t\texpectedPath: []string{\"exports\", \"defaults\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tschema := &Schema{\n\t\t\t\tType:       StringOrArrayOfString{\"object\"},\n\t\t\t\tProperties: make(map[string]*Schema),\n\t\t\t}\n\n\t\t\tresult := schema.SetPropertyAtPath(tt.path)\n\n\t\t\tif tt.path == \"\" {\n\t\t\t\tif result != schema {\n\t\t\t\t\tt.Errorf(\"Empty path should return self\")\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Verify the path was created\n\t\t\tcurrent := schema\n\t\t\tfor _, part := range tt.expectedPath {\n\t\t\t\tif current.Properties == nil {\n\t\t\t\t\tt.Errorf(\"Expected Properties to be initialized at %s\", part)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tprop, ok := current.Properties[part]\n\t\t\t\tif !ok {\n\t\t\t\t\tt.Errorf(\"Expected property %s to exist\", part)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tcurrent = prop\n\t\t\t}\n\n\t\t\tif current != result {\n\t\t\t\tt.Errorf(\"Final property should match returned schema\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHoistDefinitions(t *testing.T) {\n\t// Create a schema with nested definitions\n\trestConfig := &Schema{\n\t\tType:        StringOrArrayOfString{\"object\"},\n\t\tTitle:       \"RestConfig\",\n\t\tDescription: \"REST API configuration\",\n\t\tProperties: map[string]*Schema{\n\t\t\t\"url\": {\n\t\t\t\tType:   StringOrArrayOfString{\"string\"},\n\t\t\t\tFormat: \"uri\",\n\t\t\t},\n\t\t},\n\t}\n\n\tworkerSchema := &Schema{\n\t\tType:        StringOrArrayOfString{\"object\"},\n\t\tTitle:       \"Worker\",\n\t\tDescription: \"Worker configuration\",\n\t\tDefinitions: map[string]*Schema{\n\t\t\t\"RestConfig\": restConfig,\n\t\t},\n\t\tProperties: map[string]*Schema{\n\t\t\t\"api\": {\n\t\t\t\tRef: \"#/definitions/RestConfig\",\n\t\t\t},\n\t\t},\n\t}\n\n\trootSchema := &Schema{\n\t\tSchema: \"http://json-schema.org/draft-07/schema#\",\n\t\tType:   StringOrArrayOfString{\"object\"},\n\t\tProperties: map[string]*Schema{\n\t\t\t\"worker\": workerSchema,\n\t\t},\n\t}\n\n\t// Verify definitions are nested before hoisting\n\tif rootSchema.Definitions != nil && len(rootSchema.Definitions) > 0 {\n\t\tt.Error(\"Root should not have definitions before hoisting\")\n\t}\n\tif workerSchema.Definitions == nil || len(workerSchema.Definitions) == 0 {\n\t\tt.Error(\"Worker should have definitions before hoisting\")\n\t}\n\n\t// Hoist definitions\n\trootSchema.HoistDefinitions()\n\n\t// Verify definitions are at root after hoisting\n\tif rootSchema.Definitions == nil || len(rootSchema.Definitions) == 0 {\n\t\tt.Error(\"Root should have definitions after hoisting\")\n\t}\n\tif _, ok := rootSchema.Definitions[\"RestConfig\"]; !ok {\n\t\tt.Error(\"Root should have RestConfig definition after hoisting\")\n\t}\n\n\t// Verify definitions are removed from nested schema\n\tif workerSchema.Definitions != nil && len(workerSchema.Definitions) > 0 {\n\t\tt.Error(\"Worker should not have definitions after hoisting\")\n\t}\n\n\t// Verify the $ref still points to the correct location\n\tif rootSchema.Properties[\"worker\"].Properties[\"api\"].Ref != \"#/definitions/RestConfig\" {\n\t\tt.Error(\"$ref should still point to #/definitions/RestConfig\")\n\t}\n\n\t// Verify the hoisted definition is correct\n\tif rootSchema.Definitions[\"RestConfig\"].Title != \"RestConfig\" {\n\t\tt.Errorf(\"Hoisted definition should have correct title, got %s\", rootSchema.Definitions[\"RestConfig\"].Title)\n\t}\n}\n"
  },
  {
    "path": "pkg/schema/toposort.go",
    "content": "package schema\n\nimport (\n\t\"fmt\"\n)\n\n// TopoSort uses topological sorting to sort the results\n// If allowCircular is true, circular dependencies will be logged as warnings and results will be returned unsorted\nfunc TopoSort(results []*Result, allowCircular bool) ([]*Result, error) {\n\t// Map chart names to their Result objects for easy lookup\n\tchartMap := make(map[string]*Result)\n\tfor _, r := range results {\n\t\tif r.Chart != nil {\n\t\t\tchartMap[r.Chart.Name] = r\n\t\t}\n\t}\n\n\t// Build dependency graph as adjacency list\n\tdeps := make(map[string][]string)\n\n\t// Build dependency graph\n\tfor _, r := range results {\n\t\tif r.Chart == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Initialize empty dependency list\n\t\tdeps[r.Chart.Name] = []string{}\n\n\t\t// Add all dependencies\n\t\tfor _, dep := range r.Chart.Dependencies {\n\t\t\tdeps[r.Chart.Name] = append(deps[r.Chart.Name], dep.Name)\n\t\t}\n\t}\n\n\t// Track visited nodes during traversal\n\tvisited := make(map[string]bool)\n\t// Track nodes in current recursion stack to detect cycles\n\tinStack := make(map[string]bool)\n\t// Final sorted results\n\tvar sorted []*Result\n\n\t// Recursive DFS helper function\n\tvar visit func(string) error\n\tvisit = func(chart string) error {\n\t\t// Check for cycle first, before the visited check\n\t\tif inStack[chart] {\n\t\t\treturn &CircularError{fmt.Sprintf(\"circular dependency detected: %s\", chart)}\n\t\t}\n\n\t\t// Return if already visited\n\t\tif visited[chart] {\n\t\t\treturn nil\n\t\t}\n\n\t\t// Mark as being visited\n\t\tinStack[chart] = true\n\t\tvisited[chart] = true\n\n\t\t// Visit all dependencies first\n\t\tfor _, dep := range deps[chart] {\n\t\t\tif err := visit(dep); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\t// Add to sorted results after dependencies\n\t\tif result, exists := chartMap[chart]; exists {\n\t\t\tsorted = append(sorted, result)\n\t\t}\n\n\t\t// Remove from recursion stack\n\t\tinStack[chart] = false\n\t\treturn nil\n\t}\n\n\t// Visit all charts\n\tfor _, r := range results {\n\t\tif r.Chart != nil {\n\t\t\tif err := visit(r.Chart.Name); err != nil {\n\t\t\t\tif allowCircular {\n\t\t\t\t\t// Return unsorted results when circular dependencies are allowed\n\t\t\t\t\treturn results, nil\n\t\t\t\t}\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn sorted, nil\n}\n"
  },
  {
    "path": "pkg/schema/toposort_test.go",
    "content": "package schema\n\nimport (\n\t\"testing\"\n\n\t\"github.com/dadav/helm-schema/pkg/chart\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTopoSort(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tresults       []*Result\n\t\tallowCircular bool\n\t\twant          []string // expected order of chart names\n\t\twantErr       bool\n\t\terrorType     error\n\t}{\n\t\t{\n\t\t\tname: \"simple dependency chain\",\n\t\t\tresults: []*Result{\n\t\t\t\t{Chart: &chart.ChartFile{Name: \"A\", Dependencies: []*chart.Dependency{{Name: \"B\"}}}},\n\t\t\t\t{Chart: &chart.ChartFile{Name: \"B\", Dependencies: []*chart.Dependency{{Name: \"C\"}}}},\n\t\t\t\t{Chart: &chart.ChartFile{Name: \"C\", Dependencies: []*chart.Dependency{}}},\n\t\t\t},\n\t\t\tallowCircular: false,\n\t\t\twant:          []string{\"C\", \"B\", \"A\"},\n\t\t\twantErr:       false,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple dependencies\",\n\t\t\tresults: []*Result{\n\t\t\t\t{Chart: &chart.ChartFile{Name: \"A\", Dependencies: []*chart.Dependency{{Name: \"B\"}, {Name: \"C\"}}}},\n\t\t\t\t{Chart: &chart.ChartFile{Name: \"B\", Dependencies: []*chart.Dependency{{Name: \"D\"}}}},\n\t\t\t\t{Chart: &chart.ChartFile{Name: \"C\", Dependencies: []*chart.Dependency{{Name: \"D\"}}}},\n\t\t\t\t{Chart: &chart.ChartFile{Name: \"D\", Dependencies: []*chart.Dependency{}}},\n\t\t\t},\n\t\t\tallowCircular: false,\n\t\t\twant:          []string{\"D\", \"B\", \"C\", \"A\"},\n\t\t\twantErr:       false,\n\t\t},\n\t\t{\n\t\t\tname: \"circular dependency\",\n\t\t\tresults: []*Result{\n\t\t\t\t{Chart: &chart.ChartFile{Name: \"A\", Dependencies: []*chart.Dependency{{Name: \"B\"}}}},\n\t\t\t\t{Chart: &chart.ChartFile{Name: \"B\", Dependencies: []*chart.Dependency{{Name: \"A\"}}}},\n\t\t\t},\n\t\t\tallowCircular: false,\n\t\t\twant:          nil,\n\t\t\twantErr:       true,\n\t\t\terrorType:     &CircularError{},\n\t\t},\n\t\t{\n\t\t\tname: \"nil chart in results\",\n\t\t\tresults: []*Result{\n\t\t\t\t{Chart: &chart.ChartFile{Name: \"A\", Dependencies: []*chart.Dependency{{Name: \"B\"}}}},\n\t\t\t\t{Chart: nil},\n\t\t\t\t{Chart: &chart.ChartFile{Name: \"B\", Dependencies: []*chart.Dependency{}}},\n\t\t\t},\n\t\t\tallowCircular: false,\n\t\t\twant:          []string{\"B\", \"A\"},\n\t\t\twantErr:       false,\n\t\t},\n\t\t{\n\t\t\tname: \"circular dependency allowed\",\n\t\t\tresults: []*Result{\n\t\t\t\t{Chart: &chart.ChartFile{Name: \"A\", Dependencies: []*chart.Dependency{{Name: \"B\"}}}},\n\t\t\t\t{Chart: &chart.ChartFile{Name: \"B\", Dependencies: []*chart.Dependency{{Name: \"A\"}}}},\n\t\t\t},\n\t\t\tallowCircular: true,\n\t\t\twant:          []string{\"A\", \"B\"},\n\t\t\twantErr:       false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := TopoSort(tt.results, tt.allowCircular)\n\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\tif tt.errorType != nil {\n\t\t\t\t\tassert.IsType(t, tt.errorType, err)\n\t\t\t\t}\n\n\t\t\t\t// When allowCircular is true and we get a CircularError,\n\t\t\t\t// we should still get unsorted results back\n\t\t\t\tif tt.allowCircular && tt.want != nil {\n\t\t\t\t\t// Convert results to slice of chart names for easier comparison\n\t\t\t\t\tvar gotNames []string\n\t\t\t\t\tfor _, r := range got {\n\t\t\t\t\t\tgotNames = append(gotNames, r.Chart.Name)\n\t\t\t\t\t}\n\t\t\t\t\tassert.Equal(t, tt.want, gotNames)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\n\t\t\t// Convert results to slice of chart names for easier comparison\n\t\t\tvar gotNames []string\n\t\t\tfor _, r := range got {\n\t\t\t\tgotNames = append(gotNames, r.Chart.Name)\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.want, gotNames)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/schema/values_merge.go",
    "content": "package schema\n\nimport (\n\t\"fmt\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\n// mergeValuesDocuments merges YAML documents using Helm-style precedence:\n// later files override earlier files, and nested mappings merge recursively.\nfunc mergeValuesDocuments(base *yaml.Node, overlay *yaml.Node) (*yaml.Node, error) {\n\tif base == nil {\n\t\treturn cloneYAMLNode(overlay), nil\n\t}\n\tif overlay == nil {\n\t\treturn cloneYAMLNode(base), nil\n\t}\n\tif base.Kind != yaml.DocumentNode || overlay.Kind != yaml.DocumentNode {\n\t\treturn nil, fmt.Errorf(\"expected yaml document nodes, got %d and %d\", base.Kind, overlay.Kind)\n\t}\n\tif len(base.Content) != 1 || len(overlay.Content) != 1 {\n\t\treturn nil, fmt.Errorf(\"unexpected yaml document structure while merging values\")\n\t}\n\n\tmerged := cloneYAMLNode(base)\n\tmerged.HeadComment = mergeCommentText(merged.HeadComment, overlay.HeadComment)\n\tmerged.LineComment = mergeCommentText(merged.LineComment, overlay.LineComment)\n\tmerged.FootComment = mergeCommentText(merged.FootComment, overlay.FootComment)\n\n\tmergedContent, err := mergeValuesNodes(merged.Content[0], overlay.Content[0])\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tmerged.Content[0] = mergedContent\n\n\treturn merged, nil\n}\n\nfunc mergeValuesNodes(base *yaml.Node, overlay *yaml.Node) (*yaml.Node, error) {\n\tif base == nil {\n\t\treturn cloneYAMLNode(overlay), nil\n\t}\n\tif overlay == nil {\n\t\treturn cloneYAMLNode(base), nil\n\t}\n\n\tif base.Kind == yaml.AliasNode && base.Alias != nil {\n\t\tbase = base.Alias\n\t}\n\tif overlay.Kind == yaml.AliasNode && overlay.Alias != nil {\n\t\toverlay = overlay.Alias\n\t}\n\n\tif base.Kind == yaml.MappingNode && overlay.Kind == yaml.MappingNode {\n\t\treturn mergeMappingNodes(base, overlay)\n\t}\n\n\treplacement := cloneYAMLNode(overlay)\n\treplacement.HeadComment = mergeCommentText(base.HeadComment, overlay.HeadComment)\n\treplacement.LineComment = mergeCommentText(base.LineComment, overlay.LineComment)\n\treplacement.FootComment = mergeCommentText(base.FootComment, overlay.FootComment)\n\treturn replacement, nil\n}\n\nfunc mergeMappingNodes(base *yaml.Node, overlay *yaml.Node) (*yaml.Node, error) {\n\tmerged := cloneYAMLNode(base)\n\tmerged.Content = nil\n\tmerged.HeadComment = mergeCommentText(base.HeadComment, overlay.HeadComment)\n\tmerged.LineComment = mergeCommentText(base.LineComment, overlay.LineComment)\n\tmerged.FootComment = mergeCommentText(base.FootComment, overlay.FootComment)\n\n\toverlayIndex := make(map[string]int, len(overlay.Content)/2)\n\tfor i := 0; i+1 < len(overlay.Content); i += 2 {\n\t\toverlayIndex[overlay.Content[i].Value] = i\n\t}\n\n\tusedOverlayKeys := make(map[string]bool, len(overlayIndex))\n\n\tfor i := 0; i+1 < len(base.Content); i += 2 {\n\t\tbaseKey := base.Content[i]\n\t\tbaseValue := base.Content[i+1]\n\t\toverlayPos, exists := overlayIndex[baseKey.Value]\n\t\tif !exists {\n\t\t\tmerged.Content = append(merged.Content, cloneYAMLNode(baseKey), cloneYAMLNode(baseValue))\n\t\t\tcontinue\n\t\t}\n\n\t\toverlayKey := overlay.Content[overlayPos]\n\t\toverlayValue := overlay.Content[overlayPos+1]\n\t\tusedOverlayKeys[baseKey.Value] = true\n\n\t\tmergedKey := cloneYAMLNode(baseKey)\n\t\tmergedKey.HeadComment = mergeCommentText(baseKey.HeadComment, overlayKey.HeadComment)\n\t\tmergedKey.LineComment = mergeCommentText(baseKey.LineComment, overlayKey.LineComment)\n\t\tmergedKey.FootComment = mergeCommentText(baseKey.FootComment, overlayKey.FootComment)\n\n\t\tmergedValue, err := mergeValuesNodes(baseValue, overlayValue)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tmerged.Content = append(merged.Content, mergedKey, mergedValue)\n\t}\n\n\tfor i := 0; i+1 < len(overlay.Content); i += 2 {\n\t\toverlayKey := overlay.Content[i]\n\t\tif usedOverlayKeys[overlayKey.Value] {\n\t\t\tcontinue\n\t\t}\n\t\tmerged.Content = append(merged.Content, cloneYAMLNode(overlayKey), cloneYAMLNode(overlay.Content[i+1]))\n\t}\n\n\treturn merged, nil\n}\n\nfunc cloneYAMLNode(node *yaml.Node) *yaml.Node {\n\tif node == nil {\n\t\treturn nil\n\t}\n\n\tcloned := *node\n\tif node.Content != nil {\n\t\tcloned.Content = make([]*yaml.Node, len(node.Content))\n\t\tfor i, child := range node.Content {\n\t\t\tcloned.Content[i] = cloneYAMLNode(child)\n\t\t}\n\t}\n\tif node.Alias != nil {\n\t\tcloned.Alias = cloneYAMLNode(node.Alias)\n\t}\n\n\treturn &cloned\n}\n\nfunc mergeCommentText(base string, overlay string) string {\n\tif overlay != \"\" {\n\t\treturn overlay\n\t}\n\treturn base\n}\n"
  },
  {
    "path": "pkg/schema/worker.go",
    "content": "package schema\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/dadav/helm-schema/pkg/chart\"\n\t\"github.com/dadav/helm-schema/pkg/util\"\n\t\"gopkg.in/yaml.v3\"\n)\n\ntype Result struct {\n\tChartPath         string\n\tValuesPath        string\n\tChart             *chart.ChartFile\n\tSchema            Schema\n\tErrors            []error\n\tPreExistingSchema bool\n}\n\nfunc Worker(\n\tdryRun, uncomment, addSchemaReference, keepFullComment, helmDocsCompatibilityMode, dontRemoveHelmDocsPrefix, dontAddGlobal, annotate bool,\n\tvalueFileNames []string,\n\tskipAutoGenerationConfig *SkipAutoGenerationConfig,\n\toutFile string,\n\tqueue <-chan string,\n\tresults chan<- Result,\n) {\n\tfor chartPath := range queue {\n\t\tresult := Result{ChartPath: chartPath}\n\n\t\tchartBasePath := filepath.Dir(chartPath)\n\t\tfile, err := os.Open(chartPath)\n\t\tif err != nil {\n\t\t\tresult.Errors = append(result.Errors, err)\n\t\t\tresults <- result\n\t\t\tcontinue\n\t\t}\n\n\t\tchart, err := chart.ReadChart(file)\n\t\tfile.Close()\n\t\tif err != nil {\n\t\t\tresult.Errors = append(result.Errors, err)\n\t\t\tresults <- result\n\t\t\tcontinue\n\t\t}\n\t\tresult.Chart = &chart\n\n\t\tvar valuesPath string\n\t\tvaluesPaths := []string{}\n\t\terrorsWeMaybeCanIgnore := []error{}\n\n\t\tfor _, possibleValueFileName := range valueFileNames {\n\t\t\tcandidatePath := filepath.Join(chartBasePath, possibleValueFileName)\n\t\t\t_, err := os.Stat(candidatePath)\n\t\t\tif err != nil {\n\t\t\t\tif !os.IsNotExist(err) {\n\t\t\t\t\terrorsWeMaybeCanIgnore = append(errorsWeMaybeCanIgnore, err)\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tvaluesPaths = append(valuesPaths, candidatePath)\n\t\t}\n\n\t\tif len(valuesPaths) == 0 {\n\t\t\tresult.Errors = append(result.Errors, errorsWeMaybeCanIgnore...)\n\t\t\tresult.Errors = append(result.Errors, fmt.Errorf(\"no values file found (tried: %s)\", strings.Join(valueFileNames, \", \")))\n\t\t\tresults <- result\n\t\t\tcontinue\n\t\t}\n\t\tvaluesPath = valuesPaths[0]\n\t\tresult.ValuesPath = valuesPath\n\n\t\t// Annotate mode: write @schema annotations into values.yaml and skip schema generation\n\t\tif annotate {\n\t\t\tif err := AnnotateValuesFile(valuesPath, dryRun); err != nil {\n\t\t\t\tresult.Errors = append(result.Errors, err)\n\t\t\t}\n\t\t\tresults <- result\n\t\t\tcontinue\n\t\t}\n\n\t\tvaluesFile, err := os.Open(valuesPath)\n\t\tif err != nil {\n\t\t\tresult.Errors = append(result.Errors, err)\n\t\t\tresults <- result\n\t\t\tcontinue\n\t\t}\n\t\tcontent, err := util.ReadFileAndFixNewline(valuesFile)\n\t\tvaluesFile.Close()\n\t\tif err != nil {\n\t\t\tresult.Errors = append(result.Errors, err)\n\t\t\tresults <- result\n\t\t\tcontinue\n\t\t}\n\n\t\t// Check if we need to add a schema reference\n\t\tif addSchemaReference && !dryRun {\n\t\t\tschemaRef := `# yaml-language-server: $schema=values.schema.json`\n\t\t\tif !strings.Contains(string(content), schemaRef) {\n\t\t\t\terr = util.PrefixFirstYamlDocument(schemaRef, valuesPath)\n\t\t\t\tif err != nil {\n\t\t\t\t\tresult.Errors = append(result.Errors, err)\n\t\t\t\t\tresults <- result\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tvar mergedValues *yaml.Node\n\t\tfor _, currentValuesPath := range valuesPaths {\n\t\t\tvaluesFile, err := os.Open(currentValuesPath)\n\t\t\tif err != nil {\n\t\t\t\tresult.Errors = append(result.Errors, err)\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tcurrentContent, err := util.ReadFileAndFixNewline(valuesFile)\n\t\t\tvaluesFile.Close()\n\t\t\tif err != nil {\n\t\t\t\tresult.Errors = append(result.Errors, err)\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif uncomment {\n\t\t\t\t// Remove comments from valid yaml before parsing.\n\t\t\t\tcurrentContent, err = util.RemoveCommentsFromYaml(bytes.NewReader(currentContent))\n\t\t\t\tif err != nil {\n\t\t\t\t\tresult.Errors = append(result.Errors, err)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar currentValues yaml.Node\n\t\t\terr = yaml.Unmarshal(currentContent, &currentValues)\n\t\t\tif err != nil {\n\t\t\t\tresult.Errors = append(result.Errors, err)\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tmergedValues, err = mergeValuesDocuments(mergedValues, &currentValues)\n\t\t\tif err != nil {\n\t\t\t\tresult.Errors = append(result.Errors, err)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif len(result.Errors) > 0 {\n\t\t\tresults <- result\n\t\t\tcontinue\n\t\t}\n\n\t\tschema, err := YamlToSchema(valuesPath, mergedValues, keepFullComment, helmDocsCompatibilityMode, dontRemoveHelmDocsPrefix, dontAddGlobal, skipAutoGenerationConfig, nil)\n\t\tif err != nil {\n\t\t\tresult.Errors = append(result.Errors, err)\n\t\t\tresults <- result\n\t\t\tcontinue\n\t\t}\n\t\tresult.Schema = *schema\n\n\t\tresults <- result\n\t}\n}\n"
  },
  {
    "path": "pkg/schema/worker_test.go",
    "content": "package schema\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestWorker(t *testing.T) {\n\ttests := []struct {\n\t\tname                      string\n\t\tsetupFiles                map[string]string // map of filepath to content\n\t\tchartPath                 string\n\t\tvalueFileNames            []string\n\t\tdryRun                    bool\n\t\tuncomment                 bool\n\t\taddSchemaReference        bool\n\t\tkeepFullComment           bool\n\t\thelmDocsCompatibilityMode bool\n\t\tdontRemoveHelmDocsPrefix  bool\n\t\tdontAddGlobal             bool\n\t\tskipAutoGenerationConfig  *SkipAutoGenerationConfig\n\t\toutFile                   string\n\t\texpectedErrors            bool\n\t}{\n\t\t{\n\t\t\tname: \"valid chart and values\",\n\t\t\tsetupFiles: map[string]string{\n\t\t\t\t\"Chart.yaml\": `\napiVersion: v2\nname: test-chart\nversion: 1.0.0\n`,\n\t\t\t\t\"values.yaml\": `\n# -- first value\nkey1: value1\n# -- second value\nkey2: value2\n`,\n\t\t\t},\n\t\t\tchartPath:                 \"Chart.yaml\",\n\t\t\tvalueFileNames:            []string{\"values.yaml\"},\n\t\t\tuncomment:                 true,\n\t\t\taddSchemaReference:        true,\n\t\t\tkeepFullComment:           true,\n\t\t\thelmDocsCompatibilityMode: true,\n\t\t\tskipAutoGenerationConfig: &SkipAutoGenerationConfig{\n\t\t\t\tTitle:                false,\n\t\t\t\tDescription:          false,\n\t\t\t\tRequired:             false,\n\t\t\t\tDefault:              false,\n\t\t\t\tAdditionalProperties: false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"missing values file\",\n\t\t\tsetupFiles: map[string]string{\n\t\t\t\t\"Chart.yaml\": `\napiVersion: v2\nname: test-chart\nversion: 1.0.0\n`,\n\t\t\t},\n\t\t\tchartPath:      \"Chart.yaml\",\n\t\t\tvalueFileNames: []string{\"values.yaml\"},\n\t\t\texpectedErrors: true,\n\t\t\tskipAutoGenerationConfig: &SkipAutoGenerationConfig{\n\t\t\t\tTitle:                false,\n\t\t\t\tDescription:          false,\n\t\t\t\tRequired:             false,\n\t\t\t\tDefault:              false,\n\t\t\t\tAdditionalProperties: false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"invalid chart file\",\n\t\t\tsetupFiles: map[string]string{\n\t\t\t\t\"Chart.yaml\": `\nname: [this is invalid yaml\nversion: 1.0.0\n`,\n\t\t\t\t\"values.yaml\": `\nkey1: value1\n`,\n\t\t\t},\n\t\t\tchartPath:      \"Chart.yaml\",\n\t\t\tvalueFileNames: []string{\"values.yaml\"},\n\t\t\texpectedErrors: true,\n\t\t\tskipAutoGenerationConfig: &SkipAutoGenerationConfig{\n\t\t\t\tTitle:                false,\n\t\t\t\tDescription:          false,\n\t\t\t\tRequired:             false,\n\t\t\t\tDefault:              false,\n\t\t\t\tAdditionalProperties: false,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Create temporary directory\n\t\t\ttmpDir, err := os.MkdirTemp(\"\", \"worker-test-*\")\n\t\t\tassert.NoError(t, err)\n\t\t\tdefer os.RemoveAll(tmpDir)\n\n\t\t\t// Create test files\n\t\t\tfor filename, content := range tt.setupFiles {\n\t\t\t\tpath := filepath.Join(tmpDir, filename)\n\t\t\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\n\t\t\t// Setup channels\n\t\t\tqueue := make(chan string, 1)\n\t\t\tresults := make(chan Result, 1)\n\n\t\t\t// Update chart path to use temp directory\n\t\t\tfullChartPath := filepath.Join(tmpDir, tt.chartPath)\n\t\t\tqueue <- fullChartPath\n\t\t\tclose(queue)\n\n\t\t\t// Run worker\n\t\t\tWorker(\n\t\t\t\ttt.dryRun,\n\t\t\t\ttt.uncomment,\n\t\t\t\ttt.addSchemaReference,\n\t\t\t\ttt.keepFullComment,\n\t\t\t\ttt.helmDocsCompatibilityMode,\n\t\t\t\ttt.dontRemoveHelmDocsPrefix,\n\t\t\t\ttt.dontAddGlobal,\n\t\t\t\tfalse, // annotate\n\t\t\t\ttt.valueFileNames,\n\t\t\t\ttt.skipAutoGenerationConfig,\n\t\t\t\ttt.outFile,\n\t\t\t\tqueue,\n\t\t\t\tresults,\n\t\t\t)\n\n\t\t\t// Get result\n\t\t\tresult := <-results\n\n\t\t\tif tt.expectedErrors {\n\t\t\t\tassert.NotEmpty(t, result.Errors)\n\t\t\t} else {\n\t\t\t\tassert.Empty(t, result.Errors)\n\t\t\t\tassert.NotNil(t, result.Chart)\n\t\t\t\tassert.NotEmpty(t, result.ValuesPath)\n\t\t\t\tassert.NotNil(t, result.Schema)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestWorker_DryRunDoesNotWriteSchemaReference(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\tchartPath := filepath.Join(tmpDir, \"Chart.yaml\")\n\tvaluesPath := filepath.Join(tmpDir, \"values.yaml\")\n\n\terr := os.WriteFile(chartPath, []byte(\"apiVersion: v2\\nname: test-chart\\nversion: 1.0.0\\n\"), 0o644)\n\tassert.NoError(t, err)\n\terr = os.WriteFile(valuesPath, []byte(\"key: value\\n\"), 0o644)\n\tassert.NoError(t, err)\n\n\tqueue := make(chan string, 1)\n\tresults := make(chan Result, 1)\n\tqueue <- chartPath\n\tclose(queue)\n\n\tWorker(\n\t\ttrue,  // dryRun\n\t\tfalse, // uncomment\n\t\ttrue,  // addSchemaReference\n\t\tfalse, // keepFullComment\n\t\tfalse, // helmDocsCompatibilityMode\n\t\tfalse, // dontRemoveHelmDocsPrefix\n\t\tfalse, // dontAddGlobal\n\t\tfalse, // annotate\n\t\t[]string{\"values.yaml\"},\n\t\t&SkipAutoGenerationConfig{},\n\t\t\"values.schema.json\",\n\t\tqueue,\n\t\tresults,\n\t)\n\n\tresult := <-results\n\tassert.Empty(t, result.Errors)\n\n\tupdated, err := os.ReadFile(valuesPath)\n\tassert.NoError(t, err)\n\tassert.NotContains(t, string(updated), \"yaml-language-server: $schema=values.schema.json\")\n}\n\nfunc TestWorker_MergesMultipleValuesFilesWithRightmostPrecedence(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\tchartPath := filepath.Join(tmpDir, \"Chart.yaml\")\n\tbaseValuesPath := filepath.Join(tmpDir, \"values.base.yaml\")\n\toverrideValuesPath := filepath.Join(tmpDir, \"values.prod.yaml\")\n\n\terr := os.WriteFile(chartPath, []byte(\"apiVersion: v2\\nname: test-chart\\nversion: 1.0.0\\n\"), 0o644)\n\tassert.NoError(t, err)\n\n\terr = os.WriteFile(baseValuesPath, []byte(`\nimage:\n  repository: nginx\n  tag: stable\n# @schema\n# description: base replicas\n# @schema\nreplicas: 1\n`), 0o644)\n\tassert.NoError(t, err)\n\n\terr = os.WriteFile(overrideValuesPath, []byte(`\nimage:\n  tag: latest\n  pullPolicy: Always\n# @schema\n# description: production replicas\n# @schema\nreplicas: \"two\"\n`), 0o644)\n\tassert.NoError(t, err)\n\n\tqueue := make(chan string, 1)\n\tresults := make(chan Result, 1)\n\tqueue <- chartPath\n\tclose(queue)\n\n\tWorker(\n\t\tfalse, // dryRun\n\t\tfalse, // uncomment\n\t\tfalse, // addSchemaReference\n\t\tfalse, // keepFullComment\n\t\tfalse, // helmDocsCompatibilityMode\n\t\tfalse, // dontRemoveHelmDocsPrefix\n\t\tfalse, // dontAddGlobal\n\t\tfalse, // annotate\n\t\t[]string{\"values.base.yaml\", \"values.prod.yaml\"},\n\t\t&SkipAutoGenerationConfig{},\n\t\t\"values.schema.json\",\n\t\tqueue,\n\t\tresults,\n\t)\n\n\tresult := <-results\n\tassert.Empty(t, result.Errors)\n\n\treplicasSchema := result.Schema.Properties[\"replicas\"]\n\tif assert.NotNil(t, replicasSchema) {\n\t\tassert.Equal(t, \"production replicas\", replicasSchema.Description)\n\t\tassert.Equal(t, \"two\", replicasSchema.Default)\n\t}\n\n\timageSchema := result.Schema.Properties[\"image\"]\n\tif assert.NotNil(t, imageSchema) {\n\t\trepositorySchema := imageSchema.Properties[\"repository\"]\n\t\tif assert.NotNil(t, repositorySchema) {\n\t\t\tassert.Equal(t, \"nginx\", repositorySchema.Default)\n\t\t}\n\n\t\ttagSchema := imageSchema.Properties[\"tag\"]\n\t\tif assert.NotNil(t, tagSchema) {\n\t\t\tassert.Equal(t, \"latest\", tagSchema.Default)\n\t\t}\n\n\t\tpullPolicySchema := imageSchema.Properties[\"pullPolicy\"]\n\t\tif assert.NotNil(t, pullPolicySchema) {\n\t\t\tassert.Equal(t, \"Always\", pullPolicySchema.Default)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/util/file.go",
    "content": "package util\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\n// ReadFileAndFixNewline reads the content of a io.Reader and replaces \\r\\n with \\n\nfunc ReadFileAndFixNewline(reader io.Reader) ([]byte, error) {\n\tcontent, err := io.ReadAll(reader)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn []byte(strings.ReplaceAll(string(content), \"\\r\\n\", \"\\n\")), nil\n}\n\nfunc appendAndNL(to, from *[]byte) {\n\tif from != nil {\n\t\t*to = append(*to, *from...)\n\t}\n\t*to = append(*to, '\\n')\n}\n\nfunc appendAndNLStr(to *[]byte, from string) {\n\t*to = append(*to, from...)\n\t*to = append(*to, '\\n')\n}\n\n// PrefixFirstYamlDocument inserts a line to the beginning of the first YAML document in a file having content\nfunc PrefixFirstYamlDocument(line, file string) error {\n\tfileInfo, err := os.Stat(file)\n\tif err != nil {\n\t\treturn err\n\t}\n\tperm := fileInfo.Mode().Perm()\n\tcontent, err := os.ReadFile(file)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\teol := \"\\n\"\n\tif len(content) >= 2 && content[len(content)-2] == '\\r' && content[len(content)-1] == '\\n' {\n\t\teol = \"\\r\\n\"\n\t}\n\n\t// put line directly below YAML document_start if it exists and nothing is preceding it\n\tdocumentStart := \"---\" + eol\n\tif strings.HasPrefix(string(content), documentStart) {\n\t\tcontent = content[len(documentStart):]\n\t\tline = documentStart + line\n\t}\n\n\tnewContent := line + eol + string(content)\n\treturn os.WriteFile(file, []byte(newContent), perm)\n}\n\n// RemoveCommentsFromYaml tries to remove comments if they contain valid yaml\nfunc RemoveCommentsFromYaml(reader io.Reader) ([]byte, error) {\n\tresult := make([]byte, 0)\n\tbuff := make([]byte, 0)\n\tscanner := bufio.NewScanner(reader)\n\n\tcommentMatcher := regexp.MustCompile(`^\\s*#\\s*`)\n\t// Capture indentation and comment marker separately\n\t// Group 1: indentation (spaces/tabs before #)\n\t// Group 2: comment marker (# and following space)\n\tcommentYamlMapMatcher := regexp.MustCompile(`^(\\s*)(#\\s*)([^:]+:.*)$`)\n\tschemaMatcher := regexp.MustCompile(`^\\s*#\\s@schema\\s*`)\n\n\tvar line string\n\tvar inCode, inSchema bool\n\tvar unknownYaml interface{}\n\n\tfor scanner.Scan() {\n\t\tline = scanner.Text()\n\n\t\t// If the line is empty and we are parsing a block of potential yaml,\n\t\t// the parsed block of yaml is \"finished\" and should be added to the\n\t\t// result\n\t\tif line == \"\" && inCode {\n\t\t\tappendAndNL(&result, &buff)\n\t\t\tappendAndNLStr(&result, line)\n\t\t\t// reset\n\t\t\tinCode = false\n\t\t\tbuff = make([]byte, 0)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Line contains @schema\n\t\t// The following lines will be added to result\n\t\tif schemaMatcher.Match([]byte(line)) {\n\t\t\tinSchema = !inSchema\n\t\t\tappendAndNLStr(&result, line)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Inside a @schema block\n\t\tif inSchema {\n\t\t\tappendAndNLStr(&result, line)\n\t\t\tcontinue\n\t\t}\n\n\t\tvar indentation string\n\t\tvar commentMarkerLen int\n\n\t\t// Havent found a potential yaml block yet\n\t\tif !inCode {\n\t\t\tif matches := commentYamlMapMatcher.FindStringSubmatch(line); matches != nil {\n\t\t\t\tindentation = matches[1]           // Just the leading whitespace\n\t\t\t\tcommentMarkerLen = len(matches[2]) // Just \"# \" (typically 2 chars)\n\t\t\t\tinCode = true\n\t\t\t}\n\t\t}\n\n\t\t// Try if this line is valid yaml\n\t\tif inCode {\n\t\t\tif commentMatcher.Match([]byte(line)) {\n\t\t\t\t// Strip the commet away\n\t\t\t\tstrippedLine := indentation + line[len(indentation)+commentMarkerLen:]\n\t\t\t\t// add it to the already parsed valid yaml\n\t\t\t\tappendAndNLStr(&buff, strippedLine)\n\t\t\t\t// check if the new block is still valid yaml\n\t\t\t\terr := yaml.Unmarshal(buff, &unknownYaml)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// Invalid yaml found,\n\t\t\t\t\t// Remove the stripped line again\n\t\t\t\t\tbuff = buff[:len(buff)-len(strippedLine)-1]\n\t\t\t\t\t// and add the commented line instead\n\t\t\t\t\tappendAndNLStr(&buff, line)\n\t\t\t\t}\n\t\t\t\t// its still valid yaml\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// FIX: Line is NOT a comment - we've exited the commented block\n\t\t\t// Flush the buffer to result and reset state\n\t\t\tif len(buff) > 0 {\n\t\t\t\tappendAndNL(&result, &buff)\n\t\t\t\tbuff = make([]byte, 0)\n\t\t\t}\n\t\t\tinCode = false\n\n\t\t\t// If the line is not a comment it must be yaml\n\t\t\tappendAndNLStr(&buff, line)\n\t\t\tcontinue\n\t\t}\n\t\t// line is valid yaml\n\t\tappendAndNLStr(&result, line)\n\t}\n\n\tif len(buff) > 0 {\n\t\tappendAndNL(&result, &buff)\n\t}\n\n\treturn result, nil\n}\n\n// IsRelativeFile checks if the given string is a relative path to a file\nfunc IsRelativeFile(root, relPath string) (string, error) {\n\tif relPath == \"\" {\n\t\treturn \"\", errors.New(\"path is empty\")\n\t}\n\n\tif !filepath.IsAbs(relPath) {\n\t\tresolvedPath := filepath.Join(filepath.Dir(root), relPath)\n\t\tfileInfo, err := os.Stat(resolvedPath)\n\t\tif err != nil {\n\t\t\treturn resolvedPath, err\n\t\t}\n\t\tif fileInfo.IsDir() {\n\t\t\treturn resolvedPath, fmt.Errorf(\"path is a directory: %s\", resolvedPath)\n\t\t}\n\n\t\treturn resolvedPath, nil\n\t}\n\n\treturn \"\", errors.New(\"path is absolute\")\n}\n"
  },
  {
    "path": "pkg/util/file_test.go",
    "content": "package util\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\nfunc TestReadFileAndFixNewline(t *testing.T) {\n\ttests := []struct {\n\t\tinput  string\n\t\toutput []byte\n\t}{\n\t\t{\n\t\t\tinput:  \"foo\",\n\t\t\toutput: []byte(\"foo\"),\n\t\t},\n\t\t{\n\t\t\tinput:  \"foo\\r\\nbar\",\n\t\t\toutput: []byte(\"foo\\nbar\"),\n\t\t},\n\t\t{\n\t\t\tinput:  \"foo\\r\\n\\r\\nbar\",\n\t\t\toutput: []byte(\"foo\\n\\nbar\"),\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tdata := []byte(test.input)\n\t\treader := bytes.NewReader(data)\n\t\tcontent, err := ReadFileAndFixNewline(reader)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Wasn't expecting an error, but got this: %v\", err)\n\t\t}\n\t\tif !bytes.Equal(content, test.output) {\n\t\t\tt.Errorf(\"Was expecting %s, but got %s\", test.output, content)\n\t\t}\n\t}\n}\n\nfunc TestIsRelativeFile(t *testing.T) {\n\ttempDir := t.TempDir()\n\trootFile := filepath.Join(tempDir, \"values.yaml\")\n\trelativeFile := filepath.Join(tempDir, \"ref.json\")\n\trelativeDir := filepath.Join(tempDir, \"schemas\")\n\tabsFile := filepath.Join(tempDir, \"absolute.json\")\n\n\tif err := os.WriteFile(rootFile, []byte(\"root\"), 0o644); err != nil {\n\t\tt.Fatalf(\"failed to create root file: %v\", err)\n\t}\n\tif err := os.WriteFile(relativeFile, []byte(\"{}\"), 0o644); err != nil {\n\t\tt.Fatalf(\"failed to create relative file: %v\", err)\n\t}\n\tif err := os.Mkdir(relativeDir, 0o755); err != nil {\n\t\tt.Fatalf(\"failed to create relative dir: %v\", err)\n\t}\n\tif err := os.WriteFile(absFile, []byte(\"{}\"), 0o644); err != nil {\n\t\tt.Fatalf(\"failed to create absolute file: %v\", err)\n\t}\n\n\ttests := []struct {\n\t\tname         string\n\t\troot         string\n\t\trelPath      string\n\t\texpectedPath string\n\t\twantErr      bool\n\t}{\n\t\t{\n\t\t\tname:         \"existing relative file\",\n\t\t\troot:         rootFile,\n\t\t\trelPath:      \"ref.json\",\n\t\t\texpectedPath: relativeFile,\n\t\t},\n\t\t{\n\t\t\tname:    \"empty path\",\n\t\t\troot:    rootFile,\n\t\t\trelPath: \"\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:         \"relative directory\",\n\t\t\troot:         rootFile,\n\t\t\trelPath:      \"schemas\",\n\t\t\texpectedPath: relativeDir,\n\t\t\twantErr:      true,\n\t\t},\n\t\t{\n\t\t\tname:    \"absolute path\",\n\t\t\troot:    rootFile,\n\t\t\trelPath: absFile,\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresolvedPath, err := IsRelativeFile(tt.root, tt.relPath)\n\t\t\tif tt.wantErr {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Fatalf(\"expected an error\")\n\t\t\t\t}\n\t\t\t} else if err != nil {\n\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t}\n\n\t\t\tif resolvedPath != tt.expectedPath {\n\t\t\t\tt.Fatalf(\"expected resolved path %q, got %q\", tt.expectedPath, resolvedPath)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "plugin.yaml",
    "content": "---\nname: \"schema\"\nversion: \"0.23.2\"\nusage: \"generate jsonschemas for your helm charts\"\ndescription: \"generate jsonschemas for your helm charts\"\ncommand: \"$HELM_PLUGIN_DIR/bin/helm-schema\"\nhooks:\n  install: \"$HELM_PLUGIN_DIR/install-binary.sh\"\n  update: \"$HELM_PLUGIN_DIR/install-binary.sh -u\"\n"
  },
  {
    "path": "renovate.json",
    "content": "{\n  \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n  \"extends\": [\"config:best-practices\", \"schedule:earlyMondays\"],\n  \"bumpVersion\": \"patch\",\n  \"prHourlyLimit\": 4\n}\n"
  },
  {
    "path": "sign-plugin.sh",
    "content": "#!/bin/bash\n# Script to sign Helm plugin tarballs for Helm v4 verification\n# This creates .prov (provenance) files using GPG signing\n\nset -euo pipefail\n\nPLUGIN_NAME=\"helm-schema\"\nVERSION=\"${1:-}\"\nTARBALL=\"${2:-}\"\nGPG_KEY=\"${GPG_SIGNING_KEY:-}\"\nKEYRING=\"${GPG_KEYRING:-$HOME/.gnupg/pubring.gpg}\"\n\nusage() {\n    cat <<EOF\nUsage: $0 <version> <tarball> [gpg-key]\n\nSigns a Helm plugin tarball with GPG to create a provenance file (.prov)\nfor Helm v4 plugin verification.\n\nArguments:\n    version     Plugin version (e.g., 1.0.0)\n    tarball     Path to the plugin tarball to sign\n    gpg-key     GPG key name or email (optional, uses GPG_SIGNING_KEY env var)\n\nEnvironment Variables:\n    GPG_SIGNING_KEY     GPG key to use for signing\n    GPG_KEYRING         Path to GPG keyring (default: ~/.gnupg/pubring.gpg)\n    GPG_PASSPHRASE      GPG key passphrase (if needed)\n\nExample:\n    $0 1.0.0 dist/helm-schema_1.0.0_Linux_x86_64.tar.gz \"John Doe <john@example.com>\"\n\nEOF\n    exit 1\n}\n\nif [ -z \"$VERSION\" ] || [ -z \"$TARBALL\" ]; then\n    usage\nfi\n\nif [ ! -f \"$TARBALL\" ]; then\n    echo \"Error: Tarball not found: $TARBALL\"\n    exit 1\nfi\n\n# If GPG key not provided as argument, try environment variable\nif [ $# -ge 3 ]; then\n    GPG_KEY=\"$3\"\nfi\n\nif [ -z \"$GPG_KEY\" ]; then\n    echo \"Error: GPG signing key not specified\"\n    echo \"Provide it as third argument or set GPG_SIGNING_KEY environment variable\"\n    exit 1\nfi\n\necho \"Signing plugin tarball with GPG...\"\necho \"  Tarball: $TARBALL\"\necho \"  Version: $VERSION\"\necho \"  GPG Key: $GPG_KEY\"\necho \"  Keyring: $KEYRING\"\n\n# Export keys to legacy format if needed (for GnuPG v2)\nif ! [ -f \"$KEYRING\" ]; then\n    echo \"Exporting GPG keys to legacy format...\"\n    mkdir -p \"$(dirname \"$KEYRING\")\"\n    gpg --export > \"$KEYRING\" 2>/dev/null || true\nfi\n\n# Create a temporary directory for signing\nTEMP_DIR=$(mktemp -d)\ntrap 'rm -rf \"$TEMP_DIR\"' EXIT\n\n# Save the original directory and convert tarball path to absolute\nORIG_DIR=\"$(pwd)\"\nTARBALL_DIR=\"$(cd \"$(dirname \"$TARBALL\")\" && pwd)\"\nTARBALL_NAME=$(basename \"$TARBALL\")\n\n# Copy tarball to temp directory\ncp \"$TARBALL\" \"$TEMP_DIR/\"\n\ncd \"$TEMP_DIR\"\n\n# Create the provenance file\n# The provenance file contains:\n# 1. The plugin metadata (from plugin.yaml)\n# 2. SHA256 hash of the tarball\n# 3. GPG signature of the above\necho \"Creating provenance file...\"\n\n# Extract plugin.yaml from tarball to include in provenance\ntar -xzf \"$TARBALL_NAME\" plugin.yaml 2>/dev/null || tar -xzf \"$TARBALL_NAME\" */plugin.yaml 2>/dev/null || true\n\n# Calculate SHA256 hash\nHASH=$(sha256sum \"$TARBALL_NAME\" | awk '{print $1}')\n\n# Create provenance content\ncat > \"${TARBALL_NAME}.prov.tmp\" <<EOF\nname: $PLUGIN_NAME\nversion: $VERSION\ndescription: Generate jsonschemas for your helm charts\nhome: https://github.com/dadav/helm-schema\n\n...\nfiles:\n  $TARBALL_NAME: sha256:$HASH\nEOF\n\n# If plugin.yaml was extracted, append it\nif [ -f plugin.yaml ]; then\n    echo \"\" >> \"${TARBALL_NAME}.prov.tmp\"\n    echo \"plugin.yaml: |\" >> \"${TARBALL_NAME}.prov.tmp\"\n    sed 's/^/  /' plugin.yaml >> \"${TARBALL_NAME}.prov.tmp\"\nfi\n\n# Sign the provenance file\nif [ -n \"${GPG_PASSPHRASE:-}\" ]; then\n    # Use passphrase from environment if available\n    echo \"$GPG_PASSPHRASE\" | gpg --batch --yes --passphrase-fd 0 \\\n        --clearsign \\\n        --local-user \"$GPG_KEY\" \\\n        --output \"${TARBALL_NAME}.prov\" \\\n        \"${TARBALL_NAME}.prov.tmp\"\nelse\n    # Interactive passphrase prompt\n    gpg --clearsign \\\n        --local-user \"$GPG_KEY\" \\\n        --output \"${TARBALL_NAME}.prov\" \\\n        \"${TARBALL_NAME}.prov.tmp\"\nfi\n\n# Copy back to original location\ncp \"${TARBALL_NAME}.prov\" \"$TARBALL_DIR/\"\n\necho \"✓ Successfully created provenance file: ${TARBALL_DIR}/${TARBALL_NAME}.prov\"\necho \"\"\necho \"To verify the signature:\"\necho \"  helm plugin verify $(basename \"$TARBALL\")\"\necho \"\"\necho \"To install with verification:\"\necho \"  helm plugin install $(basename \"$TARBALL\") --verify\"\n"
  },
  {
    "path": "signing-key.asc",
    "content": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmDMEaRt56xYJKwYBBAHaRw8BAQdA4RAN5LipEazZbSMV+BLcJh1BHY39WxIxS8tm\nalFiioi0HGRhZGF2IDxkYWRhdkBwcm90b25tYWlsLmNvbT6IlgQTFgoAPhYhBIBv\ncNJWZ9Qqrk4HzvWHB5adD7+lBQJpG3nrAhsDBQkFo5qABQsJCAcCBhUKCQgLAgQW\nAgMBAh4BAheAAAoJEPWHB5adD7+lWmQA/jgCXu/usDsdt0bOeU17FQxb74mkOY/B\ny8DSFgJrj5RSAQDeh+6JMcK+np0+9S0TRNwG5TfmdvjTlH4y7pRQNDsFCrg4BGkb\neesSCisGAQQBl1UBBQEBB0D3VDcQTHbSBW1PcSbxsYukJhmNQ+fuxIje/FYk659j\nVgMBCAeIfgQYFgoAJhYhBIBvcNJWZ9Qqrk4HzvWHB5adD7+lBQJpG3nrAhsMBQkF\no5qAAAoJEPWHB5adD7+lLzMA/0WgksOYuwA4gcCCxzZadfZuwSV9UXOTZu49dSgO\nl5ozAP9CQnS6vLaNC5nXbct6f2UjXqNcEF0i8Yp1PYlWErcsAw==\n=QRXR\n-----END PGP PUBLIC KEY BLOCK-----\n"
  },
  {
    "path": "tests/.gitignore",
    "content": "*_generated.schema.json\nhelm-schema\ntest_repo_example.yaml\ntest_repo_example_expected.schema.json\n"
  },
  {
    "path": "tests/charts/Chart.yaml",
    "content": "apiVersion: v2\nname: test\ndescription: Test chart for helm-schema\ntype: application\nversion: 0.1.0\n"
  },
  {
    "path": "tests/charts/ref_input.json",
    "content": "{\n  \"foo\": {\n    \"type\": \"string\",\n    \"description\": \"from ref\"\n  },\n  \"bar\": {\n    \"type\": \"object\",\n    \"description\": \"from different ref\",\n    \"properties\": {\n      \"baz\": {\n        \"type\": \"string\",\n        \"description\": \"from ref\"\n      }\n    },\n    \"required\": [\n      \"baz\"\n    ]\n  }\n}\n"
  },
  {
    "path": "tests/charts/test_annotate_expected.yaml",
    "content": "# @schema\n# type: integer\n# @schema\nreplicaCount: 1\n\n# @schema\n# type: object\n# @schema\nimage:\n  # @schema\n  # type: string\n  # @schema\n  repository: nginx\n  # @schema\n  # type: string\n  # @schema\n  pullPolicy: IfNotPresent\n  # @schema\n  # type: string\n  # @schema\n  # Overrides the image tag whose default is the chart appVersion.\n  tag: \"\"\n\n# @schema\n# type: array\n# @schema\nimagePullSecrets: []\n# @schema\n# type: string\n# @schema\nnameOverride: \"\"\n\n# @schema\n# type: boolean\n# @schema\nalreadyAnnotated: true\n\n# @schema\n# type: object\n# @schema\nservice:\n  # @schema\n  # type: string\n  # @schema\n  type: ClusterIP\n  # @schema\n  # type: integer\n  # @schema\n  port: 80\n\n# @schema\n# type: boolean\n# @schema\nenabled: false\n\n# @schema\n# type: object\n# @schema\nconfig: {}\n\n# @schema\n# type: \"null\"\n# @schema\nkey:\n"
  },
  {
    "path": "tests/charts/test_annotate_input.yaml",
    "content": "replicaCount: 1\n\nimage:\n  repository: nginx\n  pullPolicy: IfNotPresent\n  # Overrides the image tag whose default is the chart appVersion.\n  tag: \"\"\n\nimagePullSecrets: []\nnameOverride: \"\"\n\n# @schema\n# type: boolean\n# @schema\nalreadyAnnotated: true\n\nservice:\n  type: ClusterIP\n  port: 80\n\nenabled: false\n\nconfig: {}\n\nkey:\n"
  },
  {
    "path": "tests/charts/test_helm_defaults.yaml",
    "content": "# Default values for foo.\n# This is a YAML-formatted file.\n# Declare variables to be passed into your templates.\n\nreplicaCount: 1\n\nimage:\n  repository: nginx\n  pullPolicy: IfNotPresent\n  # Overrides the image tag whose default is the chart appVersion.\n  tag: \"\"\n\nimagePullSecrets: []\nnameOverride: \"\"\nfullnameOverride: \"\"\n\nserviceAccount:\n  # Specifies whether a service account should be created\n  create: true\n  # Automatically mount a ServiceAccount's API credentials?\n  automount: true\n  # Annotations to add to the service account\n  annotations: {}\n  # The name of the service account to use.\n  # If not set and create is true, a name is generated using the fullname template\n  name: \"\"\n\npodAnnotations: {}\npodLabels: {}\n\npodSecurityContext: {}\n  # fsGroup: 2000\n\nsecurityContext: {}\n  # capabilities:\n  #   drop:\n  #   - ALL\n  # readOnlyRootFilesystem: true\n  # runAsNonRoot: true\n  # runAsUser: 1000\n\nservice:\n  type: ClusterIP\n  port: 80\n\ningress:\n  enabled: false\n  className: \"\"\n  annotations: {}\n    # kubernetes.io/ingress.class: nginx\n    # kubernetes.io/tls-acme: \"true\"\n  hosts:\n    - host: chart-example.local\n      paths:\n        - path: /\n          pathType: ImplementationSpecific\n  tls: []\n  #  - secretName: chart-example-tls\n  #    hosts:\n  #      - chart-example.local\n\nresources: {}\n  # We usually recommend not to specify default resources and to leave this as a conscious\n  # choice for the user. This also increases chances charts run on environments with little\n  # resources, such as Minikube. If you do want to specify resources, uncomment the following\n  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.\n  # limits:\n  #   cpu: 100m\n  #   memory: 128Mi\n  # requests:\n  #   cpu: 100m\n  #   memory: 128Mi\n\nlivenessProbe:\n  httpGet:\n    path: /\n    port: http\nreadinessProbe:\n  httpGet:\n    path: /\n    port: http\n\nautoscaling:\n  enabled: false\n  minReplicas: 1\n  maxReplicas: 100\n  targetCPUUtilizationPercentage: 80\n  # targetMemoryUtilizationPercentage: 80\n\n# Additional volumes on the output Deployment definition.\nvolumes: []\n# - name: foo\n#   secret:\n#     secretName: mysecret\n#     optional: false\n\n# Additional volumeMounts on the output Deployment definition.\nvolumeMounts: []\n# - name: foo\n#   mountPath: \"/etc/foo\"\n#   readOnly: true\n\nnodeSelector: {}\n\ntolerations: []\n\naffinity: {}\n"
  },
  {
    "path": "tests/charts/test_helm_defaults_expected.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"affinity\": {\n      \"additionalProperties\": false,\n      \"required\": [],\n      \"title\": \"affinity\",\n      \"type\": \"object\"\n    },\n    \"autoscaling\": {\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"enabled\": {\n          \"default\": false,\n          \"title\": \"enabled\",\n          \"type\": \"boolean\"\n        },\n        \"maxReplicas\": {\n          \"default\": 100,\n          \"title\": \"maxReplicas\",\n          \"type\": \"integer\"\n        },\n        \"minReplicas\": {\n          \"default\": 1,\n          \"title\": \"minReplicas\",\n          \"type\": \"integer\"\n        },\n        \"targetCPUUtilizationPercentage\": {\n          \"default\": 80,\n          \"title\": \"targetCPUUtilizationPercentage\",\n          \"type\": \"integer\"\n        }\n      },\n      \"required\": [\n        \"enabled\",\n        \"minReplicas\",\n        \"maxReplicas\",\n        \"targetCPUUtilizationPercentage\"\n      ],\n      \"title\": \"autoscaling\",\n      \"type\": \"object\"\n    },\n    \"fullnameOverride\": {\n      \"default\": \"\",\n      \"title\": \"fullnameOverride\",\n      \"type\": \"string\"\n    },\n    \"global\": {\n      \"description\": \"Global values are values that can be accessed from any chart or subchart by exactly the same name.\",\n      \"required\": [],\n      \"title\": \"global\",\n      \"type\": \"object\"\n    },\n    \"image\": {\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"pullPolicy\": {\n          \"default\": \"IfNotPresent\",\n          \"title\": \"pullPolicy\",\n          \"type\": \"string\"\n        },\n        \"repository\": {\n          \"default\": \"nginx\",\n          \"title\": \"repository\",\n          \"type\": \"string\"\n        },\n        \"tag\": {\n          \"default\": \"\",\n          \"description\": \"Overrides the image tag whose default is the chart appVersion.\",\n          \"title\": \"tag\",\n          \"type\": \"string\"\n        }\n      },\n      \"required\": [\n        \"repository\",\n        \"pullPolicy\",\n        \"tag\"\n      ],\n      \"title\": \"image\",\n      \"type\": \"object\"\n    },\n    \"imagePullSecrets\": {\n      \"items\": {\n        \"required\": []\n      },\n      \"title\": \"imagePullSecrets\",\n      \"type\": \"array\"\n    },\n    \"ingress\": {\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"annotations\": {\n          \"additionalProperties\": false,\n          \"required\": [],\n          \"title\": \"annotations\",\n          \"type\": \"object\"\n        },\n        \"className\": {\n          \"default\": \"\",\n          \"title\": \"className\",\n          \"type\": \"string\"\n        },\n        \"enabled\": {\n          \"default\": false,\n          \"title\": \"enabled\",\n          \"type\": \"boolean\"\n        },\n        \"hosts\": {\n          \"description\": \"kubernetes.io/ingress.class: nginx\\nkubernetes.io/tls-acme: \\\"true\\\"\",\n          \"items\": {\n            \"anyOf\": [\n              {\n                \"additionalProperties\": false,\n                \"properties\": {\n                  \"host\": {\n                    \"default\": \"chart-example.local\",\n                    \"title\": \"host\",\n                    \"type\": \"string\"\n                  },\n                  \"paths\": {\n                    \"items\": {\n                      \"anyOf\": [\n                        {\n                          \"additionalProperties\": false,\n                          \"properties\": {\n                            \"path\": {\n                              \"default\": \"/\",\n                              \"title\": \"path\",\n                              \"type\": \"string\"\n                            },\n                            \"pathType\": {\n                              \"default\": \"ImplementationSpecific\",\n                              \"title\": \"pathType\",\n                              \"type\": \"string\"\n                            }\n                          },\n                          \"required\": [\n                            \"path\",\n                            \"pathType\"\n                          ],\n                          \"type\": \"object\"\n                        }\n                      ],\n                      \"required\": []\n                    },\n                    \"title\": \"paths\",\n                    \"type\": \"array\"\n                  }\n                },\n                \"required\": [\n                  \"host\",\n                  \"paths\"\n                ],\n                \"type\": \"object\"\n              }\n            ],\n            \"required\": []\n          },\n          \"title\": \"hosts\",\n          \"type\": \"array\"\n        },\n        \"tls\": {\n          \"items\": {\n            \"required\": []\n          },\n          \"title\": \"tls\",\n          \"type\": \"array\"\n        }\n      },\n      \"required\": [\n        \"enabled\",\n        \"className\",\n        \"annotations\",\n        \"hosts\",\n        \"tls\"\n      ],\n      \"title\": \"ingress\",\n      \"type\": \"object\"\n    },\n    \"livenessProbe\": {\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"httpGet\": {\n          \"additionalProperties\": false,\n          \"properties\": {\n            \"path\": {\n              \"default\": \"/\",\n              \"title\": \"path\",\n              \"type\": \"string\"\n            },\n            \"port\": {\n              \"default\": \"http\",\n              \"title\": \"port\",\n              \"type\": \"string\"\n            }\n          },\n          \"required\": [\n            \"path\",\n            \"port\"\n          ],\n          \"title\": \"httpGet\",\n          \"type\": \"object\"\n        }\n      },\n      \"required\": [\n        \"httpGet\"\n      ],\n      \"title\": \"livenessProbe\",\n      \"type\": \"object\"\n    },\n    \"nameOverride\": {\n      \"default\": \"\",\n      \"title\": \"nameOverride\",\n      \"type\": \"string\"\n    },\n    \"nodeSelector\": {\n      \"additionalProperties\": false,\n      \"required\": [],\n      \"title\": \"nodeSelector\",\n      \"type\": \"object\"\n    },\n    \"podAnnotations\": {\n      \"additionalProperties\": false,\n      \"required\": [],\n      \"title\": \"podAnnotations\",\n      \"type\": \"object\"\n    },\n    \"podLabels\": {\n      \"additionalProperties\": false,\n      \"required\": [],\n      \"title\": \"podLabels\",\n      \"type\": \"object\"\n    },\n    \"podSecurityContext\": {\n      \"additionalProperties\": false,\n      \"required\": [],\n      \"title\": \"podSecurityContext\",\n      \"type\": \"object\"\n    },\n    \"readinessProbe\": {\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"httpGet\": {\n          \"additionalProperties\": false,\n          \"properties\": {\n            \"path\": {\n              \"default\": \"/\",\n              \"title\": \"path\",\n              \"type\": \"string\"\n            },\n            \"port\": {\n              \"default\": \"http\",\n              \"title\": \"port\",\n              \"type\": \"string\"\n            }\n          },\n          \"required\": [\n            \"path\",\n            \"port\"\n          ],\n          \"title\": \"httpGet\",\n          \"type\": \"object\"\n        }\n      },\n      \"required\": [\n        \"httpGet\"\n      ],\n      \"title\": \"readinessProbe\",\n      \"type\": \"object\"\n    },\n    \"replicaCount\": {\n      \"default\": 1,\n      \"title\": \"replicaCount\",\n      \"type\": \"integer\"\n    },\n    \"resources\": {\n      \"additionalProperties\": false,\n      \"required\": [],\n      \"title\": \"resources\",\n      \"type\": \"object\"\n    },\n    \"securityContext\": {\n      \"additionalProperties\": false,\n      \"required\": [],\n      \"title\": \"securityContext\",\n      \"type\": \"object\"\n    },\n    \"service\": {\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"port\": {\n          \"default\": 80,\n          \"title\": \"port\",\n          \"type\": \"integer\"\n        },\n        \"type\": {\n          \"default\": \"ClusterIP\",\n          \"title\": \"type\",\n          \"type\": \"string\"\n        }\n      },\n      \"required\": [\n        \"type\",\n        \"port\"\n      ],\n      \"title\": \"service\",\n      \"type\": \"object\"\n    },\n    \"serviceAccount\": {\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"annotations\": {\n          \"additionalProperties\": false,\n          \"description\": \"Annotations to add to the service account\",\n          \"required\": [],\n          \"title\": \"annotations\",\n          \"type\": \"object\"\n        },\n        \"automount\": {\n          \"default\": true,\n          \"description\": \"Automatically mount a ServiceAccount's API credentials?\",\n          \"title\": \"automount\",\n          \"type\": \"boolean\"\n        },\n        \"create\": {\n          \"default\": true,\n          \"description\": \"Specifies whether a service account should be created\",\n          \"title\": \"create\",\n          \"type\": \"boolean\"\n        },\n        \"name\": {\n          \"default\": \"\",\n          \"description\": \"The name of the service account to use.\\nIf not set and create is true, a name is generated using the fullname template\",\n          \"title\": \"name\",\n          \"type\": \"string\"\n        }\n      },\n      \"required\": [\n        \"create\",\n        \"automount\",\n        \"annotations\",\n        \"name\"\n      ],\n      \"title\": \"serviceAccount\",\n      \"type\": \"object\"\n    },\n    \"tolerations\": {\n      \"items\": {\n        \"required\": []\n      },\n      \"title\": \"tolerations\",\n      \"type\": \"array\"\n    },\n    \"volumeMounts\": {\n      \"description\": \"Additional volumeMounts on the output Deployment definition.\",\n      \"items\": {\n        \"required\": []\n      },\n      \"title\": \"volumeMounts\",\n      \"type\": \"array\"\n    },\n    \"volumes\": {\n      \"description\": \"Additional volumes on the output Deployment definition.\",\n      \"items\": {\n        \"required\": []\n      },\n      \"title\": \"volumes\",\n      \"type\": \"array\"\n    }\n  },\n  \"required\": [\n    \"replicaCount\",\n    \"image\",\n    \"imagePullSecrets\",\n    \"nameOverride\",\n    \"fullnameOverride\",\n    \"serviceAccount\",\n    \"podAnnotations\",\n    \"podLabels\",\n    \"podSecurityContext\",\n    \"securityContext\",\n    \"service\",\n    \"ingress\",\n    \"resources\",\n    \"livenessProbe\",\n    \"readinessProbe\",\n    \"autoscaling\",\n    \"volumes\",\n    \"volumeMounts\",\n    \"nodeSelector\",\n    \"tolerations\",\n    \"affinity\"\n  ],\n  \"type\": \"object\"\n}"
  },
  {
    "path": "tests/charts/test_ref.yaml",
    "content": "---\n# @schema\n# $ref: ref_input.json#/foo\n# @schema\n# Not from ref\nfoo: bar\n# @schema\n# $ref: ref_input.json#/bar\n# @schema\n# Not from ref\nbar:\n  baz: qux\n"
  },
  {
    "path": "tests/charts/test_ref_expected.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"bar\": {\n      \"additionalProperties\": false,\n      \"description\": \"from different ref\",\n      \"properties\": {\n        \"baz\": {\n          \"description\": \"from ref\",\n          \"type\": \"string\"\n        }\n      },\n      \"required\": [\n        \"baz\"\n      ],\n      \"title\": \"bar\",\n      \"type\": \"object\"\n    },\n    \"foo\": {\n      \"default\": \"bar\",\n      \"description\": \"from ref\",\n      \"title\": \"foo\",\n      \"type\": \"string\"\n    },\n    \"global\": {\n      \"description\": \"Global values are values that can be accessed from any chart or subchart by exactly the same name.\",\n      \"required\": [],\n      \"title\": \"global\",\n      \"type\": \"object\"\n    }\n  },\n  \"required\": [],\n  \"type\": \"object\"\n}"
  },
  {
    "path": "tests/charts/test_ref_properties.yaml",
    "content": "# @schema\n# definitions:\n#   port:\n#     type: integer\n#     minimum: 1\n#     maximum: 65535\n# properties:\n#   httpPort:\n#     $ref: \"#/definitions/port\"\n#   httpsPort:\n#     $ref: \"#/definitions/port\"\n# @schema\nworking:\n  httpPort: 80\n  httpsPort: 443\n"
  },
  {
    "path": "tests/charts/test_ref_properties_expected.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"additionalProperties\": false,\n  \"definitions\": {\n    \"port\": {\n      \"maximum\": 65535,\n      \"minimum\": 1,\n      \"type\": \"integer\"\n    }\n  },\n  \"properties\": {\n    \"global\": {\n      \"description\": \"Global values are values that can be accessed from any chart or subchart by exactly the same name.\",\n      \"required\": [],\n      \"title\": \"global\",\n      \"type\": \"object\"\n    },\n    \"working\": {\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"httpPort\": {\n          \"$ref\": \"#/definitions/port\",\n          \"required\": []\n        },\n        \"httpsPort\": {\n          \"$ref\": \"#/definitions/port\",\n          \"required\": []\n        }\n      },\n      \"required\": [],\n      \"title\": \"working\"\n    }\n  },\n  \"required\": [],\n  \"type\": \"object\"\n}"
  },
  {
    "path": "tests/charts/test_ref_toplevel.yaml",
    "content": "# @schema\n# definitions:\n#   toplevel:\n#     description: \"Top Level\"\n# $ref: \"#/definitions/toplevel\"\n# @schema\ntoplevel: \n"
  },
  {
    "path": "tests/charts/test_ref_toplevel_expected.schema.json",
    "content": "{\n\t\"$schema\": \"http://json-schema.org/draft-07/schema#\",\n\t\"additionalProperties\": false,\n\t\"definitions\": {\n\t\t\"toplevel\": {\n\t\t\t\"description\": \"Top Level\",\n\t\t\t\"required\": []\n\t\t}\n\t},\n\t\"properties\": {\n\t\t\"toplevel\": {\n\t\t\t\"$ref\": \"#/definitions/toplevel\",\n\t\t\t\"required\": []\n\t\t},\n\t\t\"global\": {\n\t\t\t\"description\": \"Global values are values that can be accessed from any chart or subchart by exactly the same name.\",\n\t\t\t\"required\": [],\n\t\t\t\"title\": \"global\",\n\t\t\t\"type\": \"object\"\n\t\t}\n\t},\n\t\"required\": [],\n\t\"type\": \"object\"\n}\n"
  },
  {
    "path": "tests/charts/test_simple.yaml",
    "content": "---\n# @schema\n# description: foo\n# @schema\nfoo: bar\n"
  },
  {
    "path": "tests/charts/test_simple_expected.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"foo\": {\n      \"default\": \"bar\",\n      \"description\": \"foo\",\n      \"required\": [],\n      \"title\": \"foo\"\n    },\n    \"global\": {\n      \"description\": \"Global values are values that can be accessed from any chart or subchart by exactly the same name.\",\n      \"required\": [],\n      \"title\": \"global\",\n      \"type\": \"object\"\n    }\n  },\n  \"required\": [],\n  \"type\": \"object\"\n}"
  },
  {
    "path": "tests/import-values/child/Chart.yaml",
    "content": "apiVersion: v2\nname: child\nversion: 1.0.0\ndescription: A child chart with exports\n"
  },
  {
    "path": "tests/import-values/child/values.yaml",
    "content": "# @schema\n# type: object\n# @schema\nexports:\n  # @schema\n  # type: object\n  # @schema\n  defaults:\n    # @schema\n    # type: string\n    # description: The database host\n    # @schema\n    dbHost: localhost\n    # @schema\n    # type: integer\n    # description: The database port\n    # @schema\n    dbPort: 5432\n    # @schema\n    # type: object\n    # description: Extra configuration options\n    # additionalProperties: true\n    # @schema\n    extraConfig: {}\n\n# @schema\n# type: string\n# description: Internal config not exported\n# @schema\ninternalConfig: secret\n"
  },
  {
    "path": "tests/import-values/child-complex/Chart.yaml",
    "content": "apiVersion: v2\nname: child-complex\nversion: 1.0.0\ndescription: A child chart with nested data for complex import-values\n"
  },
  {
    "path": "tests/import-values/child-complex/values.yaml",
    "content": "# @schema\n# type: object\n# @schema\ndata:\n  # @schema\n  # type: object\n  # @schema\n  database:\n    # @schema\n    # type: string\n    # description: Database connection string\n    # @schema\n    connectionString: \"postgres://localhost:5432/db\"\n    # @schema\n    # type: integer\n    # description: Max connections\n    # @schema\n    maxConnections: 100\n"
  },
  {
    "path": "tests/import-values/parent/Chart.yaml",
    "content": "apiVersion: v2\nname: parent\nversion: 1.0.0\ndescription: A parent chart using import-values\ndependencies:\n  - name: child\n    version: 1.0.0\n    repository: file://../child\n    import-values:\n      - defaults\n"
  },
  {
    "path": "tests/import-values/parent/values.schema.expected.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"appName\": {\n      \"default\": \"myapp\",\n      \"description\": \"Application name\",\n      \"title\": \"appName\",\n      \"type\": \"string\"\n    },\n    \"dbHost\": {\n      \"default\": \"localhost\",\n      \"description\": \"The database host\",\n      \"title\": \"dbHost\",\n      \"type\": \"string\"\n    },\n    \"dbPort\": {\n      \"default\": 5432,\n      \"description\": \"The database port\",\n      \"title\": \"dbPort\",\n      \"type\": \"integer\"\n    },\n    \"extraConfig\": {\n      \"additionalProperties\": true,\n      \"description\": \"Extra configuration options\",\n      \"required\": [],\n      \"title\": \"extraConfig\",\n      \"type\": \"object\"\n    },\n    \"global\": {\n      \"description\": \"Global values are values that can be accessed from any chart or subchart by exactly the same name.\",\n      \"required\": [],\n      \"title\": \"global\",\n      \"type\": \"object\"\n    }\n  },\n  \"required\": [\n    \"extraConfig\"\n  ],\n  \"type\": \"object\"\n}\n"
  },
  {
    "path": "tests/import-values/parent/values.yaml",
    "content": "# @schema\n# type: string\n# description: Application name\n# @schema\nappName: myapp\n\n# No @schema annotation - inferred schema should be replaced by child's explicit annotation\nextraConfig:\n  someDefault: value\n"
  },
  {
    "path": "tests/import-values/parent-complex/Chart.yaml",
    "content": "apiVersion: v2\nname: parent-complex\nversion: 1.0.0\ndescription: A parent chart using complex import-values form\ndependencies:\n  - name: child-complex\n    version: 1.0.0\n    repository: file://../child-complex\n    import-values:\n      - child: data.database\n        parent: db\n"
  },
  {
    "path": "tests/import-values/parent-complex/values.schema.expected.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"db\": {\n      \"title\": \"db\",\n      \"properties\": {\n        \"connectionString\": {\n          \"default\": \"postgres://localhost:5432/db\",\n          \"description\": \"Database connection string\",\n          \"title\": \"connectionString\",\n          \"type\": \"string\"\n        },\n        \"maxConnections\": {\n          \"default\": 100,\n          \"description\": \"Max connections\",\n          \"title\": \"maxConnections\",\n          \"type\": \"integer\"\n        }\n      },\n      \"required\": [],\n      \"type\": \"object\"\n    },\n    \"environment\": {\n      \"default\": \"production\",\n      \"description\": \"Application environment\",\n      \"title\": \"environment\",\n      \"type\": \"string\"\n    },\n    \"global\": {\n      \"description\": \"Global values are values that can be accessed from any chart or subchart by exactly the same name.\",\n      \"required\": [],\n      \"title\": \"global\",\n      \"type\": \"object\"\n    }\n  },\n  \"required\": [],\n  \"type\": \"object\"\n}\n"
  },
  {
    "path": "tests/import-values/parent-complex/values.yaml",
    "content": "# @schema\n# type: string\n# description: Application environment\n# @schema\nenvironment: production\n"
  },
  {
    "path": "tests/preexisting-schema/dep-with-schema/Chart.yaml",
    "content": "apiVersion: v2\nname: dep-with-schema\nversion: 1.0.0\ndescription: A dependency chart with its own schema\n"
  },
  {
    "path": "tests/preexisting-schema/dep-with-schema/values.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"port\": {\n      \"type\": \"integer\",\n      \"description\": \"The port to listen on\",\n      \"minimum\": 1,\n      \"maximum\": 65535\n    },\n    \"host\": {\n      \"type\": \"string\",\n      \"description\": \"The hostname to bind to\",\n      \"format\": \"hostname\",\n      \"x-custom-annotation\": \"preserve-me\"\n    }\n  },\n  \"required\": [\n    \"host\",\n    \"port\"\n  ]\n}\n"
  },
  {
    "path": "tests/preexisting-schema/dep-with-schema/values.yaml",
    "content": "port: 8080\nhost: localhost\n"
  },
  {
    "path": "tests/preexisting-schema/parent/Chart.yaml",
    "content": "apiVersion: v2\nname: parent\nversion: 1.0.0\ndescription: A parent chart depending on dep-with-schema\ndependencies:\n  - name: dep-with-schema\n    version: 1.0.0\n    repository: file://../dep-with-schema\n"
  },
  {
    "path": "tests/preexisting-schema/parent/values.schema.default-expected.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"appName\": {\n      \"default\": \"myapp\",\n      \"title\": \"appName\",\n      \"type\": \"string\"\n    },\n    \"dep-with-schema\": {\n      \"description\": \"A dependency chart with its own schema\",\n      \"properties\": {\n        \"global\": {\n          \"description\": \"Global values are values that can be accessed from any chart or subchart by exactly the same name.\",\n          \"required\": [],\n          \"title\": \"global\",\n          \"type\": \"object\"\n        },\n        \"host\": {\n          \"default\": \"localhost\",\n          \"title\": \"host\",\n          \"type\": \"string\"\n        },\n        \"port\": {\n          \"default\": 8080,\n          \"title\": \"port\",\n          \"type\": \"integer\"\n        }\n      },\n      \"required\": [],\n      \"title\": \"dep-with-schema\",\n      \"type\": \"object\"\n    },\n    \"global\": {\n      \"description\": \"Global values are values that can be accessed from any chart or subchart by exactly the same name.\",\n      \"required\": [],\n      \"title\": \"global\",\n      \"type\": \"object\"\n    }\n  },\n  \"required\": [\n    \"appName\"\n  ],\n  \"type\": \"object\"\n}\n"
  },
  {
    "path": "tests/preexisting-schema/parent/values.schema.expected.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"appName\": {\n      \"default\": \"myapp\",\n      \"title\": \"appName\",\n      \"type\": \"string\"\n    },\n    \"dep-with-schema\": {\n      \"description\": \"A dependency chart with its own schema\",\n      \"properties\": {\n        \"host\": {\n          \"description\": \"The hostname to bind to\",\n          \"format\": \"hostname\",\n          \"type\": \"string\",\n          \"x-custom-annotation\": \"preserve-me\"\n        },\n        \"port\": {\n          \"description\": \"The port to listen on\",\n          \"maximum\": 65535,\n          \"minimum\": 1,\n          \"type\": \"integer\"\n        }\n      },\n      \"required\": [],\n      \"title\": \"dep-with-schema\",\n      \"type\": \"object\"\n    },\n    \"global\": {\n      \"description\": \"Global values are values that can be accessed from any chart or subchart by exactly the same name.\",\n      \"required\": [],\n      \"title\": \"global\",\n      \"type\": \"object\"\n    }\n  },\n  \"required\": [\n    \"appName\"\n  ],\n  \"type\": \"object\"\n}\n"
  },
  {
    "path": "tests/preexisting-schema/parent/values.yaml",
    "content": "appName: myapp\n"
  },
  {
    "path": "tests/run.sh",
    "content": "#!/usr/bin/env bash\n\nrc=0\n\ncd charts\n\ncp ../../examples/values.yaml test_repo_example.yaml\ncp ../../examples/values.schema.json test_repo_example_expected.schema.json\n\nfor test_file in test_*.yaml; do\n\t# Skip annotate test files from normal schema generation tests\n\tcase \"$test_file\" in\n\t\ttest_annotate_*) continue ;;\n\tesac\n\n\texpected_file=\"${test_file%.yaml}_expected.schema.json\"\n\tgenerated_file=\"${test_file%.yaml}_generated.schema.json\"\n\tif ! ../helm-schema -f \"$test_file\" -o \"$generated_file\"; then\n\t\techo \"❌: $test_file\"\n\t\trc=1\n\t\tcontinue\n\tfi\n\techo \"Testing $test_file\"\n\tif diff -y --suppress-common-lines <(jq --sort-keys . \"$generated_file\") <(jq --sort-keys . \"$expected_file\"); then\n\t\techo \"✅: $test_file\"\n\telse\n\t\techo \"❌: $test_file\"\n\t\trc=1\n\tfi\ndone\n\n# Annotate test\necho \"Testing annotate mode\"\nannotate_output=$(../helm-schema --annotate -d -f test_annotate_input.yaml 2>/dev/null)\nif diff -y --suppress-common-lines <(echo \"$annotate_output\") test_annotate_expected.yaml; then\n\techo \"✅: annotate mode\"\nelse\n\techo \"❌: annotate mode\"\n\trc=1\nfi\n\n# Import-values tests (in separate directory to avoid interference with single-file tests)\necho \"Testing import-values (simple form)\"\n../helm-schema -c ../import-values >/dev/null 2>&1\nif diff -y --suppress-common-lines <(jq --sort-keys . ../import-values/parent/values.schema.json) <(jq --sort-keys . ../import-values/parent/values.schema.expected.json); then\n\techo \"✅: import-values (simple form)\"\nelse\n\techo \"❌: import-values (simple form)\"\n\trc=1\nfi\n\necho \"Testing import-values (complex form)\"\nif diff -y --suppress-common-lines <(jq --sort-keys . ../import-values/parent-complex/values.schema.json) <(jq --sort-keys . ../import-values/parent-complex/values.schema.expected.json); then\n\techo \"✅: import-values (complex form)\"\nelse\n\techo \"❌: import-values (complex form)\"\n\trc=1\nfi\n\nrm -f ../import-values/parent/values.schema.json ../import-values/child/values.schema.json\nrm -f ../import-values/parent-complex/values.schema.json ../import-values/child-complex/values.schema.json\n\n# Pre-existing schema test (opt-in via --keep-existing-dep-schemas)\necho \"Testing pre-existing dependency schema (opt-in)\"\ndep_schema_backup=$(mktemp)\ncp ../preexisting-schema/dep-with-schema/values.schema.json \"$dep_schema_backup\"\ndep_schema_before=$(cat \"$dep_schema_backup\")\n../helm-schema -c ../preexisting-schema --keep-existing-dep-schemas >/dev/null 2>&1\nif diff -y --suppress-common-lines <(jq --sort-keys . ../preexisting-schema/parent/values.schema.json) <(jq --sort-keys . ../preexisting-schema/parent/values.schema.expected.json); then\n\techo \"✅: pre-existing dependency schema (opt-in)\"\nelse\n\techo \"❌: pre-existing dependency schema (opt-in)\"\n\trc=1\nfi\n\ndep_schema_after=$(cat ../preexisting-schema/dep-with-schema/values.schema.json)\nif [ \"$dep_schema_before\" = \"$dep_schema_after\" ]; then\n\techo \"✅: dependency schema not overwritten (opt-in)\"\nelse\n\techo \"❌: dependency schema was overwritten (opt-in)\"\n\trc=1\nfi\n\nrm -f ../preexisting-schema/parent/values.schema.json\n\n# Default behavior: dependency schemas are regenerated (issue #215 regression guard)\necho \"Testing dependency schema regeneration (default)\"\n../helm-schema -c ../preexisting-schema >/dev/null 2>&1\nif diff -y --suppress-common-lines <(jq --sort-keys . ../preexisting-schema/parent/values.schema.json) <(jq --sort-keys . ../preexisting-schema/parent/values.schema.default-expected.json); then\n\techo \"✅: dependency schema regeneration (parent)\"\nelse\n\techo \"❌: dependency schema regeneration (parent)\"\n\trc=1\nfi\n\ndep_schema_after_default=$(cat ../preexisting-schema/dep-with-schema/values.schema.json)\nif [ \"$dep_schema_before\" != \"$dep_schema_after_default\" ]; then\n\techo \"✅: dependency schema regenerated (default)\"\nelse\n\techo \"❌: dependency schema was NOT regenerated (default)\"\n\trc=1\nfi\n\n# Restore the original pre-existing dep schema so the fixture is stable across runs\ncp \"$dep_schema_backup\" ../preexisting-schema/dep-with-schema/values.schema.json\nrm -f \"$dep_schema_backup\"\nrm -f ../preexisting-schema/parent/values.schema.json\n\nexit \"$rc\"\n"
  },
  {
    "path": "tests/test-sign-plugin.sh",
    "content": "#!/bin/bash\n# Test script for sign-plugin.sh\n# Creates an isolated GPG environment, generates a test key, and validates signing\n\nset -euo pipefail\n\nTEST_DIR=$(mktemp -d)\ntrap 'rm -rf \"$TEST_DIR\"' EXIT\n\necho \"=== Setting up isolated test environment in $TEST_DIR ===\"\n\n# Create isolated GPG home\nexport GNUPGHOME=\"$TEST_DIR/gnupg\"\nmkdir -p \"$GNUPGHOME\"\nchmod 700 \"$GNUPGHOME\"\n\n# Configure GPG for non-interactive use\ncat > \"$GNUPGHOME/gpg.conf\" <<EOF\nno-tty\nbatch\npinentry-mode loopback\nEOF\n\ncat > \"$GNUPGHOME/gpg-agent.conf\" <<EOF\nallow-loopback-pinentry\nEOF\n\n# Restart gpg-agent with new config\ngpgconf --kill gpg-agent 2>/dev/null || true\n\necho \"=== Generating test Ed25519 key ===\"\n# Generate an Ed25519 key (the type that was causing issues)\ngpg --batch --yes --passphrase \"testpass\" --quick-gen-key \"Test User <test@example.com>\" ed25519 sign 0\n\n# Get the key fingerprint\nKEY_FPR=$(gpg --list-keys --with-colons | grep fpr | head -1 | cut -d: -f10)\necho \"Generated key: $KEY_FPR\"\n\n# Export to legacy format\ngpg --export > \"$GNUPGHOME/pubring.gpg\"\n\necho \"=== Creating mock plugin tarball ===\"\nPLUGIN_DIR=\"$TEST_DIR/plugin\"\nmkdir -p \"$PLUGIN_DIR/bin\"\n\ncat > \"$PLUGIN_DIR/plugin.yaml\" <<EOF\n---\nname: \"test-plugin\"\nversion: \"1.0.0\"\nusage: \"test plugin\"\ndescription: \"A test plugin for signing verification\"\ncommand: \"\\$HELM_PLUGIN_DIR/bin/test-plugin\"\nEOF\n\ncat > \"$PLUGIN_DIR/bin/test-plugin\" <<'EOF'\n#!/bin/bash\necho \"Hello from test plugin\"\nEOF\nchmod +x \"$PLUGIN_DIR/bin/test-plugin\"\n\n# Create tarball\nTARBALL=\"$TEST_DIR/test-plugin_1.0.0_Linux_x86_64.tar.gz\"\ntar -czf \"$TARBALL\" -C \"$PLUGIN_DIR\" .\n\necho \"=== Running sign-plugin.sh ===\"\nexport GPG_KEYRING=\"$GNUPGHOME/pubring.gpg\"\nexport GPG_PASSPHRASE=\"testpass\"\n\n# Copy the sign-plugin.sh to test dir (assuming it's in current directory or provided)\nif [ -f \"sign-plugin.sh\" ]; then\n    cp sign-plugin.sh \"$TEST_DIR/\"\nelif [ -f \"$1\" ]; then\n    cp \"$1\" \"$TEST_DIR/sign-plugin.sh\"\nelse\n    echo \"Error: sign-plugin.sh not found. Provide path as argument.\"\n    exit 1\nfi\n\nchmod +x \"$TEST_DIR/sign-plugin.sh\"\n\n# Run the signing script\ncd \"$TEST_DIR\"\n./sign-plugin.sh \"1.0.0\" \"$TARBALL\" \"test@example.com\"\n\necho \"\"\necho \"=== Checking generated .prov file ===\"\nPROV_FILE=\"${TARBALL}.prov\"\n\nif [ ! -f \"$PROV_FILE\" ]; then\n    echo \"FAIL: .prov file not created\"\n    exit 1\nfi\n\necho \"Contents of .prov file:\"\necho \"---\"\ncat \"$PROV_FILE\"\necho \"---\"\n\necho \"\"\necho \"=== Verifying signature with GPG ===\"\nif gpg --verify \"$PROV_FILE\" 2>&1; then\n    echo \"\"\n    echo \"SUCCESS: GPG verification passed\"\nelse\n    echo \"\"\n    echo \"FAIL: GPG verification failed\"\n    exit 1\nfi\necho \"=== Verifying plugin with Helm ===\"\nif helm plugin verify \"$TARBALL\" 2>&1; then\n    echo \"\"\n    echo \"SUCCESS: Helm plugin verification passed\"\nelse\n    echo \"\"\n    echo \"FAIL: Helm plugin verification failed\"\n    exit 1\nfi\n\necho \"\"\necho \"=== Checking signature packet details ===\"\nsed -n '/-----BEGIN PGP SIGNATURE-----/,/-----END PGP SIGNATURE-----/p' \"$PROV_FILE\" | gpg --list-packets 2>&1 | grep -E \"(algo|digest)\" || true\n\necho \"\"\necho \"=== All tests passed ===\""
  }
]