From 21e92aa00ff804077b03bcd898f4b7db8d72a6a0 Mon Sep 17 00:00:00 2001 From: jjuliano <439095+jjuliano@users.noreply.github.com> Date: Fri, 2 Jan 2026 14:39:53 +0000 Subject: [PATCH] chore: auto-update pkl to 0.30.2 and pkl-go to 0.12.1 --- assets/pkl/APIServer.pkl | 2 +- assets/pkl/APIServerRequest.pkl | 2 +- assets/pkl/APIServerResponse.pkl | 2 +- assets/pkl/Data.pkl | 2 +- assets/pkl/Docker.pkl | 2 +- assets/pkl/Document.pkl | 2 +- assets/pkl/Exec.pkl | 2 +- assets/pkl/HTTP.pkl | 2 +- assets/pkl/Item.pkl | 2 +- assets/pkl/Kdeps.pkl | 2 +- assets/pkl/LLM.pkl | 2 +- assets/pkl/Memory.pkl | 2 +- assets/pkl/Project.pkl | 2 +- assets/pkl/Python.pkl | 2 +- assets/pkl/Resource.pkl | 2 +- assets/pkl/Session.pkl | 2 +- assets/pkl/Skip.pkl | 2 +- assets/pkl/Tool.pkl | 2 +- assets/pkl/Utils.pkl | 2 +- assets/pkl/WebServer.pkl | 2 +- assets/pkl/Workflow.pkl | 2 +- assets/pkl/external/pkl-go/codegen/src/go.pkl | 11 +- .../packages/com.circleci.v2/Config.pkl | 774 ++++++++++++- .../com.influxdata.telegraf/Telegraf.pkl | 240 +++- .../{configuration.pkl => Configuration.pkl} | 0 .../packages/k8s.contrib.crd/generate.pkl | 122 +- .../packages/k8s.contrib/convert.pkl | 300 ++++- .../org.apache.spark/PropertiesBase.pkl | 49 +- .../org.json_schema.contrib/generate.pkl | 122 +- .../internal/ModuleGenerator.pkl | 431 +++---- .../packages/org.openapis.v3/expressions.pkl | 94 +- .../packages/pkl.experimental.net/net.pkl | 1007 ++++++++--------- .../packages/pkl.experimental.net/u128.pkl | 200 ++-- .../ExpressionNode.pkl | 444 ++++---- .../pkl.experimental.syntax/ModuleNode.pkl | 284 ++--- .../pkl.experimental.syntax/TypeNode.pkl | 204 ++-- .../packages/pkl.experimental.uri/URI.pkl | 520 ++++++--- .../pkl-pantry/packages/pkl.table/table.pkl | 359 ++++-- .../pkl-pantry/packages/pkl.toml/toml.pkl | 215 +++- build.gradle.kts | 2 +- deps/pkl/APIServer.pkl | 2 +- deps/pkl/APIServerRequest.pkl | 2 +- deps/pkl/APIServerResponse.pkl | 2 +- deps/pkl/Data.pkl | 2 +- deps/pkl/Docker.pkl | 2 +- deps/pkl/Document.pkl | 2 +- deps/pkl/Exec.pkl | 2 +- deps/pkl/HTTP.pkl | 2 +- deps/pkl/Item.pkl | 2 +- deps/pkl/Kdeps.pkl | 2 +- deps/pkl/LLM.pkl | 2 +- deps/pkl/Memory.pkl | 2 +- deps/pkl/Project.pkl | 2 +- deps/pkl/Python.pkl | 2 +- deps/pkl/Resource.pkl | 2 +- deps/pkl/Session.pkl | 2 +- deps/pkl/Skip.pkl | 2 +- deps/pkl/Tool.pkl | 2 +- deps/pkl/Utils.pkl | 2 +- deps/pkl/WebServer.pkl | 2 +- deps/pkl/Workflow.pkl | 2 +- go.mod | 32 +- go.sum | 10 +- pr_body.md | 39 + versions.json | 6 +- 65 files changed, 3570 insertions(+), 1979 deletions(-) rename assets/pkl/external/pkl-pantry/packages/io.prometheus/{configuration.pkl => Configuration.pkl} (100%) create mode 100644 pr_body.md diff --git a/assets/pkl/APIServer.pkl b/assets/pkl/APIServer.pkl index ebcb37f..a7edf1b 100644 --- a/assets/pkl/APIServer.pkl +++ b/assets/pkl/APIServer.pkl @@ -4,7 +4,7 @@ /// server settings such as host IP and port number, as well as route definitions. The API server /// is designed to handle incoming requests and route them to the appropriate handlers, ensuring /// proper management of HTTP methods and deferred processing. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/api_server" } diff --git a/assets/pkl/APIServerRequest.pkl b/assets/pkl/APIServerRequest.pkl index 360525f..8646dd4 100644 --- a/assets/pkl/APIServerRequest.pkl +++ b/assets/pkl/APIServerRequest.pkl @@ -9,7 +9,7 @@ /// - Functions to decode Base64 encoded request data. /// - File management utilities like retrieving file types and paths. /// - Filtering files by MIME type. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/api_server_request" } diff --git a/assets/pkl/APIServerResponse.pkl b/assets/pkl/APIServerResponse.pkl index aa0bd2a..2179c0c 100644 --- a/assets/pkl/APIServerResponse.pkl +++ b/assets/pkl/APIServerResponse.pkl @@ -13,7 +13,7 @@ /// - [success]: A flag indicating the success or failure of the API request. /// - [file]: A URI pointing to any file returned by the server in the response. /// - [errors]: The error block containing details of the error if the request was unsuccessful. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/api_server_response" } diff --git a/assets/pkl/Data.pkl b/assets/pkl/Data.pkl index 4084e96..6c037a5 100644 --- a/assets/pkl/Data.pkl +++ b/assets/pkl/Data.pkl @@ -1,5 +1,5 @@ /// Abstractions for Data folder -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/data" } diff --git a/assets/pkl/Docker.pkl b/assets/pkl/Docker.pkl index 9458672..909480e 100644 --- a/assets/pkl/Docker.pkl +++ b/assets/pkl/Docker.pkl @@ -2,7 +2,7 @@ /// resources within the KDEPS framework. It allows for the specification /// of package management, including additional package repositories (PPAs) /// and models to be used within Docker containers. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/docker" } diff --git a/assets/pkl/Document.pkl b/assets/pkl/Document.pkl index 758ddeb..73d367e 100644 --- a/assets/pkl/Document.pkl +++ b/assets/pkl/Document.pkl @@ -1,7 +1,7 @@ /// Common parser and document renderer functions used across all resources. /// /// Tools for Parsing and Generating JSON, YAML and XML documents -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/document" } diff --git a/assets/pkl/Exec.pkl b/assets/pkl/Exec.pkl index e9d411d..4f080bb 100644 --- a/assets/pkl/Exec.pkl +++ b/assets/pkl/Exec.pkl @@ -3,7 +3,7 @@ /// standard output and error, as well as handling environment variables and /// exit codes. The module provides functionalities to retrieve and manage /// executable resources based on their identifiers. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/exec" } diff --git a/assets/pkl/HTTP.pkl b/assets/pkl/HTTP.pkl index 84b677d..9444e32 100644 --- a/assets/pkl/HTTP.pkl +++ b/assets/pkl/HTTP.pkl @@ -3,7 +3,7 @@ /// HTTP requests, including method specifications, request data, headers, /// and handling of responses. This module provides functionalities to /// retrieve and manage HTTP client resources based on their identifiers. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/http" } diff --git a/assets/pkl/Item.pkl b/assets/pkl/Item.pkl index a1dd53b..200456e 100644 --- a/assets/pkl/Item.pkl +++ b/assets/pkl/Item.pkl @@ -2,7 +2,7 @@ /// /// This module provides functions to interact with records representing iterations or elements in a for loop. /// The module supports retrieving, navigating, and listing records without requiring a specific identifier. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/item" } diff --git a/assets/pkl/Kdeps.pkl b/assets/pkl/Kdeps.pkl index 89e4a38..b41d3dd 100644 --- a/assets/pkl/Kdeps.pkl +++ b/assets/pkl/Kdeps.pkl @@ -1,5 +1,5 @@ /// Abstractions for Kdeps Configuration -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/kdeps" } diff --git a/assets/pkl/LLM.pkl b/assets/pkl/LLM.pkl index 62c2d17..67a3990 100644 --- a/assets/pkl/LLM.pkl +++ b/assets/pkl/LLM.pkl @@ -13,7 +13,7 @@ /// - Providing methods to retrieve various pieces of information related to the LLM interaction, /// such as the prompt text, response text, file paths, JSON keys, and whether image generation was /// involved. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/llm" } diff --git a/assets/pkl/Memory.pkl b/assets/pkl/Memory.pkl index 0413af1..53ebed7 100644 --- a/assets/pkl/Memory.pkl +++ b/assets/pkl/Memory.pkl @@ -1,5 +1,5 @@ /// Abstractions for Memory records -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/memory" } diff --git a/assets/pkl/Project.pkl b/assets/pkl/Project.pkl index 75bedff..2c02d57 100644 --- a/assets/pkl/Project.pkl +++ b/assets/pkl/Project.pkl @@ -4,7 +4,7 @@ /// configurations related to the API server, Docker agent settings, and security settings. These /// settings allow customization of the project's environment, such as enabling API server mode or /// configuring Docker and security parameters. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/project" } diff --git a/assets/pkl/Python.pkl b/assets/pkl/Python.pkl index 565b851..ccf4b44 100644 --- a/assets/pkl/Python.pkl +++ b/assets/pkl/Python.pkl @@ -3,7 +3,7 @@ /// capturing their standard output, standard error, and handling environment /// variables as well as exit codes. The module provides utilities for retrieving /// and managing executable resources identified by unique resource actionIDs. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/python" } diff --git a/assets/pkl/Resource.pkl b/assets/pkl/Resource.pkl index be61713..bba408a 100644 --- a/assets/pkl/Resource.pkl +++ b/assets/pkl/Resource.pkl @@ -4,7 +4,7 @@ /// including actions that can be performed on these resources, validation checks, /// and error handling mechanisms. Each resource can define its actionID, name, description, /// category, dependencies, and how it runs. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/resource" } diff --git a/assets/pkl/Session.pkl b/assets/pkl/Session.pkl index b294108..7d2f48d 100644 --- a/assets/pkl/Session.pkl +++ b/assets/pkl/Session.pkl @@ -1,5 +1,5 @@ /// Abstractions for Session records -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/session" } diff --git a/assets/pkl/Skip.pkl b/assets/pkl/Skip.pkl index 0793a92..01b8813 100644 --- a/assets/pkl/Skip.pkl +++ b/assets/pkl/Skip.pkl @@ -1,7 +1,7 @@ /// Skip condition functions used across all resources. /// /// Tools for creating skip logic validations -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/skip" } diff --git a/assets/pkl/Tool.pkl b/assets/pkl/Tool.pkl index f8f0b0b..4d38e0e 100644 --- a/assets/pkl/Tool.pkl +++ b/assets/pkl/Tool.pkl @@ -1,5 +1,5 @@ /// Abstractions for Tool records -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/tool" } diff --git a/assets/pkl/Utils.pkl b/assets/pkl/Utils.pkl index 7afc2fb..f3f7dd5 100644 --- a/assets/pkl/Utils.pkl +++ b/assets/pkl/Utils.pkl @@ -1,7 +1,7 @@ /// Tools for Kdeps Resources /// /// This module includes tools for interacting with Kdeps -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/utils" } diff --git a/assets/pkl/WebServer.pkl b/assets/pkl/WebServer.pkl index d814440..d813a06 100644 --- a/assets/pkl/WebServer.pkl +++ b/assets/pkl/WebServer.pkl @@ -4,7 +4,7 @@ /// server binding details (host and port) and route configurations. The server /// handles HTTP requests, routing them to appropriate handlers based on defined /// paths and server types. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/web_server" } diff --git a/assets/pkl/Workflow.pkl b/assets/pkl/Workflow.pkl index cf3590e..9f211ec 100644 --- a/assets/pkl/Workflow.pkl +++ b/assets/pkl/Workflow.pkl @@ -7,7 +7,7 @@ /// /// This module also ensures the proper structure of workflows using validation checks for names, /// workflow references, action formats, and versioning patterns. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/workflow" } diff --git a/assets/pkl/external/pkl-go/codegen/src/go.pkl b/assets/pkl/external/pkl-go/codegen/src/go.pkl index 5fe0be8..a201407 100644 --- a/assets/pkl/external/pkl-go/codegen/src/go.pkl +++ b/assets/pkl/external/pkl-go/codegen/src/go.pkl @@ -21,10 +21,13 @@ import "internal/utils.pkl" local const isValidPackageName = (it: String) -> let (packageNameShort = it.substring(it.lastIndexOf("/") + 1, it.length)) if (utils.keywords.contains(packageNameShort)) - throw(""" + throw( + """ Package name `\(it)` is not valid because it clashes with keyword `\(packageNameShort)`. - """) - else true + """ + ) + else + true /// A name in Go; i.e., an identifier. typealias GoName = String(!utils.keywords.contains(this) && matches(Regex(#"(?u)\p{L}[\p{L}\d]+"#))) @@ -90,7 +93,7 @@ open class Field extends Annotation { /// /// `true` is a shortcut for adding a `json` tag whose value is the name of the annotated Pkl /// property. - json: (*JsonStructTag|Boolean)? + json: (*JsonStructTag | Boolean)? /// Additional struct tags for the annotated Pkl property. /// diff --git a/assets/pkl/external/pkl-pantry/packages/com.circleci.v2/Config.pkl b/assets/pkl/external/pkl-pantry/packages/com.circleci.v2/Config.pkl index ae5a460..9e18785 100644 --- a/assets/pkl/external/pkl-pantry/packages/com.circleci.v2/Config.pkl +++ b/assets/pkl/external/pkl-pantry/packages/com.circleci.v2/Config.pkl @@ -13,16 +13,776 @@ // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// -module com.circleci.v2.tests.Config +/// Schema for CircleCI's `config.yml`. +/// +/// This module is a work in progress, if you find some unsuported feature +/// feel free to add it. +module com.circleci.v2.Config -amends "pkl:test" +/// Version of the `config.yml` schema to target. +/// +/// Currently, only version 2.1 is supported by this template. +version: "2.1" -local allExamples = import*("../examples/*.pkl") +/// Designates the config.yaml for use of CircleCI’s +/// [dynamic configuration](https://circleci.com/docs/dynamic-config/) feature. +setup: Boolean? -examples { - for (example in allExamples.keys) { - [example.drop("../examples/".length).replaceLast("pkl", "yaml")] { - allExamples[example].output.text +/// A map of user-selected names to either: orb references (strings) or orb definitions (maps). +/// +/// Orb definitions must be the orb-relevant subset of 2.1 config. +/// See the [Creating Orbs](https://circleci.com/docs/creating-orbs/) documentation for details. +orbs: Mapping? + +/// Executors define the execution environment in which the steps of a job will be run, +/// allowing you to reuse a single executor definition across multiple jobs. +/// +/// See [executors in the CircleCI documentation](https://circleci.com/docs/configuration-reference/#executors). +/// Also see [Using Workspaces to Share Data between Jobs](https://circleci.com/docs/workspaces/) for information on +/// how to use executors to share state between jobs. +executors: Mapping? + +/// A command defines a sequence of steps as a map to be executed in a job, enabling you to reuse +/// a single command definition across multiple jobs. +/// +/// For more information see the +/// [Reusable Config Reference Guide](https://circleci.com/docs/reusing-config/). +commands: Mapping? + +/// A Workflow is comprised of one or more uniquely named jobs. +/// +/// Jobs are specified in the jobs map, see +/// [Sample config.yml](https://circleci.com/docs/sample-config/) for two examples of a job map. +/// The name of the job is the key in the map, and the value is a map describing the job. +jobs: Mapping? + +/// Used for orchestrating all jobs. +/// +/// Each workflow consists of the workflow name as a key and a map as a value. +/// A name should be unique within the current config.yml. +/// The top-level keys for the Workflows configuration are version and jobs. +/// For more information, see the +/// [Using Workflows to Orchestrate Jobs](https://circleci.com/docs/workflows/) page. +workflows: Mapping? + +/// Pipeline parameters declared for use in the configuration. +/// +/// See [Pipeline Values and Parameters](https://circleci.com/docs/pipeline-variables/#pipeline-parameters-in-configuration) +/// for usage details. +parameters: Mapping? + +class Orb { + /// A map of user-selected names to either: orb references (strings) or orb definitions (maps). + /// + /// Orb definitions must be the orb-relevant subset of 2.1 config. + /// See the [Creating Orbs](https://circleci.com/docs/creating-orbs/) documentation for details. + orbs: Mapping? + + /// A command defines a sequence of steps as a map to be executed in a job, enabling you to reuse + /// a single command definition across multiple jobs. + /// + /// For more information see the + /// [Reusable Config Reference Guide](https://circleci.com/docs/reusing-config/). + commands: Mapping? + + /// A Workflow is comprised of one or more uniquely named jobs. + /// + /// Jobs are specified in the jobs map, see + /// [Sample config.yml](https://circleci.com/docs/sample-config/) for two examples of a job map. + /// The name of the job is the key in the map, and the value is a map describing the job. + jobs: Mapping? +} + +class Executor { + /// Which resource class to use which further determines the amount of CPU and RAM allocated to each container in a + /// job. + resource_class: ResourceClass? + + /// Shell to use for execution command in all steps. + /// Can be overridden by shell in each step (default: See [Default Shell Options](https://circleci.com/docs/configuration-reference/#default-shell-options)) + shell: String? + + /// In which directory to run the steps. Will be interpreted as an absolute path. + working_directory: String? + + /// A map of environment variable names and values. + environment: Mapping? + + /// Options for [docker executor](https://circleci.com/docs/configuration-reference/#docker) + docker: Listing(!isEmpty)?(onlyOneSet(List(this, macos, machine))) + + /// Options for [macOS executor](https://circleci.com/docs/configuration-reference/#macos) + macos: MacOSExecutor? + + /// Options for [machine executor](https://circleci.com/docs/configuration-reference/#machine) + machine: Machine? +} + +class Job { + /// Shell to use for execution command in all steps. + /// + /// Can be overridden by shell in each step (default: See Default Shell Options) + shell: String? + + /// A list of [steps](https://circleci.com/docs/configuration-reference/#steps) to be performed + steps: Listing(!isEmpty) + + /// A pre-defined executor from the defined executors or orbs in which to execute this job. + /// + /// Can be used to share data between jobs. + /// See [Using Workspaces to Share Data between Jobs](https://circleci.com/docs/workspaces/) for information on how to + /// use executors to share state between jobs. + executor: String? + + /// In which directory to run the steps. + /// + /// Will be interpreted as an absolute path. + /// + /// Default: `~/project` (where project is a literal string, not the name of your specific project). + /// + /// Processes run during the job can use the `$CIRCLE_WORKING_DIRECTORY` environment variable to + /// refer to this directory. + /// + /// Note: Paths written in your YAML configuration file will not be expanded; if your + /// store_test_results.path is $CIRCLE_WORKING_DIRECTORY/tests, then CircleCI will attempt to + /// store the test subdirectory of the directory literally named $CIRCLE_WORKING_DIRECTORY, + /// dollar sign $ and all. working_directory will be created automatically if it doesn’t exist. + working_directory: String? + + /// Number of parallel instances of this job to run (default: 1) + parallelism: Int? + + /// A map of environment variable names and values. + environment: Mapping? + + /// The [resource_class](https://circleci.com/docs/resource-class-overview/) feature allows you to configure CPU and RAM resources for each job. + /// + /// Resource classes are available for each execution environment, as described in the tables below. + /// + /// We implement soft concurrency limits for each resource class to ensure our system remains + /// stable for all customers. + /// If you are on a Performance or custom plan and experience queuing for certain resource + /// classes, it is possible you are hitting these limits. + /// Contact [CircleCI support](https://support.circleci.com/hc/en-us/requests/new) to request a + /// raise on these limits for your account. + /// + /// If you do not specify a resource class, CircleCI will use a default value that is subject + /// to change. + /// It is best practice to specify a resource class as opposed to relying on a default. + resource_class: ResourceClass? + + /// Options for [docker executor](https://circleci.com/docs/configuration-reference/#docker) + docker: Listing(!isEmpty)?(onlyOneSet(List(this, executor, macos, machine))) + + /// CircleCI supports running jobs on macOS, to allow you to build, test, and deploy apps for + /// macOS, iOS, tvOS and watchOS. + /// + /// To run a job in a macOS virtual machine, add the macos key to the top-level configuration + /// for your job and specify the version of Xcode you would like to use. + macos: MacOSExecutor? + + /// Options for [machine executor](https://circleci.com/docs/configuration-reference/#machine) + machine: Machine? +} + +typealias ResourceClass = + "small" + |"medium" + |"medium+" + |"large" + |"xlarge" + |"2xlarge" + |"2xlarge+" + |"arm.medium" + |"arm.large" + |"arm.xlarge" + |"arm.2xlarge" + |"macos.m1.medium.gen1" + |"macos.m1.large.gen1" + |"windows.medium" + |"windows.large" + |"windows.xlarge" + |"windows.2xlarge" + |"gpu.nvidia.small" + |"gpu.nvidia.small.gen2" + |"gpu.nvidia.small.multi" + |"gpu.nvidia.medium.multi" + |"gpu.nvidia.medium" + |"gpu.nvidia.large" + |"windows.gpu.nvidia.medium" + + +class MacOSExecutor { + /// The version of Xcode that is installed on the virtual machine, see the + /// [Supported Xcode Versions section of the Testing iOS document](https://circleci.com/docs/using-macos/#supported-xcode-versions) + /// for the complete list. + xcode: String +} + +class Machine { + /// The virtual machine image to use. + /// + /// View [available images](https://circleci.com/developer/images?imageType=machine). + /// + /// Note: This key is *not* supported for Linux VMs on installations of CircleCI server. + /// For information about customizing `machine` executor images on CircleCI installed on your + /// servers, see our [VM Service documentation](https://circleci.com/docs/server/v4.1/operator/manage-virtual-machines-with-vm-service/) + image: String + + /// Set this to `true` to enable [Docker Layer Caching](https://circleci.com/docs/docker-layer-caching/). + docker_layer_caching: Boolean? +} + +class DockerImage { + /// The name of a custom docker image to use. + /// + /// The first `image` listed under a job defines the job’s own primary container image where all + /// steps will run. + image: String + + /// `name` defines the the hostname for the container (the default is `localhost`), which is used + /// for reaching secondary (service) containers. + /// + /// By default, all services are exposed directly on `localhost`. + /// This field is useful if you would rather have a different hostname instead of `localhost`, + /// for example, if you are starting multiple versions of the same service. + name: String? + + /// The command used as executable when launching the container. + /// + /// [entrypoint] overrides the image’s `ENTRYPOINT` + entrypoint: (*Listing|String)? + + /// The command used as pid 1 (or args for entrypoint) when launching the container. + /// + /// [command] overrides the image’s `COMMAND`. + /// It will be used as arguments to the image `ENTRYPOINT` if it has one, or as the executable + /// if the image has no `ENTRYPOINT`. + command: (*Listing|String)? + + /// Which user to run commands as within the Docker container + user: String? + + /// environment variable names and values. + /// + /// The [environment] settings apply to the entrypoint/command run by the docker container, not + /// the job steps + environment: Mapping? + + /// Authentication for registries using standard docker login credentials + auth: Mapping? + + /// Authentication for AWS Elastic Container Registry (ECR) + aws_auth: Mapping? +} + +class ScheduleTrigger { + /// The cron key is defined using POSIX crontab syntax + @SourceCode { language = "cronexp" } + cron: String + + /// Trigger Filters can have the key branches. + filters: ScheduleTriggerFilter +} + +class ScheduleTriggerFilter { + /// The `branches` key controls whether the _current_ branch should have a schedule trigger + /// created for it, where _current_ branch is the branch containing the `config.yml` file with + /// the trigger stanza. + /// That is, a push on the `main` branch will only schedule a + /// [workflow](https://circleci.com/docs/workflows/#using-contexts-and-filtering-in-your-workflows) + /// for the `main` branch. + /// + /// Branches can have the keys only and ignore which each map to a single string naming a branch. + /// You may also use regular expressions to match against branches by enclosing them with `/`’s, + /// or map to a list of such strings. Regular expressions must match the **entire** string. + /// + /// * Any branches that match only will run the job. + /// * Any branches that match ignore will not run the job. + /// * If neither only nor ignore are specified then all branches will run the job. + /// If both only and ignore are specified, the only is used and ignore will have no effect. + branches: ScheduleTriggerFilterBranches +} + +class ScheduleTriggerFilterBranches { + /// Either a single branch specifier, or a list of branch specifiers + only: *Listing|String + + /// Either a single branch specifier, or a list of branch specifiers + ignore: (*Listing|String)? +} + +class Workflow { + /// A job can have the keys `requires`, `name`, `context`, `type`, and `filters`. + jobs: Listing<*Mapping(length == 1)|String>(!isEmpty) + + /// Specifies which triggers will cause this workflow to be executed. + /// + /// Default behavior is to trigger the workflow when pushing to a branch + triggers: Listing? + + `when`: (*LogicStatement|Boolean|String)? + + `unless`: (*LogicStatement|Boolean|String)? +} + +class WorkflowJob { + /// The name key can be used to invoke reusable jobs across any number of workflows. + /// + /// Using the name key ensures numbers are not appended to your job name (i.e. sayhello-1, + /// sayhello-2, etc.). + /// The name you assign to the name key needs to be unique, otherwise the numbers will still be + /// appended to the job name. + name: String? + + /// A list of jobs that must succeed for the job to start. + /// + /// Note: When jobs in the current workflow that are listed as dependencies are not executed + /// (due to a filter function for example), their requirement as a dependency for other jobs will + /// be ignored by the requires option. + /// However, if all dependencies of a job are filtered, then that job will not be executed either. + requires: Listing? + + /// The name of the context(s). + /// + /// Jobs may be configured to use global environment variables set for an organization, see the + /// [Contexts](https://circleci.com/docs/contexts/) document for adding a context in the + /// application settings. + /// + /// The initial default name is org-global. + /// Each context name must be unique. + /// If using CircleCI Server, only a single Context per workflow is supported. + /// Note: A maximum of 100 unique contexts across all workflows is allowed + context: (*Listing|String)? + + /// A job may have a type of `approval` indicating it must be manually approved before downstream + /// jobs may proceed. + /// + /// For more information see the Using Workflows to Orchestrate Jobs page. + type: "approval"? + + /// Job Filters can have the key branches or tags + filters: JobFilters? +} + +class JobFilters { + /// Branches can have the keys only and ignore which either map to a single string naming a branch. + /// You may also use regular expressions to match against branches by enclosing them with slashes, + /// or map to a list of such strings. + /// Regular expressions must match the *entire* string. + /// + /// Any branches that match `only` will run the job. + /// Any branches that match `ignore` will not run the job. + /// If neither `only` nor `ignore` are specified then all branches will run the job. + /// If both `only` and `ignore` are specified the `only` is considered before `ignore`. + branches: FilterSpec? + + /// CircleCI does not run workflows for tags unless you explicitly specify tag filters. + /// Additionally, if a job requires any other jobs (directly or indirectly), you must specify tag + /// filters for those jobs. + /// + /// Tags can have the keys only and ignore. + /// You may also use regular expressions to match against tags by enclosing them with slashes, + /// or map to a list of such strings. + /// Regular expressions must match the entire string. + /// Both lightweight and annotated tags are supported. + /// + /// Any tags that match only will run the job. + /// Any tags that match ignore will not run the job. + /// If neither only nor ignore are specified then the job is skipped for all tags. + /// If both only and ignore are specified the only is considered before ignore. + tags: FilterSpec? +} + +class FilterSpec { + /// Either a single branch specifier, or a list of branch specifiers + only: (String|Listing)?(this != null || ignore != null) + + /// Either a single branch specifier, or a list of branch specifiers + ignore: (String|Listing)? +} + +typealias Step = AbstractStep|SimpleStepName|OrbStep + +typealias SimpleStepName = "checkout"|"setup_remote_docker"|"add_ssh_keyes"|String + +abstract class AbstractStep { + fixed hidden __name__: String +} + +typealias OrbStep = Dynamic(hasProperty("__name__")) + +function OrbStep(name: String): OrbStep = new { + __name__ = name +} + +function run(_command: String): RunStep = new { + command = _command +} + +/// Used for invoking all command-line programs. +/// +/// Run commands are executed using non-login shells by default, so you must explicitly source any +/// dotfiles as part of the command. +class RunStep extends AbstractStep { + fixed hidden __name__ = "run" + + /// Command to run via the shell + command: String(!isEmpty) + + /// Title of the step to be shown in the CircleCI UI (default: full [command]) + name: String? + + /// Shell to use for execution command (default: See [Default Shell Options](https://circleci.com/docs/configuration-reference/#default-shell-options)) + shell: String? + + /// Additional environmental variables, locally scoped to command + environment: Mapping? + + /// Whether or not this step should run in the background (default: [false]) + background: Boolean? + + /// In which directory to run this step. + /// + /// Will be interpreted relative to the working_directory + /// of the job. (default: `.`) + working_directory: String? + + /// Elapsed time the command can run without output. + /// + /// The default is 10 minutes and the maximum is governed by the maximum time a job is allowed to run. + no_output_timeout: Duration? + + /// Specify when to enable or disable the step. + /// + /// Takes the following values: `always`, `on_success`, `on_fail` (default: on_success) + `when`: ("always"|"on_success"|"on_fail")? +} + +/// Generates and stores a cache of a file or directory of files such as dependencies or source +/// code in our object storage. +/// +/// Later jobs can [restore this cache](https://circleci.com/docs/configuration-reference/#restore_cache). +/// Learn more on the [Caching Dependencies](https://circleci.com/docs/caching/) page. +/// +/// Cache retention can be customized on the [CircleCI web app](https://app.circleci.com/) by +/// navigating to Plan > Usage Controls. +class SaveCacheStep extends AbstractStep { + fixed hidden __name__ = "save_cache" + + /// List of directories which should be added to the cache + paths: Listing + + /// Unique identifier for this cache + key: String + + /// Title of the step to be shown in the CircleCI UI (default: “Saving Cache”) + name: String? + + /// [Specify when to enable or disable the step](https://circleci.com/docs/configuration-reference/#the-when-attribute). + /// + /// Takes the following values: always, on_success, on_fail (default: on_success) + `when`: ("always"|"on_success"|"on_fail")? +} + +/// Restores a previously saved cache based on a key. +/// +/// Cache needs to have been saved first for this key using the [save_cache] step. +/// +/// Learn more in [the caching documentation](https://circleci.com/docs/caching/). +class RestoreCacheStep extends AbstractStep { + fixed hidden __name__ = "restore_cache" + + /// Single cache key to restore + key: String?(this != null || keys != null) + + /// List of cache keys to lookup for a cache to restore. + /// + /// Only first existing key will be restored. + keys: Listing? + + /// Title of the step to be shown in the CircleCI UI (default: “Restoring Cache”) + name: String? +} + +/// Allows Docker commands to be run locally. +/// +/// See [Running Docker Commands](https://circleci.com/docs/building-docker-images/) for details. +class SetupRemoteDockerStep extends AbstractStep { + fixed hidden __name__ = "setup_remote_docker" + + /// Version string of Docker you would like to use (default: 20.10.17). + /// + /// View the list of supported docker versions + /// [here](https://circleci.com/docs/building-docker-images/#docker-version). + version: String + + /// Set this to true to enable [Docker Layer Caching](https://circleci.com/docs/docker-layer-caching/) + /// in the Remote Docker Environment (default: false) + docker_layer_cacheing: Boolean +} + +/// Special step used to persist a temporary file to be used by another job in the workflow. +/// For more information on using workspaces, see the +/// [Using Workspaces to Share Data Between Jobs](https://circleci.com/docs/workspaces/) page. +/// +/// `persist_to_workspace` adopts the storage settings from the storage customization controls on +/// the CircleCI web app. +/// If no custom setting is provided, `persist_to_workspace` defaults to 15 days. +/// +/// Workspace storage retention can be customized on the CircleCI web app by navigating to Plan > Usage Controls. +class PersistToWorkspaceStep extends AbstractStep { + fixed hidden __name__ = "persist_to_workspace" + + /// Either an absolute path or a path relative to `working_directory` + root: String + + /// Glob identifying file(s), or a non-glob path to a directory to add to the shared workspace. + /// + /// Interpreted as relative to the workspace root. + /// Must not be the workspace root itself + paths: Listing +} + +/// Special step used to attach the workflow’s workspace to the current container. +/// +/// The full contents of the workspace are downloaded and copied into the directory the workspace +/// is being attached at. +/// For more information on using workspaces, see the +/// [Using Workspaces to Share Data Between Jobs](https://circleci.com/docs/workspaces/) page. +class AttachWorkspaceStep extends AbstractStep { + fixed hidden __name__ = "attach_workspace" + + /// Directory to attach the workspace to. + at: String +} + +/// A conditional step consists of a step with the key `when`. +/// +/// Under the `when` key are the subkeys `condition` and `steps`. +/// The purpose of the `when` step is customizing commands and job configuration to run on custom +/// conditions (determined at config-compile time) that are checked before a workflow runs. +/// See the [Conditional Steps section of the Reusing Config](https://circleci.com/docs/reusing-config/#defining-conditional-steps) +/// document for more details. +class WhenStep { + fixed hidden __name__ = "when" + + /// The logic statement that determines whether to execute. + condition: (*LogicStatement|Boolean|String)? + + /// A list of steps to execute when the condition is true + steps: Listing? +} + +/// A conditional step consists of a step with the key `unless`. +/// +/// Under the `unless` key are the subkeys `condition` and `steps`. +/// The purpose of the `unless` step is customizing commands and job configuration to run on custom +/// conditions (determined at config-compile time) that are checked before a workflow runs. +/// See the [Conditional Steps section of the Reusing Config](https://circleci.com/docs/reusing-config/#defining-conditional-steps) +/// document for more details. +class UnlessStep { + fixed hidden __name__ = "unless" + + /// The logic statement that determines whether to execute. + condition: (*LogicStatement|Boolean|String)? + + /// A list of steps to execute when the condition is true + steps: Listing? +} + +class Command { + /// A sequence of steps run inside the calling job of the command. + steps: Listing(!isEmpty) + + /// A map of parameter keys. + /// + /// See the [Parameter Syntax](https://circleci.com/docs/reusing-config/#parameter-syntax) + /// section of the [Reusing Config](https://circleci.com/docs/reusing-config/) document for details. + parameters: Mapping? + + /// A string that describes the purpose of the command. + description: String? +} + +class Parameter { + /// Optional. Used to generate documentation for your orb. + description: String? + + /// The default value for the parameter. If not present, the parameter is implied to be required. + default: (String|Number|Boolean)? + + /// Required. See [Parameter Types](https://circleci.com/docs/reusing-config/#parameter-types) + /// for details. + type: "string"|"boolean"|"integer"|"enum"|"executor"|"steps"|"env_var_name" +} + +/// Special step used to upload and store test results for a build. +/// +/// Test results are visible on the CircleCI web application under each build’s *Test Summary* +/// section. +/// Storing test results is useful for timing analysis of your test suites. +/// For more information on storing test results, see the +/// [Collecting Test Data](https://circleci.com/docs/collect-test-data/) page. +/// +/// It is also possible to store test results as a build artifact; to do so, please refer to the +/// [store_artifacts] step. +class StoreTestResults extends AbstractStep { + fixed hidden __name__ = "store_test_results" + + /// Path (absolute, or relative to your `working_directory`) to directory containing JUnit XML + /// test metadata files, or to a single test file. + path: String +} + +/// Step to store artifacts (for example logs, binaries, etc) to be available in the web app +/// or through the API. +/// +/// See the [Uploading Artifacts](https://circleci.com/docs/artifacts/) document for more information. +class StoreArtifacts extends AbstractStep { + fixed hidden __name__ = "store_artifacts" + + /// Directory in the primary container to save as job artifacts + path: String + + /// Prefix added to the artifact paths in the artifacts API (default: the directory of the file + /// specified in path) + destination: String? +} + +/// Certain dynamic configuration features accept logic statements as arguments. +/// +/// Logic statements are evaluated to boolean values at configuration compilation time, that is, +/// before the workflow is run. +class LogicStatement { + /// True if all arguments are truthy. + and: Listing<*LogicStatement|Boolean|String>?(onlyOneSet(List(this, or, not, equal, matches))) + + /// True if any arguments are truthy. + or: Listing<*LogicStatement|Boolean|String>? + + /// True if the argument is not truthy. + not: (*LogicStatement|Boolean|String)? + + /// True if all arguments evaluate to equal values. + equal: Listing? + + /// True if [value][Match.value] matches the [pattern][Match.pattern]. + matches: Match? +} + +/// Tells if only one of the values is not null. +local const function onlyOneSet(values: List): Boolean = + values.filter((it) -> it != null).length == 1 + +class Match { + /// The value to match against + value: String + + /// A [Java regular expression](https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html) + /// used to test against the value. + /// + /// A full match pattern must be provided, prefix matching is not an option. + /// Though, it is recommended to enclose a pattern in `^` and `$` to avoid accidental partial matches. + @SourceCode { language = "RegExp" } + pattern: String +} + +typealias CalledCommand = Mapping(length == 1) + +typealias CommandCall = Mapping + +class PipelineValues { + /// A globally unique id representing for the pipeline. + /// + /// Type: string + `pipeline.id`: "<< pipeline.id >>" + + /// A project unique integer id for the pipeline. + /// + /// Type: integer + `pipeline.number`: "<< pipeline.number >>" + + /// The URL where the current project is hosted. + /// + /// For example, `https://github.com/circleci/circleci-docs`. + `pipeline.project.git_url`: "<< pipeline.project.git_url >>" + `pipeline.project.type`: "<< pipeline.project.type >>" + `pipeline.git.tag`: "<< pipeline.git.tag >>" + `pipeline.git.branch`: "<< pipeline.git.branch >>" + `pipeline.git.revision`: "<< pipeline.git.revision >>" + `pipeline.git.base_revision`: "<< pipeline.git.base_revision >>" + `pipeline.in_setup`: "<< pipeline.in_setup >>" + `pipeline.trigger_source`: "<< pipeline.trigger_source >>" + `pipeline.schedule.name`: "<< pipeline.schedule.name >>" + `pipeline.schedule.id`: "<< pipeline.schedule.id >>" + `pipeline.trigger_parameters.circleci.trigger_type`: "<< pipeline.trigger_parameters.circleci.trigger_type >>" + `pipeline.trigger_parameters.circleci.event_time`: "<< pipeline.trigger_parameters.circleci.event_time >>" + `pipeline.trigger_parameters.circleci.event_type`: "<< pipeline.trigger_parameters.circleci.event_type >>" + `pipeline.trigger_parameters.circleci.project_id`: "<< pipeline.trigger_parameters.circleci.project_id >>" + `pipeline.trigger_parameters.circleci.actor_id`: "<< pipeline.trigger_parameters.circleci.actor_id >>" + `pipeline.trigger_parameters.gitlab.type`: "<< pipeline.trigger_parameters.gitlab.type >>" + `pipeline.trigger_parameters.github_app.type`: "<< pipeline.trigger_parameters.github_app.type >>" + `pipeline.trigger_parameters.gitlab.project_id`: "<< pipeline.trigger_parameters.gitlab.project_id >>" + `pipeline.trigger_parameters.github_app.project_id`: "<< pipeline.trigger_parameters.github_app.project_id >>" + `pipeline.trigger_parameters.gitlab.ref`: "<< pipeline.trigger_parameters.gitlab.ref >>" + `pipeline.trigger_parameters.github_app.ref`: "<< pipeline.trigger_parameters.github_app.ref >>" + `pipeline.trigger_parameters.gitlab.checkout_sha`: "<< pipeline.trigger_parameters.gitlab.checkout_sha >>" + `pipeline.trigger_parameters.github_app.checkout_sha`: "<< pipeline.trigger_parameters.github_app.checkout_sha >>" + `pipeline.trigger_parameters.gitlab.user_id`: "<< pipeline.trigger_parameters.gitlab.user_id >>" + `pipeline.trigger_parameters.github_app.user_id`: "<< pipeline.trigger_parameters.github_app.user_id >>" + `pipeline.trigger_parameters.gitlab.user_name`: "<< pipeline.trigger_parameters.gitlab.user_name >>" + `pipeline.trigger_parameters.github_app.user_name`: "<< pipeline.trigger_parameters.github_app.user_name >>" + `pipeline.trigger_parameters.gitlab.user_username`: "<< pipeline.trigger_parameters.gitlab.user_username >>" + `pipeline.trigger_parameters.github_app.user_username`: "<< pipeline.trigger_parameters.github_app.user_username >>" + `pipeline.trigger_parameters.gitlab.user_avatar`: "<< pipeline.trigger_parameters.gitlab.user_avatar >>" + `pipeline.trigger_parameters.github_app.user_avatar`: "<< pipeline.trigger_parameters.github_app.user_avatar >>" + `pipeline.trigger_parameters.gitlab.repo_name`: "<< pipeline.trigger_parameters.gitlab.repo_name >>" + `pipeline.trigger_parameters.github_app.repo_name`: "<< pipeline.trigger_parameters.github_app.repo_name >>" + `pipeline.trigger_parameters.gitlab.repo_url`: "<< pipeline.trigger_parameters.gitlab.repo_url >>" + `pipeline.trigger_parameters.github_app.repo_url`: "<< pipeline.trigger_parameters.github_app.repo_url >>" + `pipeline.trigger_parameters.gitlab.web_url`: "<< pipeline.trigger_parameters.gitlab.web_url >>" + `pipeline.trigger_parameters.github_app.web_url`: "<< pipeline.trigger_parameters.github_app.web_url >>" + `pipeline.trigger_parameters.gitlab.commit_sha`: "<< pipeline.trigger_parameters.gitlab.commit_sha >>" + `pipeline.trigger_parameters.github_app.commit_sha`: "<< pipeline.trigger_parameters.github_app.commit_sha >>" + `pipeline.trigger_parameters.gitlab.commit_title`: "<< pipeline.trigger_parameters.gitlab.commit_title >>" + `pipeline.trigger_parameters.github_app.commit_title`: "<< pipeline.trigger_parameters.github_app.commit_title >>" + `pipeline.trigger_parameters.gitlab.commit_message`: "<< pipeline.trigger_parameters.gitlab.commit_message >>" + `pipeline.trigger_parameters.github_app.commit_message`: "<< pipeline.trigger_parameters.github_app.commit_message >>" + `pipeline.trigger_parameters.gitlab.commit_timestamp`: "<< pipeline.trigger_parameters.gitlab.commit_timestamp >>" + `pipeline.trigger_parameters.github_app.commit_timestamp`: "<< pipeline.trigger_parameters.github_app.commit_timestamp >>" + `pipeline.trigger_parameters.gitlab.commit_author_name`: "<< pipeline.trigger_parameters.gitlab.commit_author_name >>" + `pipeline.trigger_parameters.github_app.commit_author_name`: "<< pipeline.trigger_parameters.github_app.commit_author_name >>" + `pipeline.trigger_parameters.gitlab.commit_author_email`: "<< pipeline.trigger_parameters.gitlab.commit_author_email >>" + `pipeline.trigger_parameters.github_app.commit_author_email`: "<< pipeline.trigger_parameters.github_app.commit_author_email >>" + `pipeline.trigger_parameters.gitlab.total_commits_count`: "<< pipeline.trigger_parameters.gitlab.total_commits_count >>" + `pipeline.trigger_parameters.github_app.total_commits_count`: "<< pipeline.trigger_parameters.github_app.total_commits_count >>" + `pipeline.trigger_parameters.gitlab.branch`: "<< pipeline.trigger_parameters.gitlab.branch >>" + `pipeline.trigger_parameters.github_app.branch`: "<< pipeline.trigger_parameters.github_app.branch >>" + `pipeline.trigger_parameters.gitlab.default_branch`: "<< pipeline.trigger_parameters.gitlab.default_branch >>" + `pipeline.trigger_parameters.github_app.default_branch`: "<< pipeline.trigger_parameters.github_app.default_branch >>" + `pipeline.trigger_parameters.gitlab.x_gitlab_event_id`: "<< pipeline.trigger_parameters.gitlab.x_gitlab_event_id >>" + `pipeline.trigger_parameters.gitlab.is_fork_merge_request`: "<< pipeline.trigger_parameters.gitlab.is_fork_merge_request >>" +} + +/// Pipeline values are available to all pipeline configurations and can be used without previous +/// declaration. +/// +/// For a full list of values and built-in environment variables, see the +/// [Project values and variables guide](https://circleci.com/docs/variables/). +/// +/// For more reference: +hidden pipelineValues: PipelineValues + +output { + text = "# Generated from CircleCI.pkl. DO NOT EDIT.\n" + super.text + renderer = new YamlRenderer { + converters { + [AbstractStep] = (it) -> Map(it.__name__, it.toMap()) + [Dynamic] = (it) -> + if (it.hasProperty("__name__")) + Map(it.__name__, it.toMap().remove("__name__")) + else it } } } diff --git a/assets/pkl/external/pkl-pantry/packages/com.influxdata.telegraf/Telegraf.pkl b/assets/pkl/external/pkl-pantry/packages/com.influxdata.telegraf/Telegraf.pkl index 1c9d0d6..c581233 100644 --- a/assets/pkl/external/pkl-pantry/packages/com.influxdata.telegraf/Telegraf.pkl +++ b/assets/pkl/external/pkl-pantry/packages/com.influxdata.telegraf/Telegraf.pkl @@ -13,16 +13,242 @@ // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// -module com.influxdata.telegraf.tests.Telegraf +/// Definitions for a [Telegraf configuration](https://docs.influxdata.com/telegraf/v1.17/administration/configuration/). +/// +/// This API nests data formats as a child object of plugins. +/// For instance, here is an example of a socket listener which uses the json data format: +/// ``` +/// import "package://pkg.pkl-lang.org/pkl-pantry/com.influxdata.telegraf@#/plugins/parsers/JsonInputDataFormat.pkl" +/// +/// inputs { +/// socket_listener { +/// new { +/// service_address = "tcp://:8085" +/// data_format = new JsonInputDataFormat { +/// json_query = "foo.bar" +/// tag_keys { +/// "my-tag-1" +/// "my-tag-2" +/// } +/// json_name_key = "my_measurement" +/// json_time_key = "time" +/// } +/// } +/// } +/// } +/// ``` +@ModuleInfo { minPklVersion = "0.25.0" } +open module com.influxdata.telegraf.Telegraf -amends "pkl:test" +import "plugins/inputs/FileInput.pkl" +import "plugins/inputs/HttpInput.pkl" +import "plugins/inputs/SocketListenerInput.pkl" +import "plugins/inputs/PrometheusInput.pkl" +import "plugins/inputs/DiskInput.pkl" +import "plugins/inputs/CpuInput.pkl" +import "plugins/inputs/NetInput.pkl" +import "plugins/inputs/ExecInput.pkl" +import "plugins/inputs/SolrInput.pkl" +import "plugins/outputs/FileOutput.pkl" +import "plugins/outputs/DiscardOutput.pkl" +import "plugins/outputs/PrometheusClientOutput.pkl" +import "plugins/processors/StarlarkProcessor.pkl" -local allExamples = import*("../examples/*.pkl") +import "plugins/Plugin.pkl" -examples { - for (name in allExamples.keys) { - [name.drop("../examples".length + 1).replaceLast("pkl", "toml")] { - allExamples[name].output.text +/// Telegraf has a few options you can configure under the [agent] section of the config. +agent: AgentConfig? + +/// Output plugins write metrics to a location. +/// +/// Outputs commonly write to databases, network services, and messaging systems. +outputs: Outputs? + +/// Input plugins gather and create metrics. +/// +/// They support both polling and event driven operation. +inputs: Inputs? + +/// Processor plugins process metrics as they pass through and immediately emit results +/// based on the values they process. +/// +/// For example, this could be printing all metrics or adding a tag to all metrics that pass through. +processors: Processors? + +/// All metrics being gathered on this host will be tagged with the tags specified here. +global_tags: Mapping? + +open class Outputs { + /// This plugin writes telegraf metrics to files. + file: Listing? + + /// This output plugin simply drops all metrics that are sent to it. + /// + /// It is only meant to be used for testing purposes. + discard: Listing? + + prometheus_client: Listing? +} + +open class Inputs { + /// The file plugin parses the complete contents of a file every interval using the selected input data format. + /// + /// Note: If you wish to parse only newly appended lines use the tail input plugin instead. + /// + file: Listing? + + /// The prometheus input plugin gathers metrics from HTTP servers exposing metrics in Prometheus format. + /// + /// + prometheus: Listing? + + /// The Socket Listener is a service input plugin that listens for messages + /// from streaming (tcp, unix) or datagram (udp, unixgram) protocols. + /// + /// The plugin expects messages in the + /// [Telegraf Input Data Formats](https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_INPUT.md). + socket_listener: Listing? + + /// The [Disk input plugin](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/disk/README.md) + /// gathers metrics about disk usage by mount point. + disk: Listing? + + /// The [CPU input plugin](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/cpu/README.md) + /// gathers metrics about cpu usage. + cpu: Listing? + + /// The [HTTP input plugin](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/http/README.md) + /// collects metrics from one or more HTTP(S) endpoints. + /// + /// The endpoint should have metrics formatted in one of the supported input data formats. + /// Each data format has its own unique set of configuration options which can be added to the input configuration. + http: Listing? + + /// The [net input plugin](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/net/NET_README.md) + /// gathers metrics about network interface and protocol usage (Linux only). + net: Listing? + + /// The [exec input plugin](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/exec/README.md) + /// read metrics from one or more commands that can output to stdout. + exec: Listing? + + /// The [Solr input plugin](https://github.com/influxdata/telegraf/blob/master/plugins/inputs/solr/README.md) + /// gathers metrics from one or more Solr servers + solr: Listing? +} + +open class Processors { + /// The [starlark processor](https://github.com/influxdata/telegraf/tree/master/plugins/processors/starlark) calls a + /// Starlark function for each matched metric, allowing for custom programmatic metric processing. + starlark: Listing? +} + +open class AgentConfig { + /// Default data collection interval for all inputs. + interval: Duration? + + /// Rounds collection interval to [interval]. + /// + /// For example, `interval = 10.s` collects on :00, :10, :20, etc. + round_interval: Boolean? + + /// Telegraf will send metrics to outputs in batches of at most [metric_batch_size] metrics. + /// + /// This controls the size of writes that Telegraf sends to output plugins. + metric_batch_size: Int? + + /// Maximum number of unwritten metrics per output. + /// + /// Increasing this value allows for longer periods of output downtime + /// without dropping metrics at the cost of higher maximum memory usage. + metric_buffer_limit: Int? + + /// Collection jitter is used to jitter the collection by a random [interval]. + /// + /// Each plugin will sleep for a random time within jitter before collecting. + /// This can be used to avoid many plugins querying things like sysfs + /// at the same time, which can have a measurable effect on the system. + collection_jitter: Duration? + + /// Default flushing [interval] for all outputs. + /// + /// Maximum flush interval is `flush_interval + flush_jitter`. + flush_interval: Duration? + + /// Default flush jitter for all outputs. + /// + /// This jitters the flush [interval] by a random amount. + /// This is primarily to avoid large write spikes for users running a large number of telegraf instances. + /// For example, a jitter of 5s and interval 10s means flushes will happen every 10-15s. + flush_jitter: Duration? + + /// Collected metrics are rounded to the precision specified as an [interval]. + /// + /// Precision will NOT be used for service inputs. + /// It is up to each individual service input to set the timestamp at the appropriate precision. + precision: ("ns"|"us"|"ms"|"s")? + + /// Log at debug level. + debug: Boolean? + + /// Log only error level messages. + quiet: Boolean? + + /// Name of the file to be logged to when using the "file" logtarget. + /// + /// If set to the empty string, logs are written to stderr. + logfile: String? + + /// The logfile will be rotated after the time interval specified. + /// + /// When set to `0.s`, no time based rotation is performed. + logfile_rotation_interval: Duration? + + /// The logfile will be rotated when it becomes larger than the specified size. + /// + /// When set to `0.b`, no size based rotation is performed. + logfile_rotation_max_size: DataSize? + + /// Maximum number of rotated archives to keep, any older logs are deleted. + /// + /// If set to -1, no archives are removed. + logfile_rotation_max_archives: Int? + + /// Override default hostname, if empty use os.Hostname(). + hostname: String? + + /// If set to true, do no set the "host" tag in the telegraf agent. + omit_hostname: Boolean? +} + +/// Telegraf uses golang's built-in `time.ParseDuration` underneath the hood. +/// +local durationConverter = (dur: Duration) -> + // Golang doesn't support days, so convert down to hours. + if (dur.unit == "d") + durationConverter.apply(dur.toUnit("h")) + else + let (unit = dur.unit) + // Most of Pkl's unit names are exact matches for Golang's unit names, except `min` needs to turn into `m`. + let (telegrafUnit = if (unit == "min") "m" else unit) + "\(dur.value)\(telegrafUnit)" + +/// Telegraf uses github.coim/alecthomas/units to parse data sizes. +/// The parser is pretty lenient; see tests here: +/// +local dataSizeConverter = (size: DataSize) -> + let (unit = size.unit) + let (telegrafUnit = + if (unit.length == 3) unit[0].toUpperCase() + unit[1] + unit[2].toUpperCase() + else if (unit.length == 2) unit.toUpperCase() + else "") + "\(size.value)\(telegrafUnit)" + +output { + renderer = (Plugin.output.renderer) { + converters { + [Duration] = durationConverter + [DataSize] = dataSizeConverter } } } diff --git a/assets/pkl/external/pkl-pantry/packages/io.prometheus/configuration.pkl b/assets/pkl/external/pkl-pantry/packages/io.prometheus/Configuration.pkl similarity index 100% rename from assets/pkl/external/pkl-pantry/packages/io.prometheus/configuration.pkl rename to assets/pkl/external/pkl-pantry/packages/io.prometheus/Configuration.pkl diff --git a/assets/pkl/external/pkl-pantry/packages/k8s.contrib.crd/generate.pkl b/assets/pkl/external/pkl-pantry/packages/k8s.contrib.crd/generate.pkl index 34bbcad..a704c1d 100644 --- a/assets/pkl/external/pkl-pantry/packages/k8s.contrib.crd/generate.pkl +++ b/assets/pkl/external/pkl-pantry/packages/k8s.contrib.crd/generate.pkl @@ -13,12 +13,20 @@ // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// -/// Generate Pkl sources from CustomResourceDefinition documents. +/// Generate Pkl sources from JSON Schema documents. /// /// Limitations: /// - Cannot generate `not`, `allOf`, or `anyOf` combinators correctly due to limitations in Pkl's model. /// * Union types exist, but they are logically the same as `oneOf` (only one subschema can match). /// * Intersection types do not exist (can use this to model `allOf`). +/// - Doesn't generate correct types for schemas that use both [JsonSchema.properties] and +/// [JsonSchema.additionalProperties] because classes cannot contain entries +/// (known limitation, will be addressed in a future Pkl release). Some possible workarounds: +/// * Add a child Mapping property to represent "additional properties" and add a renderer to inline them/ +/// * Add an option to the generator to control whether a class or a Mapping gets generated. +/// - Cannot generate [JsonSchema.patternProperties] fully (this is a limitation of Pkl). +/// If there is just one pattern property, it gets generated as a [Mapping]. +/// Otherwise, this falls back to [Dynamic], which is the loosest constraint. /// - Cannot generate tuple types (this is missing in Pkl). /// - Properties called `default` cannot be generated (currently a limitation of the json parser). /// @@ -31,116 +39,38 @@ /// Sample CLI usage: /// /// ``` -/// pkl eval package://pkg.pkl-lang.org/pkl-pantry/k8s.contrib.crd@#/generate.pkl \ +/// pkl eval package://pkg.pkl-lang.org/pkl-pantry/org.json_schema.contrib@#/generate.pkl \ /// -m . \ -/// -p source="https://raw.githubusercontent.com/monzo/egress-operator/master/config/crd/bases/egress.monzo.com_externalservices.yaml" +/// -p source="https://json.schemastore.org/github-action.json" /// ``` -/// -/// Setting up replacement of Kube native types with types from the k8s standard library can be done with amending: -/// -/// ```pkl -/// amends "package://pkg.pkl-lang.org/pkl-pantry/k8s.contrib.crd@#/generate.pkl" -/// -/// import "package://pkg.pkl-lang.org/pkl-k8s/k8s@1.0.1#/api/core/v1/ResourceRequirements.pkl" -/// -/// source = "https://raw.githubusercontent.com/monzo/egress-operator/master/config/crd/bases/egress.monzo.com_externalservices.yaml" -/// -/// converters { -/// ["externalservices.egress.monzo.com"] { -/// [List("spec", "resources")] = ResourceRequirements -/// } -/// } -/// ``` -/// -/// To figure out which paths you need to override, try running with `-p logPaths`. -/// @ModuleInfo { minPklVersion = "0.25.0" } -module k8s.contrib.crd.generate +module org.json_schema.contrib.generate -import "pkl:yaml" -import "pkl:semver" -import "@deepToTyped/deepToTyped.pkl" +import "@jsonschema/Parser.pkl" +import "@jsonschema/JsonSchema.pkl" import "@uri/URI.pkl" +import "internal/ModulesGenerator.pkl" -import "internal/ModuleGenerator.pkl" - -/// The version of the Pkl Kubernetes package to import. -/// -/// This property is not used if [k8sImportPath] is set directly. -k8sVersion: String(semver.isValid(this)) = "1.0.1" - -/// The base path to use for the Kubernetes imports. -/// -/// Examples: -/// ``` -/// // Change the version -/// k8sImportPath = "package://pkg.pkl-lang.org/pkl-k8s/k8s@1.0.0#" -/// -/// // Use dependency notation, assuming the dependency is called `@k8s`. -/// k8sImportPath = "@k8s" -/// ``` -k8sImportPath: String = "package://pkg.pkl-lang.org/pkl-k8s/k8s@\(k8sVersion)#" - -/// Where to find the CRDs; can be a URI (`https:`, `file:` etc), an absolute file path, or a relative file path -source: String = read("prop:source") - +local sourceProperty = read("prop:source") local sourceUri = - if (source.startsWith(Regex(#"\w+:"#))) source // absolute URI - else if (source.startsWith("/")) "file://\(source)" // absolute file path - else "file://\(read("env:PWD"))/\(source)" // relative file path - -/// The CRD's source contents, as computed from [source]. -sourceContents: String|Resource = read(URI.encode(sourceUri)) + if (sourceProperty.startsWith(Regex(#"\w+:"#))) sourceProperty // absolute URI + else if (sourceProperty.startsWith("/")) "file://\(sourceProperty)" // absolute file path + else "file://\(read("env:PWD"))/\(sourceProperty)" // relative file pat -/// Whether to log out every path we find in each CRD to aid in setting converters. -/// -/// Default: `false`. -logPaths: Boolean? = read?("prop:logPaths")?.toBoolean() - -local crds: Listing = - let (parser = new yaml.Parser { useMapping = true }) - new { - for (crd in parser.parseAll(sourceContents)) { - when (crd is Mapping && crd.getOrNull("kind") == "CustomResourceDefinition") { - deepToTyped.apply(ModuleGenerator.CRD, crd) as ModuleGenerator.CRD - } - } - } +local schema = read(URI.encode(sourceUri)) -/// Type conversions when generating property types. -/// -/// This is a two-dimensional mapping, where top-level entries designate CRD names (for example, -/// "restateclusters.restate.dev"). -/// -/// The inner mapping specifies how individual paths within a CRD should be mapped to a custom type. -/// -/// Example: -/// ``` -/// converters { -/// ["restateclusters.restate.dev"] { -/// [List("spec", "compute", "env", "env")] = EnvVar -/// } -/// } -/// ``` -converters: Mapping, Module|Class|TypeAlias>>? +local parsedJsonSchema = Parser.parse(schema) -fixed modules: Listing = new { - for (_crd in crds) { - new ModuleGenerator { - k8sImportPath = module.k8sImportPath - crd = _crd - baseUri = URI.parse(sourceUri)!! - converters = module.converters?.getOrNull(crd.metadata.name) ?? new Mapping {} - logPaths = module.logPaths ?? false - } - } +local modulesGenerator = new ModulesGenerator { + rootSchema = parsedJsonSchema as JsonSchema + baseUri = URI.parse(sourceUri)!! } output { text = throw("The JSON Schema generator only works with multiple-file output. Try running again with the -m option.") files { - for (mod in modules) { - ["\(mod.moduleName.split(".").last).pkl"] = mod.moduleNode.output + for (mod in modulesGenerator.modules) { + ["\(mod.moduleName).pkl"] = mod.moduleNode.output } } } diff --git a/assets/pkl/external/pkl-pantry/packages/k8s.contrib/convert.pkl b/assets/pkl/external/pkl-pantry/packages/k8s.contrib/convert.pkl index 54b163c..8e38d61 100644 --- a/assets/pkl/external/pkl-pantry/packages/k8s.contrib/convert.pkl +++ b/assets/pkl/external/pkl-pantry/packages/k8s.contrib/convert.pkl @@ -13,41 +13,293 @@ // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// -module k8s.contrib.tests.convert -amends "pkl:test" +/// Converts a Kubernetes YAML manifest file to Pkl. +/// +/// Evaluating the generated Pkl file will output (an equivalent of) the original YAML. +/// Evaluating multiple generated Pkl files at once will output a single YAML stream. +/// +/// ## Prerequisites +/// +/// * The `pkl` command is [installed](https://pkl-lang.org/main/current/pkl-cli/index.html#installation). +/// +/// ## Usage +/// +/// To convert _deployment.yml_ to _deployment.pkl_, run: +/// ``` +/// pkl eval -p input=deployment.yml \ +/// -o deployment.pkl \ +/// package://pkg.pkl-lang.org/pkl-pantry/k8s.contrib@#/convert.pkl +/// ``` +/// +/// To validate the generated Pkl file, run `pkl eval deployment.pkl`. +/// +/// ## Known limitations +/// +/// * YAML comments are not preserved. +/// * YAML aliases are inlined. +@ModuleInfo { minPklVersion = "0.25.0" } +open module k8s.contrib.convert -import "pkl:yaml" import "pkl:reflect" +import "pkl:yaml" +import "@k8s/K8sObject.pkl" +import "@k8s/K8sResource.pkl" +import "@k8s/api/core/v1/ResourceRequirements.pkl" +import "@k8s/k8sSchema.pkl" -import "../convert.pkl" - -local absolutePathToPklPantry = reflect.Module(module).uri.replaceFirst("packages/k8s.contrib/tests/convert.pkl", "") - -/// Run conversion, replacing full file import paths with paths stemming from "$pklPantryBaseDir". +/// The Kubernetes resources to convert. /// -/// Before: +/// Each resource is an object of type [Mapping] as produced by [yaml.Parser]. /// -/// `import "file:///User/me/work/pkl-pantry/packages/k8s/K8sObject.pkl"` +/// Defaults to the resources contained in the YAML file passed with `-p input=path/to/file.yml`. +resourcesToConvert: List = + new yaml.Parser { useMapping = true } + .parseAll(read(inputUri)) + .filterNonNull() as List + +/// Converters to be sequentially applied to [resourcesToConvert]. /// -/// After: +/// The default [resourceConverters] suffice to convert standard Kubernetes resources. +resourceConverters: Mapping Any> = new { + ["convert resource to conform to Pkl template"] = (resource) -> + let (template = getResourceTemplate(resource)) + let (templateType = reflect.DeclaredType(reflect.Module(template).moduleClass)) + convert(resource, templateType) + ["remove kind and apiVersion properties (set by Pkl template)"] = (resource) -> + resource.toMap().remove("apiVersion").remove("kind") |> toDynamic +} + +/// Resource templates to be used for converting custom resources. /// -/// `import "$pklPantryBaseDir/packages/k8s/K8sObject.pkl"` -local function doConvert(uri: String) = - let (converter = (convert) { - resourcesToConvert = new yaml.Parser { useMapping = true }.parseAll(read(uri)).filterNonNull() as List +/// The first [String] key is the resource's `kind`, the second its `apiVersion`. +/// +/// Example: +/// ``` +/// customResourceTemplates { +/// ["Custom"] { +/// ["v1"] = import("Custom.pkl") +/// } +/// } +/// ``` +customResourceTemplates: Mapping> + +function getResourceTemplate(resource: Mapping): K8sResource = + let (kind = resource["kind"]) + let (apiVersion = resource["apiVersion"]) + doGetResourceTemplate(kind, apiVersion, k8sSchema.resourceTemplates) ?? + doGetResourceTemplate(kind, apiVersion, customResourceTemplates) ?? + throw("Cannot find a template for resource of kind `\(kind)`.") + +function getResourceTemplateUri(resource: Mapping): String = + reflect.Module(getResourceTemplate(resource)).uri + +// below here is implementation - customResourceTemplates { - ["Custom"] { - ["v1"] = import("./fixtures/CustomType.pkl") +input = read("prop:input") + +local inputUri = + if (input.startsWith(Regex(#"\w+:"#))) input // absolute URI + else if (input.startsWith("/")) "file://\(input)" // absolute file path + else "file://\(read("env:PWD"))/\(input)" // relative file path + +function resourceConverterFn(resource) = + resourceConverters.fold(resource, (acc, _, f) -> f.apply(acc)) + +local typedClass = reflect.Class(Typed) + +/// Preserve entry key `"default"` as property `_____default_____`, because `default` conflicts with +/// built-in [Dynamic.default]. +/// +/// We'll turn this back when rendering (see [renderConvertedValue]). +local toDynamic: (Mapping|Map) -> Dynamic = (input) -> + if (input is Mapping) toDynamic.apply(input.toMap()) + else + (input.toDynamic()) { + when (input.containsKey("default")) { + _____default_____ = input["default"] } } - }) - converter.output.text.replaceAll(absolutePathToPklPantry, "$pklPantryBaseDir/") -examples = new { - for (file, _ in read*("fixtures/*.yml")) { - [file.replaceAll(Regex(".*fixtures/"), "")] { - doConvert(file) +/// Best effort conversion for union types. +/// +/// - If type is not a [Mapping] or [Listing], render as-is. +/// - If type includes [Listing] and object is [Listing], render as a [Listing]. +/// - If type includes a [Mapping], render as a [Mapping]. +/// - Otherwise, use the first remaining alternative. +local function convertUnion(value: Any, type: reflect.UnionType): Any = + if (!(value is Mapping|Listing)) + value + else + let (listingType = type.members.findOrNull((elem) -> elem is reflect.DeclaredType && elem.referent.reflectee == Listing)) + let (mappingType = type.members.findOrNull((elem) -> elem is reflect.DeclaredType && elem.referent.reflectee == Mapping)) + if (value is Listing && listingType != null) + convert(value, listingType) + else if (mappingType != null) + convert(value, mappingType) + else + let (otherType = type.members.find((elem) -> elem != listingType && elem != mappingType)) + convert(value, otherType) + +local function convert(value: Any, type: reflect.Type?): Any = + if (type is reflect.NullableType) + convert(value, type.member) + else if (type is reflect.DeclaredType) + let (referent = type.referent) + if (value is Listing && referent.reflectee == Listing) + new Listing { + for (e in value) { + convert(e, type.typeArguments[0]) + } + } + else if (value is Mapping && referent.reflectee == Mapping) + new Mapping { + for (k, v in value) { + [k] = convert(v, type.typeArguments[1]) + } + } + else if (value is Mapping && referent.reflectee == ResourceRequirements.getClass()) + new Dynamic { + when (value.containsKey("requests")) { + requests { + for (k, v in value["requests"]) { + [k] = convertDataSize(v) ?? v + } + } + } + when (value.containsKey("limits")) { + limits { + for (k, v in value["limits"]) { + [k] = convertDataSize(v) ?? v + } + } + } + } + else if (value is Mapping && referent is reflect.Class && referent.isSubclassOf(typedClass)) + value + .toMap() + .map((k, v) -> Pair(k, convert(v, referent.properties.getOrNull(k)?.type))) |> toDynamic + else value + else if (type is reflect.UnionType) + convertUnion(value, type) + else if (value is Mapping) value |> toDynamic + else value + +local function doGetResourceTemplate(kind: String, apiVersion: String, templatesByKindAndVersion: Mapping): K8sResource? = + let (templatesByVersion: Mapping? = templatesByKindAndVersion.getOrNull(kind)) + if (templatesByVersion == null) + null + else + let (template = templatesByVersion.getOrNull(apiVersion)) + if (template != null) + template as K8sResource + else + throw(""" + Cannot find a pantry template for version `\(apiVersion)` of resource `\(kind)`. + Available versions: + \(templatesByVersion.keys.join("\n")) + """) + +// https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#meaning-of-memory +local function convertDataSize(input: K8sObject.Quantity): RenderDirective? = + let (inputString: String = if (input is Int|Float|DataSize) input.toString() else input as String) + let (matches = Regex(#"^(\d+(?:\.\d+)?)(k|Ki|Mi?|Gi?|Ti?|Pi?|Ei?)$"#).findMatchesIn(inputString)) + if (matches.isEmpty) + null + else + let (groups = matches[0].groups) + let (value = groups[1].value) + let (unit = groups[2].value) + // We don't have exabytes/exbibytes in Pkl, so convert down to peta/pebi + let (pklValue = if (unit.startsWith("E")) value.toInt() * 1000 else value) + let (pklUnit = if (unit.startsWith("E")) unit.replaceFirst("E", "P") else unit) + new RenderDirective { + text = "= \(pklValue).\(pklUnit.toLowerCase())b" + } + +// Collects all imports, +// assigning unique import aliases if multiple versions of the same kind are used. +local importsByKindAndVersion: Map> = + resourcesToConvert.fold(Map(), (imports, res) -> + let (importsByVersion = imports.getOrNull(res["kind"])) + if (importsByVersion == null) + imports.put( + res["kind"], + Map( + res["apiVersion"], + new ImportInfo { + resource = res + name = res["kind"] + uri = getResourceTemplateUri(res) + isAliased = false + })) + else if (importsByVersion.containsKey(res["apiVersion"])) + imports + else if (importsByVersion.length == 1) + let (first = importsByVersion.values.single.resource) + imports.put( + res["kind"], + Map( + // for consistency, also used aliased name for first import of this kind + first["apiVersion"], createAliasedInfo(first), + res["apiVersion"], createAliasedInfo(res))) + else + imports.put( + res["kind"], + importsByVersion + .put(res["apiVersion"], createAliasedInfo(res)))) + +local function createAliasedInfo(res: Mapping): ImportInfo = new { + resource = res + name = "\(res["kind"])\(res["apiVersion"].split("/").last.capitalize())" + uri = getResourceTemplateUri(res) + isAliased = true +} + +local class ImportInfo { + resource: Mapping + name: String + uri: String + isAliased: Boolean +} + +local function renderConvertedValue(value: Any) = + new PcfRenderer {}.renderValue(value).replaceAll("_____default_____", "default") + +output { + local importInfos = importsByKindAndVersion.values + .flatMap((it) -> it.values) + .sortWith((info1, info2) -> info1.uri < info2.uri) + local textBuffer: Listing = new { + "import \"\(reflect.Module(K8sResource).uri)\"\n" + for (importInfo in importInfos) { + if (importInfo.isAliased) + "import \"\(importInfo.uri)\" as \(importInfo.name)\n" + else + "import \"\(importInfo.uri)\"\n" + } + "\n" + "resources: Listing = new {" + for (resource in resourcesToConvert) { + "\n" + " new \(importsByKindAndVersion[resource["kind"]][resource["apiVersion"]].name) " + // would be good to be able to set base indent + for (lineIdx, line in renderConvertedValue(resourceConverterFn(resource)).split("\n")) { + when (lineIdx != 0) { + " " + } + line + "\n" + } } + "}\n\n" + """ + output { + value = resources + renderer = (K8sResource.output.renderer as YamlRenderer) { + isStream = true + } + } + """ } + text = textBuffer.toList().join("") } \ No newline at end of file diff --git a/assets/pkl/external/pkl-pantry/packages/org.apache.spark/PropertiesBase.pkl b/assets/pkl/external/pkl-pantry/packages/org.apache.spark/PropertiesBase.pkl index c1d8c81..522ee88 100644 --- a/assets/pkl/external/pkl-pantry/packages/org.apache.spark/PropertiesBase.pkl +++ b/assets/pkl/external/pkl-pantry/packages/org.apache.spark/PropertiesBase.pkl @@ -13,35 +13,30 @@ // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// -module org.apache.spark.tests.PropertiesBase +abstract module org.apache.spark.PropertiesBase -amends "pkl:test" +import "pkl:semver" +import "utils.pkl" -import "../PropertiesBase.pkl" +/// The Spark version to use these properties with. +hidden targetSparkVersion: String? -local class SampleSparkProperties extends PropertiesBase { - `spark.core.numbers`: Int? - `spark.{driver|executor}.something`: Mapping? - `spark.shards.[0-9].setting.[node].memory`: Mapping>? - `spark.kubernetes.some.type.of.memory`: DataSize? - `spark.some.type.of.duration`: Duration? -} +function since(sparkVersion: String): Boolean = + if (targetSparkVersion == null) true + else + let (parsedVersion = semver.Version(sparkVersion)) + if (!parsedVersion.isGreaterThan(semver.Version(targetSparkVersion))) + throw("This property exists since Spark version \(parsedVersion), but target Spark version is \(targetSparkVersion).") + else + true + +hidden effectiveProperties: Map = + module.toMap().flatMap((key, value) -> utils.convertProperty(key, value)) + +/// Indicates that the annotated property cannot be set by the user. +class Reserved extends Annotation -examples { - ["rendering properties"] { - new SampleSparkProperties { - `spark.{driver|executor}.something` { - ["driver"] = "some driver setting" - ["executor"] = "some executor setting" - } - `spark.core.numbers` = 5 - `spark.shards.[0-9].setting.[node].memory` { - ["1"] { - ["this-node"] = 5.gib - } - } - `spark.kubernetes.some.type.of.memory` = 10.gib - `spark.some.type.of.duration` = 5.min - }.output.text - } +output { + value = effectiveProperties + renderer = new PropertiesRenderer {} } diff --git a/assets/pkl/external/pkl-pantry/packages/org.json_schema.contrib/generate.pkl b/assets/pkl/external/pkl-pantry/packages/org.json_schema.contrib/generate.pkl index 34bbcad..a704c1d 100644 --- a/assets/pkl/external/pkl-pantry/packages/org.json_schema.contrib/generate.pkl +++ b/assets/pkl/external/pkl-pantry/packages/org.json_schema.contrib/generate.pkl @@ -13,12 +13,20 @@ // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// -/// Generate Pkl sources from CustomResourceDefinition documents. +/// Generate Pkl sources from JSON Schema documents. /// /// Limitations: /// - Cannot generate `not`, `allOf`, or `anyOf` combinators correctly due to limitations in Pkl's model. /// * Union types exist, but they are logically the same as `oneOf` (only one subschema can match). /// * Intersection types do not exist (can use this to model `allOf`). +/// - Doesn't generate correct types for schemas that use both [JsonSchema.properties] and +/// [JsonSchema.additionalProperties] because classes cannot contain entries +/// (known limitation, will be addressed in a future Pkl release). Some possible workarounds: +/// * Add a child Mapping property to represent "additional properties" and add a renderer to inline them/ +/// * Add an option to the generator to control whether a class or a Mapping gets generated. +/// - Cannot generate [JsonSchema.patternProperties] fully (this is a limitation of Pkl). +/// If there is just one pattern property, it gets generated as a [Mapping]. +/// Otherwise, this falls back to [Dynamic], which is the loosest constraint. /// - Cannot generate tuple types (this is missing in Pkl). /// - Properties called `default` cannot be generated (currently a limitation of the json parser). /// @@ -31,116 +39,38 @@ /// Sample CLI usage: /// /// ``` -/// pkl eval package://pkg.pkl-lang.org/pkl-pantry/k8s.contrib.crd@#/generate.pkl \ +/// pkl eval package://pkg.pkl-lang.org/pkl-pantry/org.json_schema.contrib@#/generate.pkl \ /// -m . \ -/// -p source="https://raw.githubusercontent.com/monzo/egress-operator/master/config/crd/bases/egress.monzo.com_externalservices.yaml" +/// -p source="https://json.schemastore.org/github-action.json" /// ``` -/// -/// Setting up replacement of Kube native types with types from the k8s standard library can be done with amending: -/// -/// ```pkl -/// amends "package://pkg.pkl-lang.org/pkl-pantry/k8s.contrib.crd@#/generate.pkl" -/// -/// import "package://pkg.pkl-lang.org/pkl-k8s/k8s@1.0.1#/api/core/v1/ResourceRequirements.pkl" -/// -/// source = "https://raw.githubusercontent.com/monzo/egress-operator/master/config/crd/bases/egress.monzo.com_externalservices.yaml" -/// -/// converters { -/// ["externalservices.egress.monzo.com"] { -/// [List("spec", "resources")] = ResourceRequirements -/// } -/// } -/// ``` -/// -/// To figure out which paths you need to override, try running with `-p logPaths`. -/// @ModuleInfo { minPklVersion = "0.25.0" } -module k8s.contrib.crd.generate +module org.json_schema.contrib.generate -import "pkl:yaml" -import "pkl:semver" -import "@deepToTyped/deepToTyped.pkl" +import "@jsonschema/Parser.pkl" +import "@jsonschema/JsonSchema.pkl" import "@uri/URI.pkl" +import "internal/ModulesGenerator.pkl" -import "internal/ModuleGenerator.pkl" - -/// The version of the Pkl Kubernetes package to import. -/// -/// This property is not used if [k8sImportPath] is set directly. -k8sVersion: String(semver.isValid(this)) = "1.0.1" - -/// The base path to use for the Kubernetes imports. -/// -/// Examples: -/// ``` -/// // Change the version -/// k8sImportPath = "package://pkg.pkl-lang.org/pkl-k8s/k8s@1.0.0#" -/// -/// // Use dependency notation, assuming the dependency is called `@k8s`. -/// k8sImportPath = "@k8s" -/// ``` -k8sImportPath: String = "package://pkg.pkl-lang.org/pkl-k8s/k8s@\(k8sVersion)#" - -/// Where to find the CRDs; can be a URI (`https:`, `file:` etc), an absolute file path, or a relative file path -source: String = read("prop:source") - +local sourceProperty = read("prop:source") local sourceUri = - if (source.startsWith(Regex(#"\w+:"#))) source // absolute URI - else if (source.startsWith("/")) "file://\(source)" // absolute file path - else "file://\(read("env:PWD"))/\(source)" // relative file path - -/// The CRD's source contents, as computed from [source]. -sourceContents: String|Resource = read(URI.encode(sourceUri)) + if (sourceProperty.startsWith(Regex(#"\w+:"#))) sourceProperty // absolute URI + else if (sourceProperty.startsWith("/")) "file://\(sourceProperty)" // absolute file path + else "file://\(read("env:PWD"))/\(sourceProperty)" // relative file pat -/// Whether to log out every path we find in each CRD to aid in setting converters. -/// -/// Default: `false`. -logPaths: Boolean? = read?("prop:logPaths")?.toBoolean() - -local crds: Listing = - let (parser = new yaml.Parser { useMapping = true }) - new { - for (crd in parser.parseAll(sourceContents)) { - when (crd is Mapping && crd.getOrNull("kind") == "CustomResourceDefinition") { - deepToTyped.apply(ModuleGenerator.CRD, crd) as ModuleGenerator.CRD - } - } - } +local schema = read(URI.encode(sourceUri)) -/// Type conversions when generating property types. -/// -/// This is a two-dimensional mapping, where top-level entries designate CRD names (for example, -/// "restateclusters.restate.dev"). -/// -/// The inner mapping specifies how individual paths within a CRD should be mapped to a custom type. -/// -/// Example: -/// ``` -/// converters { -/// ["restateclusters.restate.dev"] { -/// [List("spec", "compute", "env", "env")] = EnvVar -/// } -/// } -/// ``` -converters: Mapping, Module|Class|TypeAlias>>? +local parsedJsonSchema = Parser.parse(schema) -fixed modules: Listing = new { - for (_crd in crds) { - new ModuleGenerator { - k8sImportPath = module.k8sImportPath - crd = _crd - baseUri = URI.parse(sourceUri)!! - converters = module.converters?.getOrNull(crd.metadata.name) ?? new Mapping {} - logPaths = module.logPaths ?? false - } - } +local modulesGenerator = new ModulesGenerator { + rootSchema = parsedJsonSchema as JsonSchema + baseUri = URI.parse(sourceUri)!! } output { text = throw("The JSON Schema generator only works with multiple-file output. Try running again with the -m option.") files { - for (mod in modules) { - ["\(mod.moduleName.split(".").last).pkl"] = mod.moduleNode.output + for (mod in modulesGenerator.modules) { + ["\(mod.moduleName).pkl"] = mod.moduleNode.output } } } diff --git a/assets/pkl/external/pkl-pantry/packages/org.json_schema.contrib/internal/ModuleGenerator.pkl b/assets/pkl/external/pkl-pantry/packages/org.json_schema.contrib/internal/ModuleGenerator.pkl index 4662bfb..32b0a11 100644 --- a/assets/pkl/external/pkl-pantry/packages/org.json_schema.contrib/internal/ModuleGenerator.pkl +++ b/assets/pkl/external/pkl-pantry/packages/org.json_schema.contrib/internal/ModuleGenerator.pkl @@ -16,122 +16,50 @@ /// Support for generating classes. @ModuleInfo { minPklVersion = "0.25.0" } @Unlisted -module k8s.contrib.crd.internal.ModuleGenerator +module org.json_schema.contrib.internal.ModuleGenerator -import "pkl:reflect" -import "@jsonschema.contrib/internal/Type.pkl" -import "@jsonschema.contrib/internal/TypesGenerator.pkl" -import "@jsonschema.contrib/internal/utils.pkl" -import "@jsonschema/JsonSchema.pkl" -import "@jsonschema/Parser.pkl" -import "@k8s/apiextensions-apiserver/pkg/apis/apiextensions/v1/CustomResourceDefinition.pkl" -import "@k8s/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/CustomResourceDefinition.pkl" as BetaCRD +import "@syntax/TypeNode.pkl" import "@syntax/ClassNode.pkl" import "@syntax/ClassOrModuleNode.pkl" -import "@syntax/DocCommentNode.pkl" +import "@syntax/TypeAliasNode.pkl" import "@syntax/ModuleNode.pkl" -import "@syntax/TypeNode.pkl" +import "@syntax/DocCommentNode.pkl" +import "@jsonschema/JsonSchema.pkl" import "@uri/URI.pkl" +import "utils.pkl" +import "TypesGenerator.pkl" +import "../ref.pkl" +import "Type.pkl" local pcfRenderer = new PcfRenderer { useCustomStringDelimiters = true } local jsonRenderer = new JsonRenderer {} -// noinspection Deprecated -typealias CRD = CustomResourceDefinition|BetaCRD - -/// The CRD -crd: CRD - -logPaths: Boolean = false - -k8sImportPath: String - -// noinspection Deprecated -local version: CustomResourceDefinition.CustomResourceDefinitionVersion|BetaCRD.CustomResourceDefinitionVersion = - crd.spec.versions.toList().find((version) -> version.storage) -// noinspection Deprecated -local schema: CustomResourceDefinition.CustomResourceValidation|BetaCRD.CustomResourceValidation = - (if (crd is BetaCRD) version.schema ?? crd.spec.validation else version.schema)!! - -/// The Schema -rootSchema: JsonSchema((s) -> validCRDSchema(s)) = Parser.parse(new JsonRenderer {}.renderDocument(schema.openAPIV3Schema)) as JsonSchema - -local ignoreProperties = Set("apiVersion", "kind", "metadata") -local filteredRootSchema = (rootSchema) { - when (rootSchema.properties != null) { - properties = new { - for (k, v in rootSchema.properties!!) { - when (!ignoreProperties.contains(k)) { - [k] = v - } - } - } - } -} - -// https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#validation -local function validCRDSchema(schema: (JsonSchema.Schema|Listing)?) = - if (schema == null) - true - else if (schema is JsonSchema) - validCRDJsonSchema(schema) - else if (schema is Listing) - schema.toList().every((s) -> validCRDSchema(s)) - else - true - -local function validCRDJsonSchema(schema: JsonSchema) = - schema.definitions == null - && schema.$defs == null - && schema.deprecated == null - && schema.$id == null - && schema.patternProperties == null - && schema.readOnly == null - && schema.writeOnly == null - && schema.$ref == null - && schema.uniqueItems != true - && (schema.properties?.toMap()?.values?.every((s) -> validCRDSchema(s)) ?? true) - && schema.additionalProperties != false - && !(schema.additionalProperties != null && schema.properties != null) - && validCRDSchema(schema.additionalProperties) - && validCRDSchema(schema.propertyNames) - && validCRDSchema(schema.items) - && validCRDSchema(schema.additionalItems) - && validCRDSchema(schema.contains) - && validCRDSchema(schema.oneOf) - && validCRDSchema(schema.anyOf) - && validCRDSchema(schema.allOf) - && validCRDSchema(schema.not) - +/// The root schema, used to resolve `$ref` values. +rootSchema: JsonSchema - -converters: Mapping, Module|Class|TypeAlias> - -/// The URI representing the root schema, used for the doc comment. +/// The URI representing the root schema, used to resolve `$ref` values. baseUri: URI +/// Existing type names that have been generated prior to generating this module. +existingTypes: Type.TypeNames + /// The name of this module -moduleName: String = crd.spec.group.split(".").reverse() - .add(version.name) - .add(crd.spec.names.kind) - .join(".") +moduleName: String = utils.pascalCase(baseUri.pathSegments.last.replaceAll(".json", "")) -local typesGenerator: TypesGenerator = new { baseUri = new {}; enclosingModuleName = moduleName } +local typesGenerator: TypesGenerator = new { baseUri = module.baseUri; enclosingModuleName = moduleName } -/// The types described in this JSON Schema document, included those replaced via converters -moduleTypes: Type.TypeNames = convertedSchemas.map((schema, importAndType) -> Pair(schema, importAndType.type)) + classSchemas +/// The types described directly in this JSON Schema document (excludes types in an external reference). +moduleTypes: Type.TypeNames = typeAliasSchemas + classSchemas /// Generate a Pkl [ModuleNode] from a given schema. moduleNode: ModuleNode = - let (allTypeNames = moduleTypes) + let (allTypeNames = moduleTypes + existingTypes) new { imports { - new { - value = "\(k8sImportPath)/apimachinery/pkg/apis/meta/v1/ObjectMeta.pkl" - } - for (_import in convertedSchemas.values.map((it) -> it._import).toSet()) { + for (moduleName in existingTypes.values.map((it) -> it.moduleName).toSet()) { new { - value = _import + // TODO: shouldn't include transitive deps + value = "\(moduleName.split(".").last).pkl" } } } @@ -140,68 +68,29 @@ moduleNode: ModuleNode = generatePklClass(schema, type, allTypeNames) } } - declaration { - docComment = getDocComment(filteredRootSchema, "module") - moduleHeader { - name { - parts { - for (part in moduleName.split(".")) { - new { value = part } - } - } - } - moduleExtendsOrAmendsClause { - type = "extends" - extendedModule = "\(k8sImportPath)/K8sResource.pkl" - } + typealiases { + for (schema, type in typeAliasSchemas) { + generatePklTypeAlias(schema, type, allTypeNames) } } - properties { - new { - modifiers { "fixed" } - name { value = "apiVersion" } - typeAnnotation { - type = new TypeNode.StringLiteralTypeNode { - value = "\(crd.spec.group)/\(version.name)" - } - } - } - new { - modifiers { "fixed" } - name { value = "kind" } - typeAnnotation { - type = new TypeNode.StringLiteralTypeNode { - value = crd.spec.names.kind - } - } - } - new { - name { value = "metadata" } - docComment { - value = """ - Standard object's metadata. - - More info: . - """ - } - typeAnnotation { - type = new TypeNode.NullableTypeNode { - typeNode = new TypeNode.DeclaredTypeNode { - name { - parts { - new { - value = "ObjectMeta" - } - } + declaration { + docComment = getDocComment(rootSchema, "module") + when (moduleName != null) { + moduleHeader { + name { + parts { + for (part in moduleName.split(".")) { + new { value = part } } } } } } - when (isClassLike(filteredRootSchema)) { - ...generateClassBody(filteredRootSchema, allTypeNames) - } } + properties = + if (isClassLike(rootSchema)) + generateClassBody(rootSchema, allTypeNames) + else null } /// Determine the doc comments of a schema. @@ -213,58 +102,77 @@ local function getDocComment(schema: JsonSchema|Boolean, type: "class"|"module"| null else let (docCommentText: String = - new Listing { - schema.description - when (type is "module") { "This module was generated from the CustomResourceDefinition at <\(baseUri)>." } - when (type is "class"|"module") { getWarnings(schema, type) } - when (schema.default != null) { "Default if undefined: `\(pcfRenderer.renderValue(schema.default))`" } - }.toList().filterNonNull().join("\n\n") + List( + schema.title, + schema.description, + if (type is "module") "This module was generated from JSON Schema from <\(baseUri)>." else null, + if (type is "class"|"module") getWarnings(schema, type) else null, + if (schema.default != null) "Default if undefined: `\(pcfRenderer.renderValue(schema.default))`" else null + ) + .filterNonNull() + .join("\n\n") ) if (docCommentText.isEmpty) null - else - new DocCommentNode { - value = docCommentText - autoWrap = true - } + else new DocCommentNode { + value = docCommentText + autoWrap = true + } local function getWarnings(schema: JsonSchema, type: "class"|"module"): String? = - if (type == "class") null + if (schema.properties != null && (schema.additionalProperties is JsonSchema || schema.patternProperties != null)) + """ + WARN: both properties and at least one of additionalProperties and patternProperties are set. + This is ambiguously defined; can either be defined as a `Mapping` or a class. + """ + else if (type == "class") null + else if (schema.type == "object" && schema.additionalProperties != false) + "WARN: The root schema describes open-ended properties, but this is not possible to describe at the module level." else if (schema.type != null && schema.type != "object") "WARN: The root schema's type is `\(jsonRenderer.renderValue(schema.type))`, and cannot be correctly mapped to a Pkl module." - else - null + else if (schema.properties == null) + "WARN: unable to determine module-level properties on the root schema." + else null local function generateClassBody( schema: JsonSchema(this.properties != null), typeNames: Type.TypeNames ): Listing = - new { - for (propName, propSchema in schema.properties!!) { - new { - name { - value = propName - } - when (propSchema is JsonSchema && propSchema.deprecated == true) { - annotations { utils.DEPRECATED } - } - docComment = getDocComment(propSchema, null) - typeAnnotation { - // If this property doesn't appear in the `required` array, mark it as nullable. - // We can't do this within [TypesGenerator] because it doesn't have enough information available. - type = let (underlyingType = typesGenerator.generateTypeNode(propSchema, typeNames)) + if (schema.$$refUri != null) + let (referencedSchema = ref.resolveRef(baseUri, schema)) + if (referencedSchema == null) new {} + else if (referencedSchema is Boolean) let (_ = trace("WARN: `$ref` points to a boolean somehow")) new {} + else generateClassBody(referencedSchema as JsonSchema, typeNames) + else + new { + for (propName, propSchema in schema.properties!!) { + new { + name { + value = propName + } + when (propSchema is JsonSchema && propSchema.deprecated == true) { + annotations { utils.DEPRECATED } + } + docComment = getDocComment(propSchema, null) + typeAnnotation { + // If this property doesn't appear in the `required` array, mark it as nullable. + // We can't do this within [TypesGenerator] because it doesn't have enough information available. + type = let (underlyingType = typesGenerator.generateTypeNode(propSchema, typeNames)) if (schema.required?.toSet()?.contains(propName) ?? false) underlyingType // If the type is already nullable, no need to make it *more* nullable. else if (underlyingType is TypeNode.NullableTypeNode) underlyingType else new TypeNode.NullableTypeNode { typeNode = underlyingType } + } } } } - } -/// Generates a [ClassNode] from a [CRDJsonSchema]. +/// Generates a [ClassNode] from a [JsonSchema]. local function generatePklClass(schema: JsonSchema, className: Type, typeNames: Type.TypeNames): ClassNode = new { docComment = getDocComment(schema, "class") + when (schema.deprecated == true) { + annotations { utils.DEPRECATED } + } classHeader { name { value = className.name @@ -273,120 +181,81 @@ local function generatePklClass(schema: JsonSchema, className: Type, typeNames: properties = generateClassBody(schema, typeNames) } +/// Generates a [TypeAliasNode] from a [JsonSchema]. +local function generatePklTypeAlias( + schema: JsonSchema, + typeAliasName: Type, + typeNames: Type.TypeNames +): TypeAliasNode = new { + name { + value = typeAliasName.name + } + docComment = getDocComment(schema, null) + type = typesGenerator.generateTypeNode(schema, typeNames.remove(schema)) +} + function isClassLike(schema: JsonSchema.Schema): Boolean = - !(schema is Boolean) && schema.properties != null + if (schema is Boolean) + false + else + // Edge case: if `$ref` exists, any other property should be ignored. + schema.$ref == null && schema.properties != null -/// Determines the name of a type. +/// Determine the name of a type. /// /// Try to use the parent property's name as part of the class name in case of conflict. /// If already at the root, add a number at the end. local function determineTypeName(path: List, candidateName: String, existingTypeNames: Set, index: Int): Type = - if (existingTypeNames.contains(utils.pascalCase(candidateName))) - if (path.isEmpty) - determineTypeName(path, candidateName + index.toString(), existingTypeNames, index + 1) + let (candidateType = new Type { name = utils.pascalCase(candidateName); moduleName = module.moduleName }) + if (existingTypeNames.contains(candidateType)) + if (path.isEmpty) + determineTypeName(path, candidateName + index.toString(), existingTypeNames, index + 1) + else + determineTypeName(path.dropLast(1), utils.pascalCase(path.last.capitalize()) + utils.pascalCase(candidateName), existingTypeNames, index) else - determineTypeName( - path.dropLast(1), - utils.pascalCase(path.last.capitalize()) + utils.pascalCase(candidateName), - existingTypeNames, - index - ) - else - new { name = utils.pascalCase(candidateName); moduleName = module.moduleName } + candidateType /// The schemas that should be rendered as classes. /// -/// Classes get rendered for any subschema that has [JsonSchema.properties] defined, and does not show up in converters +/// Classes get rendered for any subschema that has [JsonSchema.properties] defined, and does not have [JsonSchema.`$ref`] defined. local classSchemas: Type.TypeNames = - utils._findMatchingSubSchemas(filteredRootSchema, List(), (elem) -> elem != filteredRootSchema && isClassLike(elem)) - .filter((path, _) -> !pathPrefixes(path).any((prefix) -> converters.containsKey(prefix))) // path or prefix are not explicitly in converters - .entries - .fold(Map(), (accumulator: Type.TypeNames, pair) -> - let (path = pair.first) - let (schema = pair.second) - let (typeName = determineTypeName(path, path.lastOrNull?.capitalize() ?? "Item", accumulator.values.toSet(), 0)) - accumulator.put(schema, typeName) - ) + let (schemas = utils._findMatchingSubSchemas(rootSchema, List(), (elem) -> elem != rootSchema && isClassLike(elem))) + schemas + .entries + .fold(Map(), (accumulator: Type.TypeNames, pair) -> + let (path = pair.first) + let (schema = pair.second) + let (typeName = determineTypeName(path.dropLast(1), path.lastOrNull?.capitalize() ?? "Item", accumulator.values.toSet(), 0)) + accumulator.put(schema, typeName) + ) -// Return all possible path prefixes of this path, starting with its first entry and ending with the full path -local function pathPrefixes(path: List): List> = new Listing { - for (i, _ in path) { - path.take(i + 1) - } -}.toList() +local classNames: Set = classSchemas.values.toSet() -/// mapSchema deeply transforms a CRD schema. CRD schemas are significantly simplified, as many fields must not be set. -/// Nested types are *always* represented in properties, items or additionalProperties, even if they are also set in oneOf etc -function mapSchema( - schema: JsonSchema.Schema, - path: Listing, - f: (JsonSchema.Schema) -> JsonSchema.Schema -): JsonSchema.Schema = - let (applied = f.apply(schema)) - (applied) { - when (applied.properties != null) { - properties = new { - for (name, property in applied.properties!!) { - [name] = mapSchema(property, (path) { name }, f) - } - } - } - when (applied.additionalProperties != null) { - additionalProperties = mapSchema(applied.additionalProperties!!, (path) { "additionalProperties" }, f) - } - when (applied.items != null) { - when (applied.items is Listing) { - items = new { - for (i, item in applied.items as Listing) { - [i] = mapSchema(item, (path) { i.toString() }, f) - } - } - } else { - items = mapSchema(applied.items as JsonSchema.Schema, (path) { "items" }, f) - } - } - } - -local convertedSchemas: Map = - utils.findMatchingSubschemas(filteredRootSchema, (elem) -> elem != filteredRootSchema) - .filter((path, _) -> converters.containsKey(if (logPaths) trace(path) else path)) - .entries - .fold(Map(), (accumulator: Map, pair) -> - let (path = pair.first) - let (schema = pair.second) - let (converted = converters[path]) - let (reflected = - if (converted is Module) - reflect.Module(converted) - else if (converted is TypeAlias) - reflect.TypeAlias(converted) +/// The schemas that should be rendered as typealiases. +/// +/// This is done by traversing schema definintions. +/// Typealiases get rendered for any [JsonSchema.definitions] or [JsonSchema.`$defs`] that should not be rendered as a class. +local typeAliasSchemas: Type.TypeNames = + // Grab all schemas that have `definitions` or `$defs` + let (schemasWithDefinitions = utils._findMatchingSubSchemas(rootSchema, List(), (elem) -> (elem.definitions ?? elem.$defs) != null)) + schemasWithDefinitions + .entries + // For each schema, return the child json schema properties that are not class-like + .flatMap((pair) -> + let (path = pair.first) + let (schema = pair.second) + (schema.definitions ?? schema.$defs ?? new Mapping {}).toMap() + .filter((_, value) -> value is JsonSchema && value.$ref == null && !isClassLike(value)) + .mapKeys((key, _) -> path.add(key)) + .entries + ) + .filter((pair) -> pair.second is JsonSchema) + .fold(Map(), (accumulator: Type.TypeNames, pair: Pair, JsonSchema.Schema>) -> + if (pair.second is Boolean) + accumulator else - reflect.Class(converted) + let (schema = pair.second as JsonSchema) + let (path = pair.first) + let (typeName = determineTypeName(path, path.last.capitalize(), accumulator.values.toSet() + classNames, 0)) + accumulator.put(schema, typeName) ) - accumulator.put(schema, new ImportAndType { - type { - moduleName = if (reflected is reflect.Module) module.moduleName else reflected.enclosingDeclaration.name - name = reflected.name.split(".").last - } - local uri = - if (reflected is reflect.Module) - reflected.uri - else - reflected.enclosingDeclaration.uri - _import = determineImportPath(uri) - }) - ) - -/// If any conversions are from the core k8s library, replace the import path with [k8sImportPath] -local function determineImportPath(uri: String): String = - if ( - uri.startsWith("package://pkg.pkl-lang.org/pkl-k8s/k8s") - || uri.startsWith("projectpackage://pkg.pkl-lang.org/pkl-k8s/k8s") - ) - k8sImportPath + uri.dropWhile((it) -> it != "#").drop(1) - else uri - -class ImportAndType { - _import: String - type: Type -} diff --git a/assets/pkl/external/pkl-pantry/packages/org.openapis.v3/expressions.pkl b/assets/pkl/external/pkl-pantry/packages/org.openapis.v3/expressions.pkl index 104bde0..fd8fbe5 100644 --- a/assets/pkl/external/pkl-pantry/packages/org.openapis.v3/expressions.pkl +++ b/assets/pkl/external/pkl-pantry/packages/org.openapis.v3/expressions.pkl @@ -13,22 +13,78 @@ // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// -module org.openapis.v3.tests.expressions - -amends "pkl:test" - -import "../expressions.pkl" - -facts { - ["Expression"] { - "$method" is expressions.Expression - "$request.header.accept" is expressions.Expression - "$request.path.id" is expressions.Expression - "$request.body#/user/uuid" is expressions.Expression - "$url" is expressions.Expression - "$response.body#/status" is expressions.Expression - "$response.header.Server" is expressions.Expression - !("bar" is expressions.Expression) - !("$request.header.über" is expressions.Expression) - } -} +module org.openapis.v3.expressions + +import "expressions.pkl" + +/// A string representing an OpenAPI [Runtime Expression](https://spec.openapis.org/oas/v3.0.3#runtime-expressions). +typealias Expression = + "$url" + |"$method" + |"$statusCode" + |HeaderReferenceExpression + |QueryOrPathReferenceExpression + |BodyReferenceExpression + +/// A string representing a reference to a header of a request or response. +typealias HeaderReferenceExpression = + String(matches(Regex(#"\$(?:request|response)\.header\.[0-9A-z!#$%&'+\-.^_`|~]+"#))) + +/// A string representing a reference to a query param, or a path segment. +typealias QueryOrPathReferenceExpression = + String(expressions.isQueryOrPathReference) + +hidden isQueryOrPathReference = (it) -> + let (suffix = it.replaceFirst(Regex(#"\$(?:request|response)\.(?:query|path)\."#), "")) + if (suffix == it) false + else expressions.isJsonName(it) + +/// A string representing a reference to a segment in the body of a request or response. +typealias BodyReferenceExpression = + "$request.body" + |"$response.body" + |String(expressions.isBodyReference) + +/// Tells if this string is `body-reference` of a Runtime Expression. +/// +/// +hidden isBodyReference = (it: String) -> + let (suffix = it.replaceFirst(Regex(#"\$(?:request|response)\.body#"#), "")) + if (suffix == it) false + else expressions.isJsonPointer(suffix) + +// json-pointer = *( "/" reference-token ) +function isJsonPointer(it: String) = + it.startsWith("/") + && !it.endsWith("/") + && it.split("/").every(isReferenceToken) + +/// Tells if the input string is a `reference-token` in a Runtime Expression. +/// +/// +local isReferenceToken = (it) -> + // language=regexp + it.matches(Regex(#""" + (?x) # extended mode + (?: + ~0 + |~1 + |[\x00-\x2E] + |[\x30-\x7D] + |[\x7f-\x{10ffff}] + )* + """#)) + +/// Tells if the input string is valid JSON name. +/// +/// JSON string spec: +function isJsonName(it) = + // language=regexp + it.matches(Regex(#""" + (?x) # extended mode + (?: + [\x20-\x21\x23-\x5B\x5D-\x{10ffff}] # unescaped char (%x20-21 / %x23-5B / %x5D-10FFFF) + |\\["\\/bfnrt] # escaped char (e.g. `\n`) + |\\u[0-9a-f]{4} # code point literal + )* + """#)) diff --git a/assets/pkl/external/pkl-pantry/packages/pkl.experimental.net/net.pkl b/assets/pkl/external/pkl-pantry/packages/pkl.experimental.net/net.pkl index a94e6e3..6dcdef5 100644 --- a/assets/pkl/external/pkl-pantry/packages/pkl.experimental.net/net.pkl +++ b/assets/pkl/external/pkl-pantry/packages/pkl.experimental.net/net.pkl @@ -13,519 +13,512 @@ // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// -module pkl.experimental.net.tests.net +/// We sell IP addresses and IP address accessories +/// This module contains types and functions for handling network (IP and MAC) addresses +@ModuleInfo { minPklVersion = "0.25.0" } +module pkl.experimental.net.net -amends "pkl:test" +import "pkl:math" +import "./u128.pkl" +import "./net.pkl" -import "../net.pkl" -import "../u128.pkl" +// language=RegExp +const hidden hex: String = "[0-9a-fA-F]" -local baseIP = net.IPv6Address("::") +// language=RegExp +const hidden decByte: String = #"(25[0-5]|2[0-4][0-9]|[01]?[0-9]{1,2})"# -facts { - ["MACAddress"] { - net.MACAddress("34:73:5A:FA:AF:22").eui64(baseIP).toExpandedString() == "0000:0000:0000:0000:3673:5aff:fefa:af22" - } - // note: IPv4Address() and IPv6Address() must be invoked prior to any tests that reference the IPv[46]AddressString - // typealiases. This works around a pkl 0.25 bug that otherwise causes java exceptions. - ["IPv4Address =="] { - net.IPv4Address("127.0.0.1") != net.IPv4Address("127.0.0.2") - net.IPv4Address("127.0.0.1") != net.IPv4Address("127.0.1.1") - net.IPv4Address("127.0.0.1") != net.IPv4Address("127.1.0.1") - net.IPv4Address("127.0.0.1") != net.IPv4Address("128.0.0.1") - } - ["IPv6Address =="] { - net.IPv6Address("fe80::1") == net.IPv6Address("fe80::1") - net.IPv6Address("fe80::1") != net.IPv6Address("fe80::2") - net.IPv6Address("fe80::1") != net.IPv6Address("fe80::1:1") - net.IPv6Address("fe80::1") != net.IPv6Address("fe80::1:0:1") - net.IPv6Address("fe80::1") != net.IPv6Address("fe80::1:0:0:1") - net.IPv6Address("fe80::1") != net.IPv6Address("fe80:0:0:1::1") - net.IPv6Address("fe80::1") != net.IPv6Address("fe80:0:1::1") - net.IPv6Address("fe80::1") != net.IPv6Address("fe80:1::1") - net.IPv6Address("fe80::1") != net.IPv6Address("fe81::1") - net.IPv6Address("fe80::1%1") == net.IPv6Address("fe80::1%1") - net.IPv6Address("fe80::1%1") != net.IPv6Address("fe80::1%2") - net.IPv6Address("fe80::1%1") != net.IPv6Address("fe80::1") - net.IPv6Address("fe80::1%0") != net.IPv6Address("fe80::1") - } - // similarly IPv6Network() must be invoked prior to using IPv6CIDRString - ["IPv6Network =="] { - net.IPv6Network("2001:db8::/96") == net.IPv6Network("2001:db8::/96") - net.IPv6Network("2001:db8::/96") != net.IPv6Network("2001:db8::/48") - net.IPv6Network("2001:db8::/96") != net.IPv6Network("2001:db8::1/96") - } - ["IPv4AddressString"] { - "0.0.0.0" is net.IPv4AddressString - "127.0.0.1" is net.IPv4AddressString - "1.1.1.1" is net.IPv4AddressString - "000.000.000.000" is net.IPv4AddressString - "255.255.255.255" is net.IPv4AddressString - !("0000.0.0.0" is net.IPv4AddressString) - !("256.0.0.0" is net.IPv4AddressString) - !("260.0.0.0" is net.IPv4AddressString) - !("300.0.0.0" is net.IPv4AddressString) - !("1.1.1" is net.IPv4AddressString) - !("1.1.1." is net.IPv4AddressString) - !("1.1.1.1." is net.IPv4AddressString) - !("1.a.1.1" is net.IPv4AddressString) - !("1..1.1" is net.IPv4AddressString) - !("1..1.1.1" is net.IPv4AddressString) - !(".1.1.1" is net.IPv4AddressString) - !(".1.1.1.1" is net.IPv4AddressString) - !("" is net.IPv4AddressString) - } - ["IPv6AddressString (pure IPv6)"] { - "0:0:0:0:0:0:0:0" is net.IPv6AddressString - "1:2:3:4:5:6:7:8" is net.IPv6AddressString - "FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF" is net.IPv6AddressString - "ffff::" is net.IPv6AddressString - "::" is net.IPv6AddressString - "::1" is net.IPv6AddressString - "::1:2:3:4:5:6:7" is net.IPv6AddressString - "1::" is net.IPv6AddressString - "1:2:3:4:5:6:7::" is net.IPv6AddressString - "::1" is net.IPv6AddressString - "::1:2:3:4:5:6:7" is net.IPv6AddressString - "1:2:3:4:5:6::7" is net.IPv6AddressString - "1:2:3:4:5::6:7" is net.IPv6AddressString - "1:2:3:4::5:6:7" is net.IPv6AddressString - "1:2:3::4:5:6:7" is net.IPv6AddressString - "1:2::3:4:5:6:7" is net.IPv6AddressString - "1::2:3:4:5:6:7" is net.IPv6AddressString - "1:2:3:4:5::6" is net.IPv6AddressString - "1:2:3:4::5" is net.IPv6AddressString - "1:2:3::4" is net.IPv6AddressString - "1:2::3" is net.IPv6AddressString - "1::2" is net.IPv6AddressString - !("1:2:3:4:5:6:7:8:9" is net.IPv6AddressString) - !("1:2:3:4:5:6:7:" is net.IPv6AddressString) - !("1:2:3:4:5:6:7:8:" is net.IPv6AddressString) - !(":1:2:3:4:5:6:7" is net.IPv6AddressString) - !(":1:2:3:4:5:6:7:8" is net.IPv6AddressString) - !("::1:2:3:4:5:6:7:8" is net.IPv6AddressString) - !("1:2:3:4:5:6:7:8::" is net.IPv6AddressString) - !("1::2::" is net.IPv6AddressString) - !("::1::2" is net.IPv6AddressString) - !("1::g" is net.IPv6AddressString) - !("1:::2" is net.IPv6AddressString) - !("FFFFF::" is net.IPv6AddressString) - !("1.2.3.4" is net.IPv6AddressString) - !("" is net.IPv6AddressString) - } - ["IPv6AddressString (IPv4-compatible)"] { - "::0.0.0.0" is net.IPv6AddressString - "::000.000.000.000" is net.IPv6AddressString - "::255.255.255.255" is net.IPv6AddressString - !("::256.0.0.0" is net.IPv6AddressString) - !("::260.0.0.0" is net.IPv6AddressString) - !("::300.0.0.0" is net.IPv6AddressString) - !("::0000.0.0.0" is net.IPv6AddressString) - !("::a.0.0.0" is net.IPv6AddressString) - !("::0.0.0" is net.IPv6AddressString) - !("::0.0.0." is net.IPv6AddressString) - !("::0.0.0.0." is net.IPv6AddressString) - !("::0.0.0.0.0" is net.IPv6AddressString) - } - ["IPv6AddressString (IPv4-mapped)"] { - "::ffff:0.0.0.0" is net.IPv6AddressString - "::ffff:000.000.000.000" is net.IPv6AddressString - "::ffff:255.255.255.255" is net.IPv6AddressString - !("::ffff:256.0.0.0" is net.IPv6AddressString) - !("::ffff:260.0.0.0" is net.IPv6AddressString) - !("::ffff:300.0.0.0" is net.IPv6AddressString) - !("::ffff:0000.0.0.0" is net.IPv6AddressString) - !("::ffff:a.0.0.0" is net.IPv6AddressString) - !("::ffff:0.0.0" is net.IPv6AddressString) - !("::ffff:0.0.0." is net.IPv6AddressString) - !("::ffff:0.0.0.0." is net.IPv6AddressString) - !("::ffff:0.0.0.0.0" is net.IPv6AddressString) - } - ["IPv6AddressString (IPv4-translated)"] { - "::ffff:0:0.0.0.0" is net.IPv6AddressString - "::ffff:0000:0.0.0.0" is net.IPv6AddressString - "::ffff:0:255.255.255.255" is net.IPv6AddressString - "::ffff:0:000.000.000.000" is net.IPv6AddressString - !("::ffff:0:256.0.0.0" is net.IPv6AddressString) - !("::ffff:0:260.0.0.0" is net.IPv6AddressString) - !("::ffff:0:300.0.0.0" is net.IPv6AddressString) - !("::ffff:0:0000.0.0.0" is net.IPv6AddressString) - !("::ffff:0:a.0.0.0" is net.IPv6AddressString) - !("::ffff:0:0.0.0" is net.IPv6AddressString) - !("::ffff:0:0.0.0." is net.IPv6AddressString) - !("::ffff:0:0.0.0.0." is net.IPv6AddressString) - !("::ffff:0:0.0.0.0.0" is net.IPv6AddressString) - } - ["IPv6AddressString (IPv4-embedded)"] { - "64:ff9b::0.0.0.0" is net.IPv6AddressString - "0::0.0.0.0" is net.IPv6AddressString - "0:0:0:0::0.0.0.0" is net.IPv6AddressString - "ffff:ffff:ffff:ffff::255.255.255.255" is net.IPv6AddressString - "64:ff9b::255.255.255.255" is net.IPv6AddressString - "64:ff9b::000.000.000.000" is net.IPv6AddressString - !("64:ff9b::256.0.0.0" is net.IPv6AddressString) - !("64:ff9b::260.0.0.0" is net.IPv6AddressString) - !("64:ff9b::300.0.0.0" is net.IPv6AddressString) - !("64:ff9b::0000.0.0.0" is net.IPv6AddressString) - !("64:ff9b::a.0.0.0" is net.IPv6AddressString) - !("64:ff9b::0.0.0" is net.IPv6AddressString) - !("64:ff9b::0.0.0." is net.IPv6AddressString) - !("64:ff9b::0.0.0.0." is net.IPv6AddressString) - !("64:ff9b::0.0.0.0.0" is net.IPv6AddressString) - } - ["IPv6AddressString (miscellaneous IPv4)"] { - "0:0:0:0:0:0:0.0.0.0" is net.IPv6AddressString - "1:2:3:4:5:6:1.2.3.4" is net.IPv6AddressString - "FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:255.255.255.255" is net.IPv6AddressString - "::1:1.2.3.4" is net.IPv6AddressString - "::1:2:3:4:5:1.2.3.4" is net.IPv6AddressString - "1::1.2.3.4" is net.IPv6AddressString - "1:2:3:4:5::1.2.3.4" is net.IPv6AddressString - "1:2:3:4::5:1.2.3.4" is net.IPv6AddressString - "1:2:3::4:5:1.2.3.4" is net.IPv6AddressString - "1:2::3:4:5:1.2.3.4" is net.IPv6AddressString - "1::2:3:4:5:1.2.3.4" is net.IPv6AddressString - "1:2:3::4:1.2.3.4" is net.IPv6AddressString - "1:2::3:1.2.3.4" is net.IPv6AddressString - "1::2:1.2.3.4" is net.IPv6AddressString - "::1.2.3.4" is net.IPv6AddressString - "::0.0.0.0" is net.IPv6AddressString - !("1:2:3:4:5:6:7:1.2.3.4" is net.IPv6AddressString) - !("1:2:3:4:5:6:1.2.3" is net.IPv6AddressString) - !("1:2:3:4:5:6:1.2.3.4.5" is net.IPv6AddressString) - !("1:2:3:4:5:6::1.2.3.4" is net.IPv6AddressString) - !(":1:2:3:4:5:1.2.3.4" is net.IPv6AddressString) - !(":1:2:3:4:5:6:1.2.3.4" is net.IPv6AddressString) - !("::1:2:3:4:5:6:1.2.3.4" is net.IPv6AddressString) - !("1::2::1.2.3.4" is net.IPv6AddressString) - !("1::2::3:1.2.3.4" is net.IPv6AddressString) - !("::1::2:1.2.3.4" is net.IPv6AddressString) - !("::1::1.2.3.4" is net.IPv6AddressString) - !("1:::2:1.2.3.4" is net.IPv6AddressString) - !("FFFFF::1.2.3.4" is net.IPv6AddressString) - !("1.2.3.4::" is net.IPv6AddressString) - } - ["Scoped IPv6AddressString (link-local unicast)"] { - "fe80::%1" is net.IPv6AddressString - "FE80::%1" is net.IPv6AddressString - "fe80::%999" is net.IPv6AddressString - "fe80:0:0:0:0:0:0:0%1" is net.IPv6AddressString - "fe80:0:0:0:a:b:c:d%2" is net.IPv6AddressString - "fe80::aaaa%0" is net.IPv6AddressString - "fe80:0:0:0:1::%1" is net.IPv6AddressString - "fe80:0:0::1%2" is net.IPv6AddressString - !("fe80:1::%0" is net.IPv6AddressString) - !("fe80::a:b:c:d:e%0" is net.IPv6AddressString) - !("fe81::%1" is net.IPv6AddressString) - !("febf::%1" is net.IPv6AddressString) - !("fe80::%" is net.IPv6AddressString) - !("fe80::%a" is net.IPv6AddressString) - !("fe80::%eth0" is net.IPv6AddressString) - !("fe80:0:0:1::%1" is net.IPv6AddressString) - !("fe80::%-1" is net.IPv6AddressString) - !("fe80::% 1" is net.IPv6AddressString) - !("fe80::%%1" is net.IPv6AddressString) - !("fe80::%1%1" is net.IPv6AddressString) - } - ["Scoped IPv6AddressString (multicast)"] { - "ff00::%1" is net.IPv6AddressString - "FF00::%1" is net.IPv6AddressString - "ff0d::%1" is net.IPv6AddressString - "ff1d::%1" is net.IPv6AddressString - "fffd::%1" is net.IPv6AddressString // we don't restrict flags, for future compatibility reasons - "ff00::%999" is net.IPv6AddressString - "ff0d:0:0:0:0:0:0:0%0" is net.IPv6AddressString - "ff00::7:8%0" is net.IPv6AddressString - "ff00:2:3:4::8%0" is net.IPv6AddressString - !("ff0e::%1" is net.IPv6AddressString) // global scope - !("fffe::%1" is net.IPv6AddressString) // global scope - !("FF0E::%1" is net.IPv6AddressString) // global scope - !("ff00::%-1" is net.IPv6AddressString) - !("ff00::% 1" is net.IPv6AddressString) - !("ff00::%" is net.IPv6AddressString) - !("ff00::%a" is net.IPv6AddressString) - !("ff00::%eth0" is net.IPv6AddressString) - !("ff00::%1%1" is net.IPv6AddressString) - !("ff00:1:2:3:4:5:6:7:8%1" is net.IPv6AddressString) - } - ["Global IPv6AddressString can't be scoped"] { - !("::%0" is net.IPv6AddressString) // technically this is implementation-defined, we don't support it - !("::1%0" is net.IPv6AddressString) - !("0:0:0:0:0:0:0:0%0" is net.IPv6AddressString) - !("a:b:c:d:1:2:3:4%0" is net.IPv6AddressString) - !("fec0::%1" is net.IPv6AddressString) // site-local unicast is deprecated, considered global now - !("::1.2.3.4%0" is net.IPv6AddressString) // IPv6+IPv4 syntax implies global scope - !("::ffff:1.2.3.4%0" is net.IPv6AddressString) // IPv4-mapped can be scoped in some implementations, but not ours - !("0:fe80::%0" is net.IPv6AddressString) - !("::fe80%0" is net.IPv6AddressString) - !("fdff::%0" is net.IPv6AddressString) - } - ["IPv4CIDRString"] { - "0.0.0.0/1" is net.IPv4CIDRString - "255.255.255.255/32" is net.IPv4CIDRString - !("0.0.0.0/" is net.IPv4CIDRString) - !("0.0.0.0/100" is net.IPv4CIDRString) - !("0.0.0.0/a" is net.IPv4CIDRString) - !("/10" is net.IPv4CIDRString) - !("" is net.IPv4CIDRString) - } - ["IPv6CIDRString"] { - "fe80::/10" is net.IPv6CIDRString - "::1/128" is net.IPv6CIDRString - "::1.2.3.4/96" is net.IPv6CIDRString - "fe80::%2/10" is net.IPv6CIDRString - !("fe80::/" is net.IPv6CIDRString) - !("fe80::/1000" is net.IPv6CIDRString) - !("fe80::/a" is net.IPv6CIDRString) - !("/10" is net.IPv6CIDRString) - !("" is net.IPv6CIDRString) - !("0.0.0.0/10" is net.IPv6CIDRString) - } - ["IPv4AddressPortString"] { - "0.0.0.0:0" is net.IPv4AddressPortString - "255.255.255.255:65535" is net.IPv4AddressPortString - !("0.0.0.0:" is net.IPv4AddressPortString) - !("0.0.0.0:123456" is net.IPv4AddressPortString) - !(":1" is net.IPv4AddressPortString) - !("" is net.IPv4AddressPortString) - !("0.0.0.0:a" is net.IPv4AddressPortString) - !("[0.0.0.0]:1" is net.IPv4AddressPortString) - } - ["IPv6AddressPortString"] { - "[::1]:80" is net.IPv6AddressPortString - "[::]:1" is net.IPv6AddressPortString - "[fe80::]:65535" is net.IPv6AddressPortString - "[fe80::%2]:65535" is net.IPv6AddressPortString - !("[::1]:" is net.IPv6AddressPortString) - !("[::1]:123456" is net.IPv6AddressPortString) - !(":1" is net.IPv6AddressPortString) - !("" is net.IPv6AddressPortString) - !("[::1]:a" is net.IPv6AddressPortString) - !("0.0.0.0:1" is net.IPv6AddressPortString) - !("[0.0.0.0]:1" is net.IPv6AddressPortString) - !("::1:10" is net.IPv6AddressPortString) - } - ["IPv4Address()"] { - net.IPv4Address("127.0.0.1").repr == 0x7F000001 - net.IPv4Address("0.0.0.0").repr == 0 - net.IPv4Address("255.255.255.255").repr == 0xFFFFFFFF - net.IPv4Address("126.127.128.129").repr == 0x7E7F8081 - net.IPv4Address("172.16.254.1").repr == 0xAC10FE01 - } - ["IPv6Address()"] { - net.IPv6Address("fe80::1").repr == u128.UInt128(0xFE800000, 0, 0, 1) - net.IPv6Address("1:10:100:1000:ff00:0ff0:00ff:f00f").repr == u128.UInt128(0x10010, 0x1001000, 0xff000ff0, 0xfff00f) - net.IPv6Address("::a:b").repr == u128.UInt128(0, 0, 0, 0xa000b) - net.IPv6Address("2001::").repr == u128.UInt128(0x20010000, 0, 0, 0) - net.IPv6Address("::ffff:129.144.52.38").repr == u128.UInt128(0, 0, 0xffff, 0x81903426) - net.IPv6Address("fe80::127.0.0.1").repr == u128.UInt128(0xfe800000, 0, 0, 0x7f000001) - } - ["IPv6Address() scopeId"] { - net.IPv6Address("fe80::1").scopeId == null - net.IPv6Address("fe80::127.0.0.1").scopeId == null - net.IPv6Address("fe80:3::1ff:fe23:4567:890a").scopeId == null // non-standard alternative we don't support - net.IPv6Address("fe80::1%1").scopeId == 1 - net.IPv6Address("ff00::%2").scopeId == 2 - net.IPv6Address("fe80::%123").scopeId == 123 - net.IPv6Address("fe80::%0").scopeId == 0 - // check that manually-constructed addresses respect restrictions on scopeId - let (ip = net.IPv6Address("::")) module.catch(() -> (ip) { scopeId = 1 }.scopeId ) != null - let (ip = net.IPv6Address("::1")) module.catch(() -> (ip) { scopeId = 1 }.scopeId ) != null - let (ip = net.IPv6Address("fec0::")) module.catch(() -> (ip) { scopeId = 1 }.scopeId ) != null - let (ip = net.IPv6Address("2001:db8::")) module.catch(() -> (ip) { scopeId = 1 }.scopeId ) != null - let (ip = net.IPv6Address("ff0e::")) module.catch(() -> (ip) { scopeId = 1 }.scopeId ) != null - let (ip = net.IPv6Address("ff0f::")) module.catch(() -> (ip) { scopeId = 1 }.scopeId ) != null - let (ip = net.IPv6Address("fe7f::")) module.catch(() -> (ip) { scopeId = 1 }.scopeId ) != null - let (ip = net.IPv6Address("fe80:0:0:1::")) module.catch(() -> (ip) { scopeId = 1 }.scopeId ) != null - } - ["IPv6Address methods work with scopeId"] { - net.IPv6Address("fe80::%1").next() == net.IPv6Address("fe80::1%1") - net.IPv6Address("fe80::1:2%1").reverse() == net.IPv6Address("fe80::1:2").reverse() // scopeId gets ignored here - } - ["IPv6Address.isUnspecified"] { - net.IPv6Address("::").isUnspecified - !net.IPv6Address("::1").isUnspecified - !net.IPv6Address("1::").isUnspecified - } - ["IPv6Address.isLoopback"] { - net.IPv6Address("::1").isLoopback - !net.IPv6Address("::").isLoopback - !net.IPv6Address("1::").isLoopback - !net.IPv6Address("::127.0.0.1").isLoopback - !net.IPv6Address("::ffff:127.0.0.1").isLoopback - !net.IPv6Address("::ffff:0:127.0.0.1").isLoopback - !net.IPv6Address("64:ff9b::127.0.0.1").isLoopback - } - ["IPv6Address.isIPv4Mapped"] { - net.IPv6Address("::ffff:192.168.1.1").isIPv4Mapped - net.IPv6Address("::ffff:0.0.0.0").isIPv4Mapped - net.IPv6Address("::ffff:255.255.255.255").isIPv4Mapped - !net.IPv6Address("::192.168.1.1").isIPv4Mapped - !net.IPv6Address("::ffff:0:192.168.1.1").isIPv4Mapped - !net.IPv6Address("::ffff:255.255.255.255").next().isIPv4Mapped - !net.IPv6Address("::fffe:255.255.255.255").isIPv4Mapped - !net.IPv6Address("64:ff9b::192.168.1.1").isIPv4Mapped - } - ["IPv6Address.isIPv4Embedded"] { - net.IPv6Address("64:ff9b::192.168.1.1").isIPv4Embedded - net.IPv6Address("64:ff9b::0.0.0.0").isIPv4Embedded - net.IPv6Address("64:ff9b::255.255.255.255").isIPv4Embedded - !net.IPv6Address("64:ff9b::255.255.255.255").next().isIPv4Embedded - !net.IPv6Address("64:ff9a::255.255.255.255").isIPv4Embedded - !net.IPv6Address("::ffff:192.168.1.1").isIPv4Embedded - !net.IPv6Address("0:64:ff9b::192.168.1.1").isIPv4Embedded - } - ["IPv6Address.isNonGlobalScope"] { - // link-local unicast - net.IPv6Address("fe80::").isNonGlobalScope - net.IPv6Address("fe80::ffff:ffff:ffff:ffff").isNonGlobalScope - net.IPv6Address("fe80:0:0:0:1::").isNonGlobalScope - !net.IPv6Address("fe80:0:0:1::").isNonGlobalScope - !net.IPv6Address("fe81::").isNonGlobalScope - // multicast - net.IPv6Address("ff00::").isNonGlobalScope - net.IPv6Address("ff10::").isNonGlobalScope - net.IPv6Address("fffd::").isNonGlobalScope - !net.IPv6Address("ff0e::").isNonGlobalScope // scop=e - !net.IPv6Address("ff0f::").isNonGlobalScope // scop>e - !net.IPv6Address("fe00::").isNonGlobalScope - // addresses that don't count - !net.IPv6Address("::").isNonGlobalScope - !net.IPv6Address("::1").isNonGlobalScope - !net.IPv6Address("fec0::").isNonGlobalScope // site-local, deprecated - } - ["IPv6Network.contains() and scopeId"] { - net.IPv6Network("fe80::%1/96").contains(net.IPv6Address("fe80::1")) - net.IPv6Network("fe80::%1/96").contains(net.IPv6Address("fe80::1%1")) - net.IPv6Network("fe80::/96").contains(net.IPv6Address("fe80::1%1")) - !net.IPv6Network("fe80::%1/96").contains(net.IPv6Address("fe80::1%2")) - } - ["IPv6Network.subdivideTo() preserves scopeId"] { - net.IPv6Network("fe80::%1/64").subdivideTo(67).toList().every((it) -> it.base.scopeId == 1) - net.IPv6Network("fe80::/64").subdivideTo(67).toList().every((it) -> it.base.scopeId == null) - } - ["IPv6Network first/last address preserves scopeId"] { - net.IPv6Network("fe80::%1/64").firstAddress.scopeId == 1 - net.IPv6Network("fe80::%1/64").lastAddress.scopeId == 1 - net.IPv6Network("fe80::/64").firstAddress.scopeId == null - net.IPv6Network("fe80::/64").lastAddress.scopeId == null - } - ["IPv6Range() preserves scopeId as appropriate"] { - net.IPv6Range(net.IPv6Address("fe80::%1"), net.IPv6Address("fe80::10%1")).toList().every((ip) -> ip.scopeId == 1) - net.IPv6Range(net.IPv6Address("fe80::%1"), net.IPv6Address("fe80::10%2")).toList().every((ip) -> ip.scopeId == null) - net.IPv6Range(net.IPv6Address("fe80::"), net.IPv6Address("fe80::10%1")).toList().every((ip) -> ip.scopeId == null) - net.IPv6Range(net.IPv6Address("fe80::%1"), net.IPv6Address("fe80::10")).toList().every((ip) -> ip.scopeId == null) - net.IPv6Range(net.IPv6Address("fe80::"), net.IPv6Address("fe80::10")).toList().every((ip) -> ip.scopeId == null) - } - ["IPv4AddressString can't contain unicode digits"] { - // U+FF11 is FULLWIDTH DIGIT ONE - !("25\u{FF11}.0.0.0" is net.IPv4AddressString) - !("20\u{FF11}.0.0.0" is net.IPv4AddressString) - !("2\u{FF11}0.0.0.0" is net.IPv4AddressString) - !("10\u{FF11}.0.0.0" is net.IPv4AddressString) - !("1\u{FF11}0.0.0.0" is net.IPv4AddressString) - !("\u{FF11}00.0.0.0" is net.IPv4AddressString) - !("00\u{FF11}.0.0.0" is net.IPv4AddressString) - !("0\u{FF11}0.0.0.0" is net.IPv4AddressString) - !("0\u{FF11}.0.0.0" is net.IPv4AddressString) - !("\u{FF11}0.0.0.0" is net.IPv4AddressString) - !("\u{FF11}.0.0.0" is net.IPv4AddressString) - } - ["IPv6AddressString can't contain unicode digits"] { - !("\u{FF11}::" is net.IPv6AddressString) - !("fe80::%\u{FF11}" is net.IPv6AddressString) - !("::25\u{FF11}.0.0.0" is net.IPv6AddressString) - !("::20\u{FF11}.0.0.0" is net.IPv6AddressString) - !("::2\u{FF11}0.0.0.0" is net.IPv6AddressString) - !("::10\u{FF11}.0.0.0" is net.IPv6AddressString) - !("::1\u{FF11}0.0.0.0" is net.IPv6AddressString) - !("::\u{FF11}00.0.0.0" is net.IPv6AddressString) - !("::00\u{FF11}.0.0.0" is net.IPv6AddressString) - !("::0\u{FF11}0.0.0.0" is net.IPv6AddressString) - !("::0\u{FF11}.0.0.0" is net.IPv6AddressString) - !("::\u{FF11}0.0.0.0" is net.IPv6AddressString) - !("::\u{FF11}.0.0.0" is net.IPv6AddressString) - } - ["IPv4AddressPortString can't contain unicode digits in the port"] { - !("127.0.0.1:\u{FF11}" is net.IPAddressPortString) - } - ["IPv6AddressPortString can't contain unicode digits in the port"] { - !("[::1]:\u{FF11}" is net.IPAddressPortString) - } - ["IPv4CIDRString can't contain unicode digits in the suffix"] { - !("10.0.0.0/\u{FF11}" is net.IPv4CIDRString) - } - ["IPv6CIDRString can't contain unicode digits in the suffix"] { - !("::1/\u{FF11}" is net.IPv6CIDRString) - } +// language=RegExp +const hidden ipv4String = #"(\#(decByte)\.){3}\#(decByte)"# + +// language=RegExp +const hidden ipv6String = #""" + (?x: + (\#(hex){1,4}:){7}\#(hex){1,4} # x:x:x:x:x:x:x:x + | (\#(hex){1,4}:){1,7}: # x:: … x:x:x:x:x:x:x:: + | (\#(hex){1,4}:){6}:\#(hex){1,4} # x:x:x:x:x:x::x + | (\#(hex){1,4}:){5}(:\#(hex){1,4}){1,2} # x:x:x:x:x::x … x:x:x:x:x::x:x + | (\#(hex){1,4}:){4}(:\#(hex){1,4}){1,3} # x:x:x:x::x … x:x:x:x::x:x:x + | (\#(hex){1,4}:){3}(:\#(hex){1,4}){1,4} # x:x:x::x … x:x:x::x:x:x:x + | (\#(hex){1,4}:){2}(:\#(hex){1,4}){1,5} # x:x::x … x:x::x:x:x:x:x + | \#(hex){1,4}:(:\#(hex){1,4}){1,6} # x::x … x::x:x:x:x:x:x + | :((:\#(hex){1,4}){1,7}|:) # ::x … ::x:x:x:x:x:x:x, :: + | (?: # IPv6 with trailing IPv4 + (\#(hex){1,4}:){6} # x:x:x:x:x:x: + | (\#(hex){1,4}:){5}: # x:x:x:x:x:: + | (\#(hex){1,4}:){4}(:\#(hex){1,4})?: # x:x:x:x:: … x:x:x:x::x: + | (\#(hex){1,4}:){3}(:\#(hex){1,4}){0,2}: # x:x:x:: … x:x:x::x:x: + | (\#(hex){1,4}:){2}(:\#(hex){1,4}){0,3}: # x:x:: … x:x::x:x:x: + | \#(hex){1,4}:(:\#(hex){1,4}){0,4}: # x:: … x::x:x:x:x: + | :(:\#(hex){1,4}){0,5}: # :: … ::x:x:x:x:x: + )\#(ipv4String) # …d.d.d.d + | \#(nonGlobalIPv6String)%[0-9]+ # Scoped address + ) + """# + +// language=RegExp +const hidden nonGlobalIPv6String = #""" + (?xi-U: + ff[0-9a-f][0-9a-d](?: # multicast (FF00::/8 with "scop" less than E (global scope)) + (:[0-9a-f]{1,4}){7} # FFxx:x:x:x:x:x:x:x + | :(:|(:[0-9a-f]{1,4}){1,6}) # FFxx:: … FFxx::x:x:x:x:x:x + | :[0-9a-f]:(:|(:[0-9a-f]{1,4}){1,5}) # FFxx:x:: … FFxx:x::x:x:x:x:x + | (:[0-9a-f]){2}:(:|(:[0-9a-f]{1,4}){1,4}) # FFxx:x:x:: … FFxx:x:x::x:x:x:x + | (:[0-9a-f]){3}:(:|(:[0-9a-f]{1,4}){1,3}) # FFxx:x:x:x:: … FFxx:x:x:x::x:x:x + | (:[0-9a-f]){4}:(:|(:[0-9a-f]{1,4}){1,2}) # FFxx:x:x:x:x:: … FFxx:x:x:x:x::x:x + | (:[0-9a-f]){5}:(:|(:[0-9a-f]{1,4})) # FFxx:x:x:x:x:x:: … FFxx:x:x:x:x:x::x + | (:[0-9a-f]){6}:: # FFxx:x:x:x:x:x:x:: + ) + | fe80(?: # link-local unicast (FE80::/10) + (:0{1,4}){3}(?: # FE80:0:0:0… + :: | :(:[0-9a-f]{1,4}){1,3} # FE80:0:0:0:: … FE80:0:0:0::x:x:x + | :[0-9a-f]{1,4}:(:|(:[0-9a-f]{1,4}){1,2}) # FE80:0:0:0:x:: … FE80:0:0:0:x::x:x + | (:[0-9a-f]{1,4}){2}::([0-9a-f]{1,4})? # FE80:0:0:0:x:x:: … FE80:0:0:0:x:x::x + | (:[0-9a-f]{1,4}){3}:(:|[0-9a-f]{1,4}) # FE80:0:0:0:x:x:x::, FE80:0:0:0:x:x:x:x + ) + | (:0{1,4}){0,2}:(:|(:[0-9a-f]{1,4}){1,4}) # FE80:: … FE80:0:0::x:x:x:x + ) + ) + """# + +/// A string that contains a MAC address +// language=RegExp +typealias MACAddressString = String(matches(Regex(#"(\#(net.hex){1,2}[\.:-]){5}(\#(net.hex){1,2})"#))) + +/// A string that contains either an IPv4 or IPv6 address. +typealias IPAddressString = IPv4AddressString|IPv6AddressString + +/// A string that contains either an IPv4 or IPv6 address and port. +typealias IPAddressPortString = IPv4AddressPortString|IPv6AddressPortString + +/// A string that contains either an IPv4 or IPv6 CIDR range. +typealias IPCIDRString = IPv4CIDRString|IPv6CIDRString + +/// An IPv4 or IPv6 address. +typealias IPAddress = IPv4Address|IPv6Address + +/// An IPv4 or IPv6 network. +typealias IPNetwork = IPv4Network|IPv6Network + +/// A string that contains an IPv4 address. +// language=RegExp +typealias IPv4AddressString = String(matches(Regex(net.ipv4String))) + +/// A string that contains an IPv6 address. +/// +/// IPv6 unicast and multicast addresses of non-global scope except for the unspecified address and the loopback address +/// may include a scope ID suffix, e.g. `"fe80::1234%1"`. Site-local unicast addresses (`fec0::/10`) are deprecated and +/// thus are considered to be global unicast addresses. +// language=RegExp +typealias IPv6AddressString = String(matches(Regex(net.ipv6String))) + +/// A string that contains an IPv4 address and port. +// language=RegExp +typealias IPv4AddressPortString = String(matches(Regex(#"\#(net.ipv4String):[0-9]{1,5}"#))) + +/// A string that contains an IPv6 address and port. +// language=RegExp +typealias IPv6AddressPortString = String(matches(Regex(#"\[\#(net.ipv6String)\]:[0-9]{1,5}"#))) + +/// A string that contains an IPv4 address. +// language=RegExp +typealias IPv4CIDRString = String(matches(Regex(#"\#(net.ipv4String)/[0-9]{1,2}"#))) + +/// A string that contains an IPv6 address. +// language=RegExp +typealias IPv6CIDRString = String(matches(Regex(#"\#(net.ipv6String)/[0-9]{1,3}"#))) + +/// Creates an [IPAddress] from an [IPAddressString]. +function IP(ip: IPAddressString): IPAddress = + if (ip is IPv6AddressString) IPv6Address(ip) + else if (ip is IPv4AddressString) IPv4Address(ip) + else throw("Invalid IP: \(ip)") + +/// Creates an [IPv4Address] from an [IPv4AddressString]. +function IPv4Address(ip: IPv4AddressString): IPv4Address = new { + local parts = ip.split(".") + repr = parts[0].toInt().shl(24) + .or(parts[1].toInt().shl(16)) + .or(parts[2].toInt().shl(8)) + .or(parts[3].toInt()) } -examples { - ["expandIPv6AddressString"] { - net.expandIPv6AddressString("fe80::") - net.expandIPv6AddressString("fe80::1") - net.expandIPv6AddressString("::aa") - } - ["expandIPv6AddressString with scoped addresses"] { - net.expandIPv6AddressString("fe80:0000:0000:0000:0000:0000:0000:00%1") // 39 bytes long - net.expandIPv6AddressString("fe80::%0") - net.expandIPv6AddressString("fe80:0:0:0:5:6:7:8%1") - net.expandIPv6AddressString("fe80::%000") // we don't canonicalize scope ids here - net.expandIPv6AddressString("fe80::%001") // that happens in compressIPv6AddressString - } - ["expandIPv6AddressString with IPv4 suffix"] { - net.expandIPv6AddressString("ffff:ffff:ffff:ffff:0:0:255.255.255.255") // 39 bytes long - net.expandIPv6AddressString("::1.2.3.4") - net.expandIPv6AddressString("2001:db8::1.2.3.4") - net.expandIPv6AddressString("a:b:c:d:e:f:001.000.030.000") - } - ["compressIPv6AddressString"] { - net.compressIPv6AddressString("0001:0002:0003:0004:0005:0006:0007:0008") - net.compressIPv6AddressString("fe80:000:000:000:1:000:000:1") - net.compressIPv6AddressString("fe80::0001:0000:0000:0001") - net.compressIPv6AddressString("1:2:3:4:0:6:7:8") // :: shouldn't compress a single field - net.compressIPv6AddressString("1:2:3:4::6:7:8") - net.compressIPv6AddressString("::") - net.compressIPv6AddressString("::0:0") - net.compressIPv6AddressString("FE80::A:B:C:D:E:F") - net.compressIPv6AddressString("2001:0:0:1:0:0:0:1") - net.compressIPv6AddressString("2001:db8:0:0:1:0:0:1") - } - ["compressIPv6AddressString with scoped addresses"] { - net.compressIPv6AddressString("fe80:0000:0000:0000:0005:0006:0007:0008%0") - net.compressIPv6AddressString("fe80::1:0%0") - net.compressIPv6AddressString("fe80::%0") - net.compressIPv6AddressString("fe80::%1") - net.compressIPv6AddressString("fe80::%000") // we canonicalize scope ids here - net.compressIPv6AddressString("fe80::%001") - } - ["compressIPv6AddressString with IPv4 suffix"] { - net.compressIPv6AddressString("::ffff:1.2.3.4") - net.compressIPv6AddressString("0000:0000:0000:0000:0000:ffff:001.020.000.004") - net.compressIPv6AddressString("fe80:0:0:0::1.2.3.4") - net.compressIPv6AddressString("fe80:0:0:0:0:0:0.0.0.0") - net.compressIPv6AddressString("::0.0.0.0") +/// An IPv4 address. +class IPv4Address { + repr: UInt32 + + const hidden bitWidth: UInt = 32 + + local self = this + + /// maskHi generates a mask of 1s in the top [prefix] bits of a [UInt32]. + function maskHi(prefix: UInt(isBetween(0, bitWidth))): UInt32 = mask32Hi(prefix) + + /// maskLo generates a mask of 1s in the bottom [suffix] bits of a [UInt32]. + function maskLo(suffix: UInt(isBetween(0, bitWidth))): UInt32 = mask32Lo(suffix) + + /// reverse returns the PTR record name for this address. + function reverse(): String = new IPv4Network { base = self; prefix = self.bitWidth }.reverse() + + /// return the ip address immediately after this one + function next(): IPv4Address = add(1) + + /// return the ip address [n] after this one + function add(n: UInt32): IPv4Address = new { repr = self.repr + n } + + function toString(): IPv4AddressString = new Listing { + repr.ushr(24).and(math.maxUInt8).toString() + repr.ushr(16).and(math.maxUInt8).toString() + repr.ushr(8).and(math.maxUInt8).toString() + repr.and(math.maxUInt8).toString() + }.join(".") +} + +// noinspection TypeMismatch +/// Creates an [IPv6Address] from an [IPv6AddressString]. +function IPv6Address(ip: IPv6AddressString): IPv6Address = + let (idx = ip.lastIndexOfOrNull("%")) + let (scope = idx.ifNonNull((it) -> ip.drop(it+1).toInt())) + let (ip = idx.ifNonNull((it) -> ip.take(it)) ?? ip) + let (ipIdx = ip.indexOfOrNull(Regex(":\(ipv4String)$"))) + let (ipv4 = ipIdx.ifNonNull((_) -> ip.drop(ipIdx+1).split(".").map((s) -> s.toInt()))) + let (ip = ipIdx.ifNonNull((it) -> ip.replaceRange(it, ip.length, ":0:0")) ?? ip) + let (_ip = expandIPv6AddressString(ip).toLowerCase().split(":")) + new { + repr = u128.UInt128( + parseHex32(_ip[0] + _ip[1]), + parseHex32(_ip[2] + _ip[3]), + parseHex32(_ip[4] + _ip[5]), + if (ipv4 != null) ipv4[0].shl(24) + ipv4[1].shl(16) + ipv4[2].shl(8) + ipv4[3] + else parseHex32(_ip[6] + _ip[7]) + ) + scopeId = scope + } + +/// An IPv6 address. +class IPv6Address { + repr: u128.UInt128 + + /// The scope ID associated with this address, or [null] if the scope is unknown or not applicable. + /// + /// Only addresses of [non-global scope][isNonGlobalScope] are allowed to have a scope ID. + scopeId: UInt(isNonGlobalScope)? + + const hidden bitWidth: UInt = 128 + + /// Tells if this address is the unspecified address (`::`). + fixed hidden isUnspecified: Boolean = repr == u128.zero + + /// Tells if this address is the loopback address (`::1`). + fixed hidden isLoopback: Boolean = repr == u128.one + + /// Tells if this address is an IPv4-mapped address (`::ffff:0:0/96`). + fixed hidden isIPv4Mapped: Boolean = + repr.hihi == 0 && repr.hilo == 0 && repr.lohi == 0xffff + + /// Tells if this address is an IPv4-embedded address using the Well-Known Prefix (`64:ff9b::/96`) described by + /// [RFC 6052 §2.1](https://tools.ietf.org/html/rfc6052#section-2.1). + fixed hidden isIPv4Embedded: Boolean = + repr.hihi == 0x64ff9b && repr.hilo == 0 && repr.lohi == 0 + + /// Tells if this address has non-global unicast or multicast scope. + /// + /// Returns [true] for link-local unicast and for multicast with "scop" field less than global. Returns [false] for + /// the [unspecified address][isUnspecified] and the [loopback address][isLoopback], and for site-local addresses as + /// per [RFC 4291 §2.5.7](https://tools.ietf.org/html/rfc4291#section-2.5.7). + fixed hidden isNonGlobalScope: Boolean = + (repr.hihi == 0xfe800000 && repr.hilo == 0) || + (repr.hihi.ushr(24) == 0xff && repr.hihi.ushr(16).and(0xF) < 0xE) + + local self = this + + /// maskHi generates a mask of 1s in the top [prefix] bits of a [u128.UInt128]. + function maskHi(prefix: UInt(isBetween(0, bitWidth))): u128.UInt128 = + if (prefix <= 32) u128.UInt128(mask32Hi(prefix), 0, 0, 0) + else if (prefix <= 64) u128.UInt128(math.maxUInt32, mask32Hi(prefix - 32), 0, 0) + else if (prefix <= 96) u128.UInt128(math.maxUInt32, math.maxUInt32, mask32Hi(prefix - 64), 0) + else u128.UInt128(math.maxUInt32, math.maxUInt32, math.maxUInt32, mask32Hi(prefix - 96)) + + /// maskLo generates a mask of 1s in the bottom [suffix] bits of a [u128.UInt128]. + function maskLo(suffix: UInt(isBetween(0, bitWidth))): u128.UInt128 = + if (suffix <= 32) u128.UInt128(0, 0, 0, mask32Lo(suffix)) + else if (suffix <= 64) u128.UInt128(0, 0, mask32Lo(suffix - 32), math.maxUInt32) + else if (suffix <= 96) u128.UInt128(0, mask32Lo(suffix - 64), math.maxUInt32, math.maxUInt32) + else u128.UInt128(mask32Lo(suffix - 96), math.maxUInt32, math.maxUInt32, math.maxUInt32) + + /// reverse returns the PTR record name for this address. + function reverse(): String = new IPv6Network { base = self; prefix = self.bitWidth }.reverse() + + /// return the ip address immediately after this one + function next(): IPv6Address = add(1) + + /// return the ip address [n] after this one + function add(n: UInt32): IPv6Address = (self) { repr = self.repr.add(u128.UInt128(0, 0, 0, n)) } + + /// Returns the [IPv6Address] in the canonical style described by [RFC 5952](https://tools.ietf.org/html/rfc5952). + /// + /// This includes returning the address in the alternative IPv4-suffixed format if it is [IPv4-mapped][isIPv4Mapped] + /// or [IPv4-embedded][isIPv4Embedded]. + function toString(): IPv6AddressString = _compressIPv6Groups(new Listing { + repr.hihi.ushr(16).toRadixString(16) + repr.hihi.and(math.maxUInt16).toRadixString(16) + repr.hilo.ushr(16).toRadixString(16) + repr.hilo.and(math.maxUInt16).toRadixString(16) + repr.lohi.ushr(16).toRadixString(16) + repr.lohi.and(math.maxUInt16).toRadixString(16) + when (isIPv4Mapped || isIPv4Embedded) { + new Listing { + repr.lolo.ushr(24).toString() + repr.lolo.ushr(16).and(0xFF).toString() + repr.lolo.ushr(8).and(0xFF).toString() + repr.lolo.and(0xFF).toString() + }.join(".") + } else { + repr.lolo.ushr(16).toRadixString(16) + repr.lolo.and(math.maxUInt16).toRadixString(16) + } + }.join(":")) + (scopeId.ifNonNull((id) -> "%\(id)") ?? "") + + /// Returns the [IPv6Address] as a string in expanded form. + /// + /// Example: + /// ``` + /// IPv6Address("fe80::1").toExpandedString() == "fe80:0000:0000:0000:0000:0000:0000:0001" + /// ``` + function toExpandedString(): IPv6AddressString = expandIPv6AddressString(new Listing { + repr.hihi.ushr(16).toRadixString(16) + repr.hihi.and(math.maxUInt16).toRadixString(16) + repr.hilo.ushr(16).toRadixString(16) + repr.hilo.and(math.maxUInt16).toRadixString(16) + repr.lohi.ushr(16).toRadixString(16) + repr.lohi.and(math.maxUInt16).toRadixString(16) + repr.lolo.ushr(16).toRadixString(16) + repr.lolo.and(math.maxUInt16).toRadixString(16) + }.join(":")) + (scopeId.ifNonNull((id) -> "%\(id)") ?? "") +} + +/// Creates an [IPNetwork] from an IPv4 or IPv6 CIDR block string +function IPNetwork(cidr: IPCIDRString): IPNetwork = + if (cidr is IPv4CIDRString) IPv4Network(cidr) + else if (cidr is IPv6CIDRString) IPv6Network(cidr) + else throw("Invalid network CIDR: \(cidr)") + +/// Creates an [IPv4Network] from an IPv4 CIDR block string +function IPv4Network(cidr: IPv4CIDRString): IPv4Network = new { + base = IPv4Address(cidr.split("/").first) + prefix = cidr.split("/").last.toInt() +} + +/// An IPv4 network. +class IPv4Network { + /// The base address of this network + base: IPv4Address + + /// The CIDR prefix of this network + prefix: UInt(isBetween(0, bitWidth)) + + fixed hidden bitWidth = base.bitWidth + + const hidden reverseBitResolution: UInt = 8 + + local self = this + + /// The first address in this network. + /// Will be equivalent to [lastAddress] when [prefix] is equal to [bitWidth]. + fixed firstAddress: IPv4Address = new { repr = base.repr.and(base.maskHi(prefix)) } + + /// The last address in this network. + /// Will be equivalent to [firstAddress] when [prefix] is equal to [bitWidth]. + fixed lastAddress: IPv4Address = new { repr = base.repr.or(base.maskLo(bitWidth - prefix)) } + + /// Return the subnet-mask for this network. + function getSubnetMask(): IPv4AddressString = new IPv4Address { repr = base.maskHi(prefix) }.toString() + + /// Return true if this network contains [ip]. + function contains(ip: IPv4Address): Boolean = firstAddress.repr <= ip.repr && ip.repr <= lastAddress.repr + + /// Generate the name of the reverse DNS zone for this network. + function reverse(): String = base.toString().split(".").take(prefix ~/ reverseBitResolution).reverse().join(".") + ".in-addr.arpa" + + /// Calculate all subnets of this network with prefix [target]. + /// For example, given IPv4Network("10.53.120.0/21").subdivideTo(24), it outputs 8 /24 networks + function subdivideTo(target: UInt(isBetween(0, bitWidth) && this >= prefix)): Listing = + if (prefix == target) new { self } + else new { + ...new IPv4Network { base = self.base; prefix = self.prefix + 1 }.subdivideTo(target) + ...new IPv4Network { base = new { repr = self.base.repr + 1.shl(bitWidth - self.prefix - 1) }; prefix = self.prefix + 1 }.subdivideTo(target) + } + + function toString(): String = "\(base.toString())/\(prefix)" +} + +/// Produces a listing of IPv4 addresses between [start] and [end], inclusive. +function IPv4Range(start: IPv4Address, end: IPv4Address): Listing = new { + for (ipu in IntSeq(start.repr, end.repr)) { + new { repr = ipu } } - ["IPv6Address.toString()"] { - net.IPv6Address("fe80::1").toString() - net.IPv6Address("101:1001:1010:110:100:1000:1100:1110").toString() - net.IPv6Address("::").toString() - net.IPv6Address("::0:1").toString() - net.IPv6Address("fe80::0").toString() - net.IPv6Address("fe80:0000:0000:0000:0000:0000:0001:0000").toString() - net.IPv6Address("::ffff:7f00:1").toString() - net.IPv6Address("64:ff9b::c000:0221").toString() - net.IPv6Address("2001:db8:122:344::192.0.2.33").toString() - net.IPv6Address("fe80::1%1").toString() - net.IPv6Address("ff11::1%02").toString() - net.IPv6Address("fe80::%0").toString() +} + +/// Creates an [IPv6Network] from an IPv6 CIDR block string +function IPv6Network(cidr: IPv6CIDRString): IPv6Network = new { + base = IPv6Address(cidr.split("/").first) + prefix = cidr.split("/").last.toInt() +} + +/// An IPv6 network. +class IPv6Network { + /// The base address of this network + base: IPv6Address + + /// The CIDR prefix of this network + prefix: UInt(isBetween(0, bitWidth)) + + fixed hidden bitWidth = base.bitWidth + + const hidden reverseBitResolution: UInt = 4 + + local self = this + + /// The first address in this network. + /// Will be equivalent to [lastAddress] when [prefix] is equal to [bitWidth]. + fixed firstAddress: IPv6Address = (base) { repr = base.repr.and(base.maskHi(prefix)) } + + /// The last address in this network. + /// Will be equivalent to [firstAddress] when [prefix] is equal to [bitWidth]. + fixed lastAddress: IPv6Address = (base) { repr = base.repr.or(base.maskLo(bitWidth - prefix)) } + + /// Return true if this network contains [ip]. + /// + /// If both [base.scopeId][IPv6Address.scopeId] and [ip.scopeId][IPv6Address.scopeId] are non-[null], they must match. + /// If either is [null] then scope ID is ignored. + function contains(ip: IPv6Address): Boolean = firstAddress.repr.le(ip.repr) && ip.repr.le(lastAddress.repr) && + (base.scopeId == ip.scopeId || base.scopeId == null || ip.scopeId == null) + + /// Generate the name of the reverse DNS zone for this network. + function reverse(): String = (base) { scopeId = null }.toExpandedString().replaceAll(":", "").chars.take(prefix ~/ reverseBitResolution).reverse().join(".") + ".ip6.arpa" + + /// Calculate all subnets of this network with prefix [target]. + /// + /// For example, given `IPv6Network("2620:149:a:960::/61").subdivideTo(64)`, it outputs 8 /64 networks + function subdivideTo(target: UInt(isBetween(0, bitWidth) && this >= prefix)): Listing = + if (prefix == target) new { self } + else new { + ...(self) { prefix = self.prefix + 1 }.subdivideTo(target) + ...(self) { base { repr = self.base.repr.add(u128.one.shl(bitWidth - self.prefix - 1)) }; prefix = self.prefix + 1 }.subdivideTo(target) + } + + function toString(): String = "\(base)/\(prefix)" +} + +/// Produces a listing of IPv6 addresses between [start] and [end], inclusive. +/// +/// If both [start.scopeId][IPv6Address.scopeId] and [end.scopeId][IPv6Address.scopeId] are equal, the resulting +/// addresses will have the same scope ID, otherwise their [scopeId][IPv6Address.scopeId] will be [null]. +function IPv6Range(start: IPv6Address, end: IPv6Address): Listing = new { + local _scopeId = if (start.scopeId == end.scopeId) start.scopeId else null + for (ipu in start.repr.seq(end.repr)) { + new { repr = ipu; scopeId = _scopeId } } - ["IPv6Address.toExpandedString()"] { - net.IPv6Address("fe80::1").toExpandedString() - net.IPv6Address("::").toExpandedString() - net.IPv6Address("::1").toExpandedString() - net.IPv6Address("2001:db8::").toExpandedString() - net.IPv6Address("fe80::1%1").toExpandedString() - net.IPv6Address("fe80::1%0").toExpandedString() +} + +// noinspection TypeMismatch +/// Canonicalizes IPv6 addresses by expanding each component to be 4 digits (zero-padded) and expanding `::` +/// +/// Given input `"123:45:6::7890"`, the output is `"0123:0045:0006:0000:0000:0000:0000:7890"`. +/// +/// If the input is in the alternate form with a trailing IPv4 address, the trailing IPv4 address will be kept intact, +/// although any leading zeroes on the IPv4 components will be dropped. +const function expandIPv6AddressString(_ip: IPv6AddressString): IPv6AddressString = + let (idx = _ip.lastIndexOfOrNull("%")) + if ((idx ?? _ip.length) == 39 && !_ip.contains(".")) _ip // assume it's already canonicalized + else + let (scope = idx.ifNonNull((it) -> _ip.drop(it))) + let (ip = idx.ifNonNull((it) -> _ip.take(it)) ?? _ip) + let (ipIdx = ip.indexOfOrNull(Regex(":\(ipv4String)$"))) + let (ipv4 = ipIdx.ifNonNull((it) -> ip.drop(it).replaceAll(Regex("0+(?=[0-9])"), ""))) + let (ip = ipIdx.ifNonNull((it) -> if (ip[it-1] == ":") ip.take(it+1) else ip.take(it)) ?? ip) + let (ip = if (ip.endsWith("::")) ip + "0" else ip) + let (groupCount = if (ipv4 == null) 8 else 6) + let (stuff = ip.split("::").map((half) -> half.split(":").map((octet) -> octet.padStart(4, "0")))) + (if (stuff.length == 1) stuff.first.join(":") + else if (stuff.length == 2) (stuff.first + List("0000").repeat(groupCount - stuff.first.length - stuff.last.length) + stuff.last).join(":") + else throw("unintelligible IPv6 address: " + _ip)) + + (ipv4 ?? "") + (scope ?? "") + +// noinspection TypeMismatch +/// Compresses IPv6 addresses by stripping leading zeros from each component and collapsing repeated zero components to `::`. +/// +/// Given input `"0123:0045:0006:0000:0000:0000:0000:7890"`, the output is `"123:45:6::7890"`. +/// +/// If there are multiple possible spans of zeroes to collapse, the longest span is chosen. If there are multiple +/// candidates for longest span, the first such span is chosen. The output is consistent with +/// [RFC5952 §4.2.3](https://datatracker.ietf.org/doc/html/rfc5952#section-4.2.3). +const function compressIPv6AddressString(ip: IPv6AddressString): IPv6AddressString = + let (ip = expandIPv6AddressString(ip).toLowerCase()) + let (idx = ip.lastIndexOfOrNull("%")) + let (scope = idx.ifNonNull((it) -> ip.drop(it).replaceFirst(Regex("^%0+(?=[0-9])"), "%"))) + let (ip = idx.ifNonNull((it) -> ip.take(it)) ?? ip) + let (trimmed = ip.split(":").map((octet) -> + if (octet == "0000") "0" + else if (octet.contains(".")) octet // must be a trailing IPv4 address + else octet.dropWhile((c) -> c == "0") + ).join(":")) + _compressIPv6Groups(trimmed) + (scope ?? "") + +// Performs only the portion of IPv6 address canonicalization that is replacing the longest sequence of zeroes +// with `::`. +local const function _compressIPv6Groups(ip: String): IPv6AddressString = + let (match = Regex(#"(?<=:|^)(?:0:)+0(?=:|$)"#).findMatchesIn(ip).maxByOrNull((it) -> it.value.length)) + if (match == null) ip // no groups to replace with :: + else if (match.start == 0 && match.end == ip.length) "::" + else if (match.start == 0 || match.end == ip.length) ip.replaceRange(match.start, match.end, ":") + else ip.replaceRange(match.start, match.end, "") + +function MACAddress(mac: MACAddressString): MACAddress = new { + repr = mac.split(Regex("[.:-]")).map((octet) -> parseHexOctet(octet)) +} + +class MACAddress { + hidden repr: List(length == 6) + + local self = this + + function toString(): MACAddressString = repr.map((octet) -> octet.toRadixString(16).padStart(2, "0")).join(":") + + function eui64(addr: IPv6Address): IPv6Address = new { + repr = u128.UInt128( + addr.repr.hihi, + addr.repr.hilo, + uint32FromBytes(self.repr[0].xor(0x02), self.repr[1], self.repr[2], 0xff), + uint32FromBytes(0xfe, self.repr[3], self.repr[4], self.repr[5]) + ) } -} \ No newline at end of file +} + +/// parseHex tranforms a single hexadecimal character into its unsigned integer representation. +function parseHex(digit: Char): UInt8 = + let (d = digit.toLowerCase()) + "0123456789abcdef".chars.findIndexOrNull((it) -> it == d) ?? + throw("Unrecognized hex digit: \(d)") + +/// parseHexOctet tranforms a two hexadecimal characters into its unsigned integer representation. +function parseHexOctet(octet: String(length == 2)): UInt8 = byteLut[octet.toLowerCase()] + +/// parseHex32 transforms an 8 character hexidecimal string into its UInt32 representation. +function parseHex32(s: String(length == 8)): UInt32 = + IntSeq(0, 7) + .step(2) + .map((it) -> s.substring(it, it + 2)) + .fold(0, (acc, it) -> acc.shl(8) + parseHexOctet(it)) + +/// byteLut is a lookup table mapping a string of two lowercase hex digits (zero-padded) to the UInt8 value. +const local byteLut = IntSeq(0, 255).map((it) -> it).toMap((it) -> it.toRadixString(16).padStart(2, "0"), (it) -> it) + +/// mask32Hi generates a mask of 1s in the top [prefix] bits of a [UInt32]. +const function mask32Hi(prefix: UInt(isBetween(0, 32))): UInt32 = math.maxUInt32.ushr(32-prefix).shl(32-prefix) +/// mask32Lo generates a mask of 1s in the bottom [suffix] bits of a [UInt32]. +const function mask32Lo(suffix: UInt(isBetween(0, 32))): UInt32 = math.maxUInt32.ushr(32-suffix) + +/// uint32FromBytes constructs a [UInt32] from four [UInt8] values. +const function uint32FromBytes(hihi: UInt8, hilo: UInt8, lohi: UInt8, lolo: UInt8): UInt32 = + hihi.shl(24).or(hilo.shl(16)).or(lohi.shl(8)).or(lolo) diff --git a/assets/pkl/external/pkl-pantry/packages/pkl.experimental.net/u128.pkl b/assets/pkl/external/pkl-pantry/packages/pkl.experimental.net/u128.pkl index 2b4e1eb..d35c229 100644 --- a/assets/pkl/external/pkl-pantry/packages/pkl.experimental.net/u128.pkl +++ b/assets/pkl/external/pkl-pantry/packages/pkl.experimental.net/u128.pkl @@ -13,92 +13,138 @@ // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// -module pkl.experimental.net.tests.u128 - -amends "pkl:test" +/// Who was it that said we'd never need more than 64 bits?? +/// Some tasks work best with unsigned 128-bit integers, eg. working with IPv6 addresses. +/// This module implements a [UInt128] type composed out of 4 [UInt]s that can handle limited bit-wise and arithmetic operations. +/// NB: this task is a better fit for [UInt32], but it cannot be used here as we rely on shifting bits off the significant end and Pkl's implementation doesn't permit this +module pkl.experimental.net.u128 import "pkl:math" -import "../u128.pkl" - -local someValue = u128.UInt128(0x74ab3ef8, 0x09bfce41, 0xab38bef1, 0x10efba83) - -facts { - ["le"] { - u128.zero.le(u128.zero) - u128.zero.le(u128.one) - u128.zero.le(u128.maxUInt128) - someValue.le(u128.maxUInt128) - !u128.maxUInt128.le(someValue) - u128.zero.le(someValue) - someValue.le(someValue) - } - ["lt"] { - !u128.zero.lt(u128.zero) - u128.zero.lt(u128.one) - u128.zero.lt(u128.maxUInt128) - someValue.lt(u128.maxUInt128) - !u128.maxUInt128.lt(someValue) - u128.zero.lt(someValue) - !someValue.lt(someValue) - } - ["=="] { - u128.zero == u128.zero - u128.zero != u128.one - u128.one == u128.one - u128.one != u128.maxUInt128 - u128.maxUInt128 == u128.maxUInt128 - u128.zero != u128.UInt128(0, 0, 1, 0) - u128.zero != u128.UInt128(0, 1, 0, 0) - u128.zero != u128.UInt128(1, 0, 0, 0) - } +/// A [UInt128] with value 0. +const zero: UInt128 = UInt128(0, 0, 0, 0) +/// A [UInt128] with value 1. +const one: UInt128 = UInt128(0, 0, 0, 1) +/// The maximum [UInt128] value. +const maxUInt128: UInt128 = UInt128(math.maxUInt32, math.maxUInt32, math.maxUInt32, math.maxUInt32) + +/// Creates a [UInt128] with components [UInt32]s [high], [midHigh], [midLow], and [low] +const function UInt128(high: UInt32, midHigh: UInt32, midLow: UInt32, low: UInt32): UInt128 = new { + words = List(low, midLow, midHigh, high) } -examples { - ["toString"] { - u128.one.toString() - u128.maxUInt128.toString() - module.catch(() -> - u128.UInt128(math.maxUInt32 + 1, math.maxUInt32 + 2, math.maxUInt32 + 3, math.maxUInt32 + 4).toString() - ) - } +/// An unsigned 128-bit integer. +class UInt128 { + words: List(length == 4) + hidden hihi: UInt = words[3] + hidden hilo: UInt = words[2] + hidden lohi: UInt = words[1] + hidden lolo: UInt = words[0] - ["and"] { - someValue.and(u128.zero).toString() - u128.zero.and(someValue).toString() - u128.maxUInt128.and(someValue).toString() - someValue.and(u128.maxUInt128).toString() - } + local self = this - ["or"] { - someValue.or(u128.zero).toString() - u128.zero.or(someValue).toString() - u128.maxUInt128.or(someValue).toString() - someValue.or(u128.maxUInt128).toString() - } + function toString(): String = new Listing { + hihi.toRadixString(16).padStart(8, "0") + hilo.toRadixString(16).padStart(8, "0") + lohi.toRadixString(16).padStart(8, "0") + lolo.toRadixString(16).padStart(8, "0") + }.join("") - ["shl"] { - u128.one.shl(0).toString() - u128.one.shl(1).toString() - u128.one.shl(19).toString() - u128.one.shl(32).toString() - u128.one.shl(33).toString() - u128.one.shl(51).toString() - u128.one.shl(64).toString() - u128.one.shl(65).toString() - u128.one.shl(84).toString() - } + /// Bitwise AND of this integer and [n]. + function and(other: UInt128): UInt128 = UInt128( + self.hihi.and(other.hihi), + self.hilo.and(other.hilo), + self.lohi.and(other.lohi), + self.lolo.and(other.lolo) + ) - ["add"] { - u128.one.add(u128.one).toString() - someValue.add(someValue).toString() - someValue.add(u128.zero).toString() - } + /// Bitwise OR of this integer and [n]. + function or(other: UInt128): UInt128 = UInt128( + self.hihi.or(other.hihi), + self.hilo.or(other.hilo), + self.lohi.or(other.lohi), + self.lolo.or(other.lolo) + ) + + /// Returns true if this integer is less than or equal to [other]. + function le(other: UInt128): Boolean = self.words == other.words || self.lt(other) + + /// Returns true if this integer is strictly less than [other]. + function lt(other: UInt128): Boolean = + self.hihi < other.hihi || self.hihi == other.hihi && ( + self.hilo < other.hilo || self.hilo == other.hilo && ( + self.lohi < other.lohi || self.lohi == other.lohi && ( + self.lolo < other.lolo + ))) + + /// Shifts this integer left by [n] bits. + function shl(n: UInt): UInt128 = + if (n == 0) self + else if (n > 32) self.shl(32).shl(n - 32) // just recurse, 5head! + // not only is this n == 32 case more efficient, but it protects us from the fact that Pkl has no UInt64 type + // we would be in for a bad time if we shifted a [math.maxUInt32] left 32 bits into the sign bit of [Int] + else if (n == 32) UInt128(self.hilo, self.lohi, self.lolo, 0) + else UInt128( + self.hihi.shl(n).and(math.maxUInt32).or(self.hilo.ushr(32 - n)), + self.hilo.shl(n).and(math.maxUInt32).or(self.lohi.ushr(32 - n)), + self.lohi.shl(n).and(math.maxUInt32).or(self.lolo.ushr(32 - n)), + self.lolo.shl(n).and(math.maxUInt32) + ) - ["seq"] { - u128.zero.seq(u128.UInt128(0, 0, 0, 64)).length// - // u128.zero.seq(u128.UInt128(0, 0, 1, 64)).length - // womp womp - // java.lang.NegativeArraySizeException: -2147483648 + /// Return the sum of this integer and [other]. + function add(other: UInt128): UInt128 = new { + local complement = math.maxUInt32 - other.lolo + local loloOverflows = self.lolo > complement + local lolo = + if (loloOverflows) self.lolo - math.maxUInt32 + other.lolo - 1 + else self.lolo + other.lolo + + local lohiComplement = math.maxUInt32 - other.lohi + local lohiCarry = if (loloOverflows) 1 else 0 + local lohiOverflows = (self.lohi + lohiCarry) > lohiComplement + local lohi = + if (lohiOverflows) self.lohi - math.maxUInt32 + other.lohi - 1 + lohiCarry + else self.lohi + other.lohi + lohiCarry + + local hiloComplement = math.maxUInt32 - other.hilo + local hiloCarry = if (lohiOverflows) 1 else 0 + local hiloOverflows = (self.hilo + hiloCarry) > hiloComplement + local hilo = + if (hiloOverflows) self.hilo - math.maxUInt32 + other.hilo - 1 + hiloCarry + else self.hilo + other.hilo + hiloCarry + + local hihiCarry = if (hiloOverflows) 1 else 0 + local hihi = (self.hihi + other.hihi + hihiCarry).and(math.maxUInt32) + words = List(lolo, lohi, hilo, hihi) } + + /// Generate a sequence of [UInt128] between this integer and [other], inclusive. + /// Behaves like [IntSeq]. + function seq(other: UInt128): Listing = + if (self.hihi == other.hihi) + if (self.hilo == other.hilo) + if (self.lohi == other.lohi) new { + for (ll in IntSeq(self.lolo, other.lolo)) { UInt128(self.hihi, self.hilo, self.lohi, ll) } + } + else new { + ...UInt128(self.hihi, self.hilo, self.lohi, self.lolo).seq(UInt128(self.hihi, self.hilo, self.lohi, math.maxUInt32)) + for (lh in IntSeq(self.lohi, other.lohi)) { + ...UInt128(self.hihi, self.hilo, lh, 0).seq(UInt128(self.hihi, self.hilo, lh, math.maxUInt32)) + } + ...UInt128(other.hihi, other.hilo, other.lohi, 0).seq(UInt128(other.hihi, other.hilo, other.lohi, other.lolo)) + } + else new { + ...UInt128(self.hihi, self.hilo, self.lohi, self.lolo).seq(UInt128(self.hihi, self.hilo, math.maxUInt32, math.maxUInt32)) + for (hl in IntSeq(self.hilo, other.hilo)) { + ...UInt128(self.hihi, hl, 0, 0).seq(UInt128(self.hihi, hl, math.maxUInt32, math.maxUInt32)) + } + ...UInt128(other.hihi, other.hilo, 0, 0).seq(UInt128(other.hihi, other.hilo, other.lohi, other.lolo)) + } + else new { + ...UInt128(self.hihi, self.hilo, self.lohi, self.lolo).seq(UInt128(self.hihi, math.maxUInt32, math.maxUInt32, math.maxUInt32)) + for (hh in IntSeq(self.hilo, other.hilo)) { + ...UInt128(hh, 0, 0, 0).seq(UInt128(hh, math.maxUInt32, math.maxUInt32, math.maxUInt32)) + } + ...UInt128(other.hihi, 0, 0, 0).seq(UInt128(other.hihi, other.hilo, other.lohi, other.lolo)) + } } diff --git a/assets/pkl/external/pkl-pantry/packages/pkl.experimental.syntax/ExpressionNode.pkl b/assets/pkl/external/pkl-pantry/packages/pkl.experimental.syntax/ExpressionNode.pkl index 7f147ce..93329d7 100644 --- a/assets/pkl/external/pkl-pantry/packages/pkl.experimental.syntax/ExpressionNode.pkl +++ b/assets/pkl/external/pkl-pantry/packages/pkl.experimental.syntax/ExpressionNode.pkl @@ -13,213 +13,249 @@ // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// -module pkl.experimental.syntax.tests.ExpressionNode +abstract module pkl.experimental.syntax.ExpressionNode -amends "pkl:test" +extends "Node.pkl" -import "../ExpressionNode.pkl" +import "ExpressionNode.pkl" +import "ObjectBodyNode.pkl" +import "IdentifierNode.pkl" +import "TypeNode.pkl" +import "operators.pkl" -examples { - ["binary operators - basic"] { - new ExpressionNode.BinaryOperatorExpressionNode { - lhs = new ExpressionNode.LiteralValueExpressionNode { - value = "foo" - } - operator = "+" - rhs = new ExpressionNode.LiteralValueExpressionNode { - value = "bar" - } - }.render("") - } - ["binary operators - precedence: no parentheses when child precedence is higher"] { - new ExpressionNode.BinaryOperatorExpressionNode { - operator = "+" - lhs = new ExpressionNode.BinaryOperatorExpressionNode { - lhs = new ExpressionNode.LiteralValueExpressionNode { - value = 5 - } - rhs = new ExpressionNode.LiteralValueExpressionNode { - value = 5 - } - operator = "*" - } - rhs = new ExpressionNode.BinaryOperatorExpressionNode { - lhs = new ExpressionNode.LiteralValueExpressionNode { - value = 5 - } - rhs = new ExpressionNode.LiteralValueExpressionNode { - value = 5 - } - operator = "*" - } - }.render("") - } - ["binary operators - precedence: parenthese added when child precedence is lower"] { - new ExpressionNode.BinaryOperatorExpressionNode { - operator = "*" - lhs = new ExpressionNode.BinaryOperatorExpressionNode { - lhs = new ExpressionNode.LiteralValueExpressionNode { - value = 5 - } - rhs = new ExpressionNode.LiteralValueExpressionNode { - value = 5 - } - operator = "+" - } - rhs = new ExpressionNode.BinaryOperatorExpressionNode { - lhs = new ExpressionNode.LiteralValueExpressionNode { - value = 5 - } - rhs = new ExpressionNode.LiteralValueExpressionNode { - value = 5 - } - operator = "+" - } - }.render("") - } - ["unary operators"] { - new ExpressionNode.PrefixOperatorExpressionNode { - operator = "!" - expression = new ExpressionNode.LiteralValueExpressionNode { value = false } - }.render("") - } - ["unary operators - parenthesizes binary expressions"] { - new ExpressionNode.PrefixOperatorExpressionNode { - operator = "-" - expression = new ExpressionNode.BinaryOperatorExpressionNode { - operator = "+" - lhs = new ExpressionNode.LiteralValueExpressionNode { - value = 5 - } - rhs = new ExpressionNode.LiteralValueExpressionNode { - value = 10 - } - } - }.render("") - } - ["member access - single identifier"] { - new ExpressionNode.MemberAccessExpressionNode { - identifier { - value = "foo" - } - }.render("") - } - ["member access - method call"] { - new ExpressionNode.MemberAccessExpressionNode { - identifier { - value = "foo" - } - arguments { - new ExpressionNode.MemberAccessExpressionNode { - identifier { - value = "bar" - } - } - new ExpressionNode.MemberAccessExpressionNode { - identifier { - value = "biz" - } - } - } - }.render("") - } - ["if/else"] { - new ExpressionNode.IfElseExpressionNode { - condition = new ExpressionNode.BinaryOperatorExpressionNode { - lhs = new ExpressionNode.MemberAccessExpressionNode { - identifier { - value = "foo" - } - } - operator = "==" - rhs = new ExpressionNode.LiteralValueExpressionNode { - value = "foo" - } - } - ifBranch = new ExpressionNode.LiteralValueExpressionNode { value = "foo" } - elseBranch = new ExpressionNode.LiteralValueExpressionNode { value = "bar" } - }.render("") - - new ExpressionNode.IfElseExpressionNode { - condition = new ExpressionNode.BinaryOperatorExpressionNode { - lhs = new ExpressionNode.MemberAccessExpressionNode { - identifier { - value = "foo" - } - } - operator = "==" - rhs = new ExpressionNode.LiteralValueExpressionNode { - value = "foo" - } - } - ifBranch = new ExpressionNode.LiteralValueExpressionNode { value = "foo" } - elseBranch = new ExpressionNode.LiteralValueExpressionNode { value = "bar" } - }.render(" ") - } - ["super access"] { - new ExpressionNode.SuperAccessExpressionNode { - rhs { - identifier { value = "someProperty" } - } - }.render("") - new ExpressionNode.SuperAccessExpressionNode { - rhs { - identifier { value = "someProperty" } - arguments { - new ExpressionNode.LiteralValueExpressionNode { value = true } - } - } - }.render("") - } - ["super subscript"] { - new ExpressionNode.SuperSubscriptExpressionNode { - subscript = new ExpressionNode.LiteralValueExpressionNode { value = "test" } - }.render("") - } - ["subscript"] { - new ExpressionNode.SubscriptExpressionNode { - lhs = new ExpressionNode.MemberAccessExpressionNode { - identifier { value = "test" } - } - subscript = new ExpressionNode.LiteralValueExpressionNode { value = "key" } - }.render("") - new ExpressionNode.SubscriptExpressionNode { - lhs = new ExpressionNode.IfElseExpressionNode { - condition = new ExpressionNode.MemberAccessExpressionNode { - identifier { value = "test" } - } - ifBranch = new ExpressionNode.MemberAccessExpressionNode { - identifier { value = "testTrue" } - } - elseBranch = new ExpressionNode.MemberAccessExpressionNode { - identifier { value = "testFalse" } - } - } - subscript = new ExpressionNode.BinaryOperatorExpressionNode { - operator = "+" - lhs = new ExpressionNode.LiteralValueExpressionNode { value = "test" } - rhs = new ExpressionNode.LiteralValueExpressionNode { value = "Key" } - } - }.render("") - } - ["read"] { - new ExpressionNode.ReadExpressionNode { - value = new ExpressionNode.LiteralValueExpressionNode { value = "env:HOME" } - }.render("") - new ExpressionNode.ReadExpressionNode { - keyword = "read*" - value = new ExpressionNode.LiteralValueExpressionNode { value = "env:HOME" } - }.render("") - new ExpressionNode.ReadExpressionNode { - keyword = "read?" - value = new ExpressionNode.LiteralValueExpressionNode { value = "env:HOME" } - }.render("") +typealias CompoundExpressionNode = BinaryOperatorExpressionNode|PrefixOperatorExpressionNode|IfElseExpressionNode + +class BinaryOperatorExpressionNode extends ExpressionNode { + local precedences: Mapping = new { + [operators.MULTIPLY] = 1 + [operators.DIVIDE] = 1 + [operators.INTEGER_DIVIDE] = 1 + [operators.MODULO] = 1 + [operators.PLUS] = 2 + [operators.BINARY_MINUS] = 2 + [operators.GREATER_THAN] = 3 + [operators.LESS_THAN] = 3 + [operators.GREATER_THAN_OR_EQUALS] = 3 + [operators.LESS_THAN_OR_EQUALS] = 3 + [operators.IS] = 4 + [operators.AS] = 4 + [operators.EQUALS] = 5 + [operators.NOT_EQUALS] = 5 + [operators.AND] = 6 + [operators.OR] = 7 + [operators.PIPE] = 8 } - ["trace"] { - new ExpressionNode.TraceExpressionNode { - value = new ExpressionNode.MemberAccessExpressionNode { - identifier { value = "test" } + + local function hasHigherPrecedence(a: String, b: String) = precedences[a] >= precedences[b] + + operator: operators.BinaryOperator + + /// The right hand side of the expression + lhs: ExpressionNode + + /// The left hand side of the expression + rhs: ExpressionNode + + local function renderChild(child: ExpressionNode, currentIndent: String) = + let (rendered = child.render(currentIndent)) + if ( + !(child is CompoundExpressionNode) + || child is BinaryOperatorExpressionNode && hasHigherPrecedence(operator, child.operator) + ) + rendered + else + parenthesize(rendered) + + function render(currentIndent: String) = + "\(renderChild(lhs, currentIndent)) \(operator) \(renderChild(rhs, currentIndent))" +} + +class BuiltInKeywordExpressionNode extends ExpressionNode { + keyword: "this"|"outer"|"module" + + function render(_) = keyword +} + +/// Unary operators in the prefix position: "!" and "-". +class PrefixOperatorExpressionNode extends ExpressionNode { + operator: operators.PrefixOperator + + expression: ExpressionNode + + function render(currentIndent: String) = + let (body = expression.render(currentIndent)) + if (expression is CompoundExpressionNode) operator + parenthesize(body) + else operator + body +} + +/// Unary operators in the postfix position: "!!" +class PostfixOperatorExpressionNode extends ExpressionNode { + operator: operators.PostfixOperator + + expression: ExpressionNode + + function render(currentIndent: String) = + let (body = expression.render(currentIndent)) + if (expression is CompoundExpressionNode) parenthesize(body) + operator + else body + operator +} + +class MemberAccessExpressionNode extends ExpressionNode { + identifier: IdentifierNode + + arguments: Listing? + + function renderArguments(currentIndent: String) = + if (arguments == null) "" + else + let (args = arguments.toList().map((arg) -> arg.render(currentIndent))) + parenthesize(args.join(", ")) + + function render(currentIndent: String) = "\(identifier.render(currentIndent))\(renderArguments(currentIndent))" +} + +/// Can either be an identifer or a function call. +/// +/// ``` +/// foo.bar +/// foo.bar() +/// ``` +class QualifiedMemberAccessExpressionNode extends ExpressionNode { + lhs: ExpressionNode + + symbol: "."|"?." = "." + + rhs: MemberAccessExpressionNode + + local function renderLhs(currentIndent: String) = + if (lhs is CompoundExpressionNode) + parenthesize(lhs.render(currentIndent)) + else lhs.render(currentIndent) + + function render(currentIndent: String) = "\(renderLhs(currentIndent))\(symbol)\(rhs.render(currentIndent))" +} + +class SuperAccessExpressionNode extends ExpressionNode { + rhs: MemberAccessExpressionNode + + function render(currentIndent) = "super.\(rhs.render(currentIndent))" +} + +class SuperSubscriptExpressionNode extends ExpressionNode { + subscript: ExpressionNode + + function render(currentIndent) = "super[\(subscript.render(currentIndent))]" +} + +class SubscriptExpressionNode extends ExpressionNode { + lhs: ExpressionNode + + subscript: ExpressionNode + + local function renderLhs(currentIndent: String) = + if (lhs is CompoundExpressionNode) + parenthesize(lhs.render(currentIndent)) + else lhs.render(currentIndent) + + function render(currentIndent: String): String = "\(renderLhs(currentIndent))[\(subscript.render(currentIndent))]" +} + +/// Node representing a literal value; e.g. `true` and `"hello"`. +class LiteralValueExpressionNode extends ExpressionNode { + value: String|Boolean|Number|Null + + function render(_) = pcfRenderer.renderValue(value) +} + +/// Node representing `new { ... }` or `new Type { ... }`. +class ObjectExpressionNode extends ExpressionNode { + body: ObjectBodyNode + + type: TypeNode? + + function render(currentIndent: String) = List( + "new", + type?.render(currentIndent), + body.render(currentIndent) + ).filterNonNull().join(" ") +} + +/// `if (...) ... else ...` +/// +/// To express `else if`, [elseBranch] should also be an [IfElseExpressionNode]. +class IfElseExpressionNode extends ExpressionNode { + condition: ExpressionNode + + ifBranch: ExpressionNode + + elseBranch: ExpressionNode + + function renderElseBranch(currentIndent: String) = + if (elseBranch is IfElseExpressionNode) + "\(currentIndent)else \(elseBranch.render(currentIndent))" + else + """ + \(currentIndent)else + \(currentIndent + indent)\(elseBranch.render(currentIndent + indent)) + """ + + function render(currentIndent: String) = """ + if \(parenthesize(condition.render(currentIndent))) + \(currentIndent + indent)\(ifBranch.render(currentIndent)) + \(renderElseBranch(currentIndent)) + """ +} + +class ImportExpressionNode extends ExpressionNode { + value: String + + keyword: "import"|"import*" = "import" + + fixed stringLiteralNode: LiteralValueExpressionNode = new { value = outer.value } + + function render(currentIndent: String) = "\(keyword)(\(stringLiteralNode.render(currentIndent)))" +} + +class ReadExpressionNode extends ExpressionNode { + value: ExpressionNode + + keyword: "read"|"read*"|"read?" = "read" + + function render(currentIndent: String) = "\(keyword)(\(value.render(currentIndent)))" +} + +class TraceExpressionNode extends ExpressionNode { + value: ExpressionNode + + function render(currentIndent: String) = "trace(\(value.render(currentIndent)))" +} + +/// Provides interpolation and multi-line strings that are not available using [LiteralValueExpressionNode]. +class StringExpressionNode extends ExpressionNode { + isMultiLine: Boolean = false + + stringParts: Listing + + function renderStringContents(currentIndent: String) = new Listing { + for (part in stringParts) { + when (part is ExpressionNode) { + #"\("# + part.render(currentIndent) + ")" + } else { + if (isMultiLine) part.replaceAll("\n", "\n\(currentIndent)") + else part.replaceAll("\n", "\\n") } - }.render("") - } + } + }.join("") + + function render(currentIndent: String) = + if (!isMultiLine) #""\#(renderStringContents(currentIndent))""# + else + let (newIndent = currentIndent + indent) + new Listing { + "\"\"\"" + newIndent + renderStringContents(newIndent) + "\(newIndent)\"\"\"" + }.join("\n") } diff --git a/assets/pkl/external/pkl-pantry/packages/pkl.experimental.syntax/ModuleNode.pkl b/assets/pkl/external/pkl-pantry/packages/pkl.experimental.syntax/ModuleNode.pkl index 4acfe19..7a9899d 100644 --- a/assets/pkl/external/pkl-pantry/packages/pkl.experimental.syntax/ModuleNode.pkl +++ b/assets/pkl/external/pkl-pantry/packages/pkl.experimental.syntax/ModuleNode.pkl @@ -13,189 +13,113 @@ // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// -module pkl.experimental.syntax.tests.ModuleNode - -amends "pkl:test" - -import "../ModuleNode.pkl" -import "../TypeNode.pkl" -import "../ExpressionNode.pkl" -import "../ObjectBodyNode.pkl" -import "../operators.pkl" - -facts { - ["new line at the end"] { - new ModuleNode { - declaration { - moduleHeader { - name { - parts { - new { value = "modulename" } - } - } - } - } - }.output.text == """ - module modulename +/// Node representing a Pkl module. +@Unlisted +module pkl.experimental.syntax.ModuleNode + +extends "ClassOrModuleNode.pkl" + +import "AnnotationNode.pkl" +import "DocCommentNode.pkl" +import "Node.pkl" +import "ClassNode.pkl" +import "QualifiedIdentifierNode.pkl" +import "TypeAliasNode.pkl" + +/// The top-level section of a module +/// +/// E.g. `module com.package.MyModule` +declaration: ModuleDeclarationNode? + +/// The list of imports for a module. +imports: Listing? + +/// The classes as defined in the module. +classes: Listing? - """ - } +/// The typealiases as defined in the module. +typealiases: Listing? + +class ModuleDeclarationNode extends Node { + /// Content for the first line of the module following "#!" + /// Enables modules to be evaluated as executable scripts + /// + /// Example: "/usr/bin/env pkl eval" + shebang: String? + + /// Annotations for the module itself, for instance, `@ModuleInfo` + annotations: Listing? + + docComment: DocCommentNode? + + moduleHeader: ModuleHeaderNode? + + function render(currentIndent: String) = + List( + shebang.ifNonNull((it) -> "#!\(it)"), + docComment?.render(currentIndent), + annotations?.toList()?.map((a) -> a.render(currentIndent))?.join("\n"), + moduleHeader?.render(currentIndent) + ).filterNonNull().join("\n") } -examples { - ["full declaration"] { - new ModuleNode { - declaration { - shebang = "/usr/bin/env pkl eval" - annotations { - new { - identifier { parts { new { value = "test" } } } - body { - renderInline = true - members { - new ObjectBodyNode.PropertyMemberNode { - propertyName { value = "annotation" } - assignment = new ExpressionNode.LiteralValueExpressionNode { value = true } - } - } - } - } - } - docComment { - value = """ - This is a module doc comment! - """ - } - moduleHeader { - modifiers { "open" } - name { - parts { - new { value = "pkl" } - new { value = "experimental" } - new { value = "syntax" } - new { value = "tests" } - new { value = "ModuleNode" } - } - } - moduleExtendsOrAmendsClause { - type = "amends" - extendedModule = "pkl:test" - } - } - } - }.output.text - } - ["short methods are rendered inline"] { - new ModuleNode { - methods { - new { - docComment { value = "Adds five to its argument." } - modifiers { "const"; "local" } - name { value = "addFive" } - parameters { - new { - name { value = "input" } - typeAnnotation { - type = new TypeNode.DeclaredTypeNode { name { parts { new { value = "Int" } } } } - } - } - } - body = new ExpressionNode.BinaryOperatorExpressionNode { - lhs = new ExpressionNode.MemberAccessExpressionNode { identifier { value = "input" } } - operator = operators.PLUS - rhs = new ExpressionNode.LiteralValueExpressionNode { value = 5 } - } - } - } - }.output.text - } - ["methods with long values are rendered indented"] { - new ModuleNode { - methods { - new { - docComment { value = "Adds five to its argument." } - modifiers { "const"; "local" } - name { value = "myReallyLongMethodForAddingFiveToAnInteger" } - parameters { - new { - name { value = "inputIntegerValue" } - typeAnnotation { - type = new TypeNode.DeclaredTypeNode { name { parts { new { value = "Int" } } } } - } - } - } - body = new ExpressionNode.BinaryOperatorExpressionNode { - lhs = new ExpressionNode.MemberAccessExpressionNode { identifier { value = "inputIntegerValue" } } - operator = operators.PLUS - rhs = new ExpressionNode.LiteralValueExpressionNode { value = 5 } - } - } - } - }.output.text - } - ["methods with long values but short first lines are rendered inline"] { - new ModuleNode { - methods { - new { - docComment { value = "Adds five to its argument and returns a dynamic object containing the result." } - modifiers { "const"; "local" } - name { value = "myLongMethodForReturningADynamicObject" } - parameters { - new { - name { value = "inputIntegerValue" } - typeAnnotation { - type = new TypeNode.DeclaredTypeNode { name { parts { new { value = "Int" } } } } - } - } - } - body = new ExpressionNode.ObjectExpressionNode { - type = new TypeNode.DeclaredTypeNode { name { parts { new { value = "Dynamic" } } } } - body { - members { - new ObjectBodyNode.PropertyMemberNode { - propertyName { value = "value" } - assignment = new ExpressionNode.BinaryOperatorExpressionNode { - lhs = new ExpressionNode.MemberAccessExpressionNode { identifier { value = "inputIntegerValue" } } - operator = operators.PLUS - rhs = new ExpressionNode.LiteralValueExpressionNode { value = 5 } - } - } - } - } - } - } +local const renderer = new PcfRenderer {} + +class ModuleHeaderNode extends Node { + modifiers: Listing<"abstract"|"open">(isDistinct, name != null)? + + name: QualifiedIdentifierNode? + + moduleExtendsOrAmendsClause: ModuleExtendsOrAmendsClauseNode? + + function render(header) = + let (moduleHeader = new Listing { + when (modifiers != null) { + modifiers.join(" ") + " " } - }.output.text - } - ["typealiases"] { - new ModuleNode { - typealiases { - new { - name { value = "Short" } - type = new TypeNode.UnionTypeNode { - members { - new TypeNode.StringLiteralTypeNode { value = "A" } - new TypeNode.StringLiteralTypeNode { value = "B" } - } - } - } - new { - name { value = "Long" } - type = new TypeNode.UnionTypeNode { - members { - new TypeNode.StringLiteralTypeNode { value = "Apple" } - new TypeNode.StringLiteralTypeNode { value = "Blackberry" } - new TypeNode.StringLiteralTypeNode { value = "Cherry" } - new TypeNode.StringLiteralTypeNode { value = "Durian" } - new TypeNode.StringLiteralTypeNode { value = "Elderberry" } - new TypeNode.StringLiteralTypeNode { value = "Fig" } - new TypeNode.StringLiteralTypeNode { value = "Guava" } - new TypeNode.StringLiteralTypeNode { value = "Huckleberry" } - new TypeNode.StringLiteralTypeNode { value = "I_can't_think_of_a_fruit_that_starts_with_I" } - } - } - } + when (name != null) { + "module " + name.render(header) } - }.output.text - } + }.join("")) + List( + if (moduleHeader.isEmpty) null else moduleHeader, + moduleExtendsOrAmendsClause?.render() + ).filterNonNull().join("\n\n") } + +class ModuleExtendsOrAmendsClauseNode extends Node { + type: "extends"|"amends" + extendedModule: String + function render() = + """ + \(type) \(renderer.renderValue(extendedModule)) + """ +} + +class ImportNode extends Node { + value: String + alias: String? + keyword: "import"|"import*" = "import" + function render() = new Listing { + keyword + " " + renderer.renderValue(value) + when (alias != null) { + " as " + alias + } + }.join("") +} + +function renderImports() = + (imports?.toList() ?? List()).map((it) -> it.render()).join("\n") + +function render(currentIndent: String) = List( + declaration?.render(currentIndent), + renderImports(), + super.render(currentIndent), + classes?.toList()?.map((c) -> c.render(currentIndent))?.join("\n\n"), + typealiases?.toList()?.map((t) -> t.render(currentIndent))?.join("\n\n") +) + .filter((line) -> line != null && !line.isEmpty) + .join("\n\n") + "\n" diff --git a/assets/pkl/external/pkl-pantry/packages/pkl.experimental.syntax/TypeNode.pkl b/assets/pkl/external/pkl-pantry/packages/pkl.experimental.syntax/TypeNode.pkl index b3377e6..55272e3 100644 --- a/assets/pkl/external/pkl-pantry/packages/pkl.experimental.syntax/TypeNode.pkl +++ b/assets/pkl/external/pkl-pantry/packages/pkl.experimental.syntax/TypeNode.pkl @@ -13,130 +13,82 @@ // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// -module pkl.experimental.syntax.tests.TypeNode - -amends "pkl:test" - -import "../TypeNode.pkl" -import "../ExpressionNode.pkl" - -facts { - ["declared type"] { - new TypeNode.DeclaredTypeNode { - name { - parts { - new { value = "String" } - } - } - }.render("") == "String" - new TypeNode.DeclaredTypeNode { - name { - parts { - new { value = "myModule" } - new { value = "Bar" } - } - } - }.render("") == "myModule.Bar" - } - ["declared type - type arguments"] { - new TypeNode.DeclaredTypeNode { - name { - parts { - new { value = "Listing" } - } - } - typeArguments { - new TypeNode.DeclaredTypeNode { - name { - parts { - new { value = "Bar" } - } - } - } - new TypeNode.DeclaredTypeNode { - name { - parts { - new { value = "Baz" } - } - } - } - } - }.render("") == "Listing" - } - ["constrained"] { - new TypeNode.ConstrainedTypeNode { - typeNode = new TypeNode.DeclaredTypeNode { - name { - parts { - new { value = "Bar" } - } - } - } - constraints { - new ExpressionNode.MemberAccessExpressionNode { - identifier { - value = "isLessThan" - } - arguments { - new ExpressionNode.LiteralValueExpressionNode { - value = 5 - } - } - } - } - }.render("") == "Bar(isLessThan(5))" - } - ["string literal"] { - new TypeNode.StringLiteralTypeNode { - value = "my string type" - }.render("") == """ - "my string type" - """ - } - ["union"] { - new TypeNode.UnionTypeNode { - members { - new TypeNode.StringLiteralTypeNode { - value = "one" - } - new TypeNode.StringLiteralTypeNode { - value = "two" - } - } - }.render("") == """ - "one"|"two" - """ - - new TypeNode.UnionTypeNode { - members { - new TypeNode.UnionDefaultType { - typeNode = new TypeNode.StringLiteralTypeNode { - value = "one" - } - } - new TypeNode.StringLiteralTypeNode { - value = "two" - } - } - }.render("") == """ - *"one"|"two" - """ - - module.catch(() -> - new TypeNode.UnionTypeNode { - members { - new TypeNode.UnionDefaultType { - typeNode = new TypeNode.StringLiteralTypeNode { - value = "one" - } - } - new TypeNode.UnionDefaultType { - typeNode = new TypeNode.StringLiteralTypeNode { - value = "two" - } - } - } - }.render("") - ).startsWith("Type constraint `atMostOneUnionDefaultType` violated.") - } +abstract module pkl.experimental.syntax.TypeNode + +extends "Node.pkl" + +import "QualifiedIdentifierNode.pkl" +import "ExpressionNode.pkl" +import "TypeNode.pkl" +import "Node.pkl" + +class NullableTypeNode extends TypeNode { + typeNode: TypeNode + function render(currentIndent: String) = + let (underlyingRendered = if (typeNode is UnionTypeNode) parenthesize(typeNode.render(currentIndent)) else typeNode.render(currentIndent)) + underlyingRendered + "?" +} + +class BuiltInTypeNode extends TypeNode { + type: "unknown"|"nothing"|"module" + function render(_) = type +} + +class StringLiteralTypeNode extends TypeNode { + value: String + + function render(_) = pcfRenderer.renderValue(value) +} + +class DeclaredTypeNode extends TypeNode { + name: QualifiedIdentifierNode + + typeArguments: Listing + + local function renderTypeArguments(currentIndent: String) = + if (typeArguments.isEmpty) "" + else "<" + typeArguments.toList().map((t) -> t.render(currentIndent)).join(", ") + ">" + + function render(currentIndent: String) = name.render(currentIndent) + renderTypeArguments(currentIndent) +} + +class ConstrainedTypeNode extends TypeNode { + /// The underlying type + typeNode: TypeNode + + constraints: Listing + + local function renderConstraints(currentIndent: String) = + "(" + constraints.toList().map((c) -> c.render(currentIndent)).join(", ") + ")" + + function render(currentIndent: String) = + let (renderedUnderlyingType = if (typeNode is UnionTypeNode) parenthesize(typeNode.render(currentIndent)) else typeNode.render(currentIndent)) + renderedUnderlyingType + renderConstraints(currentIndent) +} + +class UnionDefaultType extends Node { + typeNode: TypeNode + + function render(currentIndent: String) = "*\(typeNode.render(currentIndent))" +} + +class UnionTypeNode extends TypeNode { + members: Listing(atMostOneUnionDefaultType) + + const local atMostOneUnionDefaultType = (members: Listing) -> + members.fold(0, (acc, member) -> acc + if (member is UnionDefaultType) 1 else 0) <= 1 + + function render(currentIndent: String) = + let (childrenRendered = members.toList().map((t) -> t.render(currentIndent))) + // Multiline if length exceeds `maxColumnWidth` chars. + // If multiline, indent one level deeper. + if (childrenRendered.fold(0, (acc, elem) -> acc + elem.length) > maxColumnWidth) + "\n\(currentIndent + indent)" + childrenRendered.join("\n\(currentIndent + indent)|") + else + childrenRendered.join("|") +} + +// TODO fill me in +class FunctionTypeNode extends TypeNode { + } diff --git a/assets/pkl/external/pkl-pantry/packages/pkl.experimental.uri/URI.pkl b/assets/pkl/external/pkl-pantry/packages/pkl.experimental.uri/URI.pkl index d33f922..c264280 100644 --- a/assets/pkl/external/pkl-pantry/packages/pkl.experimental.uri/URI.pkl +++ b/assets/pkl/external/pkl-pantry/packages/pkl.experimental.uri/URI.pkl @@ -13,185 +13,373 @@ // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// -module pkl.experimental.uri.tests.URI +/// A Uniform Resource Identifier as defined by +/// [RFC-3986](https://datatracker.ietf.org/doc/html/rfc3986). +/// +/// [URI]s may be constructed literally, or parsed from a string via [parse]. +@ModuleInfo { minPklVersion = "0.25.0" } +module pkl.experimental.uri.URI -amends "pkl:test" +import "URI.pkl" -import "../URI.pkl" +/// The scheme component. +scheme: String? -local alphaLower = "abcdefghijklmnopqrstuvwxyz" -local alphaUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" -local nums = "0123456789" +/// The user information portion of the authority component. +userInfo: String? -facts { - ["encode"] { - URI.encode("https://example.com/some path") == "https://example.com/some%20path" - URI.encode(alphaLower) == alphaLower - URI.encode(alphaUpper) == alphaUpper - URI.encode(nums) == nums +/// The host portion of the authority component. +host: String? - local safeChars = "!#$&'()*+,-./:;=?@_~" - URI.encode(safeChars) == safeChars - URI.encode("\u{ffff}") == "%EF%BF%BF" - URI.encode("🏀") == "%F0%9F%8F%80" - URI.encode("\n") == "%0A" - } - ["encodeComponent"] { - URI.encodeComponent("https://example.com/some path") == "https%3A%2F%2Fexample.com%2Fsome%20path" - URI.encode(alphaLower) == alphaLower - URI.encode(alphaUpper) == alphaUpper - URI.encode(nums) == nums - local safeChars = "-_.!~*'()" - URI.encode(safeChars) == safeChars - } - ["percentDecode"] { - URI.percentDecode("foo%20bar") == "foo bar" - URI.percentDecode("foo%20bar") == "foo bar" - URI.percentDecode("foo%20%20bar") == "foo bar" - URI.percentDecode("%F0%9F%8F%80") == "🏀" - URI.percentDecode("%C2%A3") == "£" - URI.percentDecode("%EF%BF%BF") == "\u{ffff}" - URI.percentDecode("%2Fbar%2Fbaz") == "/bar/baz" - } - ["parse"] { - local uri1 = URI.parse("https://www.example.com") - uri1 == new URI { - scheme = "https" - host = "www.example.com" - path = "" - } - uri1.pathSegments == List("") - - local uri2 = URI.parse("https://bilbo@www.example.com:1234/my/path?search=mysearch#fragment-value") - uri2 == new URI { - scheme = "https" - userInfo = "bilbo" - host = "www.example.com" - port = 1234 - path = "/my/path" - query = "search=mysearch" - fragment = "fragment-value" - } - uri2.pathSegments == List("my", "path") +/// The port portion of the authority component. +port: UInt16? - local uri3 = URI.parse("pkl:base?mybase") - uri3 == new URI { - scheme = "pkl" - path = "base" - query = "mybase" - } - uri3.pathSegments == List("base") - - local uri4 = URI.parse("pkl:base?mybase#fragment") - uri4 == new URI { - scheme = "pkl" - path = "base" - query = "mybase" - fragment = "fragment" - } - uri4.pathSegments == List("base") +/// The path component, URI-encoded. +/// +/// Access the decoded form of the path via [pathSegments]. +path: UriString = "" - local uri5 = URI.parse("https://www.example.com/my%20path") - uri5 == new URI { - scheme = "https" - host = "www.example.com" - path = "/my%20path" - } - uri5.pathSegments == List("my path") - - local uri6 = URI.parse("https://www.example.com/search?q=%F0%9F%8F%80") - uri6 == new URI { - scheme = "https" - host = "www.example.com" - path = "/search" - query = "q=%F0%9F%8F%80" - } - uri6.pathSegments == List("search") +/// The authority component of the URI. +function authority(): String? = + if (hasAuthority()) + List( + "//", + if (userInfo != null) "\(encode(userInfo))@" else "", + encode(host!!), + if (port != null) ":\(port)" else "" + ) + .join("") + else null - local uri7 = URI.parse("myscheme:%F0%9F%8F%80#myfragment") - uri7 == new URI { - scheme = "myscheme" - path = "%F0%9F%8F%80" - fragment = "myfragment" - } - uri7.pathSegments == List("🏀") +/// The URI path split into its segments. +/// +/// Each segment is decoded. +fixed pathSegments: List = + let (parts = path.split("/").map((it) -> percentDecode(it))) + if (hasAbsolutePath) parts.drop(1) + else parts - local uri8 = URI.parse("file:/my%20file.txt") - uri8 == new URI { - scheme = "file" - path = "/my%20file.txt" - } - uri8.pathSegments == List("my file.txt") +/// The query component, URI-encoded. +query: UriString? - local uri9 = URI.parse("file:///my%20file.txt") - uri9 == new URI { - scheme = "file" - path = "/my%20file.txt" - host = "" - } - uri9.pathSegments == List("my file.txt") +/// The fragment component. +fragment: String? - local uri10 = URI.parse("schemey:///first%2Fsegment/second") - uri10 == new URI { - scheme = "schemey" - path = "/first%2Fsegment/second" - host = "" - } - uri10.pathSegments == List("first/segment", "second") +/// The base path portion of this URI. +/// +/// This equivalent to the current URI without its query and fragment components. +hidden basePath: URI = (module) { + query = null + fragment = null +} - local uri11 = URI.parse("../bar/baz") - uri11 == new URI { - path = "../bar/baz" - } - uri11.pathSegments == List("..", "bar", "baz") +/// Tells if this URI is an [absolute URI](https://datatracker.ietf.org/doc/html/rfc3986#section-4.3). +hidden isAbsolute: Boolean = scheme != null - local uri12 = URI.parse("//www.example.com/foo") - uri12 == new URI { - host = "www.example.com" - path = "/foo" - } - uri12.pathSegments == List("foo") +/// Tells if the [path] is absolute. +hidden hasAbsolutePath: Boolean = path?.startsWith("/") ?? false + +/// Tells if this URI has an [authority](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2). +function hasAuthority(): Boolean = (userInfo ?? host ?? port) != null - local uri13 = URI.parse("#") - uri13 == new URI { - path = "" - fragment = "" +// +local function _removeDotSegments(input: List, result: List): List = + if (input.isEmpty) result + else + let (currentSegment = input.first) + if (currentSegment == ".") + _removeDotSegments(input.drop(1), result) + else if (currentSegment == "..") + _removeDotSegments(input.drop(1), result.dropLast(1)) + else + _removeDotSegments(input.drop(1), result.add(currentSegment)) + +local function removeDotSegments(input: String): String = + _removeDotSegments(input.split("/"), List()).join("/") + + if (input.endsWith("/")) "/" else "" + +// +local function mergePaths(base: URI, other: URI): String = + if (other.hasAbsolutePath) + other.path + else if (base.hasAuthority() && base.path == "") + "/" + other.path + else + let (basePath = if (base.path.contains("/")) base.path.substring(0, base.path.lastIndexOf("/") + 1) else "/") + removeDotSegments(basePath + other.path) + +/// Resolves [other] as a URI reference to this URI. +/// +/// Follows the rules described in +/// [RFC-3986 Section 5.2](https://www.rfc-editor.org/rfc/rfc3986#section-5.2). +function resolveUri(other: URI): URI = + let (self = this) + new { + when (other.scheme != null) { + scheme = other.scheme + userInfo = other.userInfo + host = other.host + port = other.port + path = other.path + query = other.query + } else { + scheme = self.scheme + when (other.hasAuthority()) { + userInfo = other.userInfo + host = other.host + port = other.port + path = removeDotSegments(other.path) + } else { + userInfo = self.userInfo + host = self.host + port = self.port + when (other.path == "") { + path = self.path + query = other.query ?? self.query + } else { + path = mergePaths(self, other) + query = other.query + } + } + } + fragment = other.fragment } - uri13.pathSegments == List("") - } - ["toString()"] { - URI.parse("https://www.example.com").toString() == "https://www.example.com" - URI.parse("https://bilbo@www.example.com:1234/my/path?search=mysearch#fragment-value").toString() == "https://bilbo@www.example.com:1234/my/path?search=mysearch#fragment-value" - URI.parse("pkl:base?mybase").toString() == "pkl:base?mybase" - URI.parse("pkl:base?mybase#fragment").toString() == "pkl:base?mybase#fragment" - URI.parse("https://www.example.com/my%20path").toString() == "https://www.example.com/my%20path" - URI.parse("https://www.example.com/search?q=%F0%9F%8F%80").toString() == "https://www.example.com/search?q=%F0%9F%8F%80" - URI.parse("pkl:%F0%9F%8F%80#myfragment").toString() == "pkl:%F0%9F%8F%80#myfragment" - URI.parse("file:/my%20file.txt").toString() == "file:/my%20file.txt" - URI.parse("file:///my%20file.txt").toString() == "file:///my%20file.txt" - } - ["resolve"] { - URI.parse("schemey:/first/second/").resolve("../third").toString() - == "schemey:/first/third" - URI.parse("schemey:/first/second/").resolve("../../third").toString() - == "schemey:/third" - URI.parse("schemey:/first/second/").resolve("./").toString() - == "schemey:/first/second/" - URI.parse("schemey:/first/second").resolve("./").toString() - == "schemey:/first/" - URI.parse("schemey:/first/second/").resolve("/some/other/path").toString() - == "schemey:/some/other/path" - URI.parse("schemey:/first/second/").resolve("otherscheme:/some/other/path").toString() - == "otherscheme:/some/other/path" - URI.parse("schemey:/first/leaf").resolve("./").toString() - == "schemey:/first/" - URI.parse("schemey:/first/leaf").resolve("./second").toString() - == "schemey:/first/second" - URI.parse("https://www.example.com").resolve("some/relative/path").toString() - == "https://www.example.com/some/relative/path" - URI.parse("https://www.example.com").resolve("some/relative/path").toString() - == "https://www.example.com/some/relative/path" - URI.parse("https://www.example.com").resolve("//foo.com/some/relative/path").toString() - == "https://foo.com/some/relative/path" + +/// Resolves [other] as a URI string to this URI. +function resolve(other: String): URI? = + let (parsed = parse(other)) + if (parsed == null) null + else resolveUri(parsed) + +function toString(): String = new Listing { + when (scheme != null) { "\(encode(scheme)):" } + when (hasAuthority()) { + authority() } -} + path + when (query != null) { "?\(query)" } + when (fragment != null) { "#\(encode(fragment))" } +}.join("") + +/// Tells if [str] is a valid URI string. +const local isUriString = (str: String) -> + str + .replaceAll(PERCENT_REGEX, "") + .codePoints + .every((it) -> isUriSafe(it)) + +/// A string that has valid URI characters. +typealias UriString = String(isUriString) + +// alphanumeric or !#$&'()*+,-./:;=?@_~ +const local function isUriSafe(codePoint: Int) = + isAlphaNumeric(codePoint) + // ! + || codePoint == 33 + // #$ + || codePoint.isBetween(35, 36) + // &'()*+,-./ + || codePoint.isBetween(38, 47) + // :; + || codePoint.isBetween(58, 59) + // = + || codePoint == 61 + // ?@ + || codePoint.isBetween(63, 64) + // _ + || codePoint == 95 + // ~ + || codePoint == 126 + +// alphanumeric or !'()*-._~ +local function isUriComponentSafe(codePoint: Int) = + isAlphaNumeric(codePoint) + // ! + || codePoint == 33 + // '()* + || codePoint.isBetween(39, 42) + // -. + || codePoint.isBetween(45, 46) + // _ + || codePoint == 95 + // ~ + || codePoint == 126 + +local function getUtf8Bytes(codePoint: Int): List = + if (codePoint <= 0x7f) + List(codePoint) + else if (codePoint <= 0x7ff) + List( + 0xc0.or(codePoint.shr(6)), + 0x80.or(codePoint.and(0x3f)) + ) + else if (codePoint <= 0xffff) + List( + 0xe0.or(codePoint.shr(12)), + 0x80.or(codePoint.shr(6).and(0x3f)), + 0x80.or(codePoint.and(0x3f)) + ) + else + List( + 0xf0.or(codePoint.shr(18)), + 0x80.or(codePoint.shr(12).and(0x3f)), + 0x80.or(codePoint.shr(6).and(0x3f)), + 0x80.or(codePoint.and(0x3f)) + ) + +/// Encode into percent encoding into utf-8 bytes. +/// +/// Facts: +/// ``` +/// percentEncode(" ".codePoints.first) == "%20" +/// percentEncode("/".codePoints.first) == "%2F" +/// ``` +local function percentEncode(codePoint: Int) = + getUtf8Bytes(codePoint) + .map((it) -> "%" + it.toRadixString(16).toUpperCase().padStart(2, "0")) + .join("") + +local hexDigits = "0123456789ABCDEF" + +/// Facts: +/// ``` +/// getBytes("%20") == List(32) +/// getBytes("%7F") == List(127) +/// getBytes("%20%7F") == List(32, 127) +/// ``` +local function getBytes(str: String): List = + str + .split("%") + .drop(1) + .map((it) -> + let (msb = hexDigits.indexOf(it[0].toUpperCase())) + let (lsb = hexDigits.indexOf(it[1].toUpperCase())) + lsb + (msb * 16) + ) + +const local function isAlphaNumeric(codePoint: Int) = + codePoint.isBetween(48, 57) // 0-9 + || codePoint.isBetween(65, 90) // a-z + || codePoint.isBetween(97, 122) // A-Z + +/// Encodes [str] using percent-encoding bytes to make it safe for the literal use as a URI. +/// +/// All characters except for alphanumeric chracters, and the chracters `!#$&'()*+,-./:;=?@_~` +/// are percent-encoded. +/// +/// Follows the rules for the `encodeURI` function as described by +/// [ECMA-262](https://262.ecma-international.org/5.1/#sec-15.1.3.3). +/// +/// Facts: +/// ``` +/// encode("https://example.com/some path/") == "https://example.com/some%20path" +/// ``` +/// +function encode(str: String): String = + str.codePoints + .map((codePoint) -> + if (isUriSafe(codePoint)) codePoint.toChar() + else percentEncode(codePoint) + ) + .join("") + +/// Encodes [str] using percent-encoding to make it safe to literal use as a URI component. +/// +/// All characters except for alphanumeric characters, and the characters `-_.!~*'()` are +/// percent-encoded. +/// +/// Follows the rules for the `encodeURIComponent` function as described by +/// [ECMA-262](https://262.ecma-international.org/5.1/#sec-15.1.3.4). +/// +/// Facts: +/// ``` +/// encodeComponent("https://example.com/some path") == "https%3A%2F%2example.com%2Fsome%20path" +/// ``` +function encodeComponent(str: String): String = + str.codePoints + .map((codePoint) -> + if (isUriComponentSafe(codePoint)) codePoint.toChar() + else percentEncode(codePoint) + ) + .join("") + +const local PERCENT_REGEX = Regex(#"(?:%[\da-fA-F]{2})+"#) + +/// Decodes [str] given a percent-encoded string. +function percentDecode(str: String): String = + str.replaceAllMapped(PERCENT_REGEX, (match) -> + let (bytes = getBytes(match.value)) + doPercentDecode(bytes) + ) + +local function doPercentDecode(bytes: List): String = _doPercentDecode(bytes, "") + +local function _doPercentDecode(bytes: List, ret: String) = + if (bytes.length == 0) ret + else if (bytes[0] < 0x80) + _doPercentDecode(bytes.drop(1), ret + bytes[0].toChar()) + else if (bytes[0] < 0xE0) + let (b0 = bytes[0].and(0x1f).shl(6)) + let (b1 = bytes[1].and(0x3f)) + _doPercentDecode(bytes.drop(2), ret + b0.or(b1).toChar()) + else if (bytes[0] < 0xF0) + let (b0 = bytes[0].and(0xf).shl(12)) + let (b1 = bytes[1].and(0x3f).shl(6)) + let (b2 = bytes[2].and(0x3f)) + _doPercentDecode(bytes.drop(3), ret + b0.or(b1).or(b2).toChar()) + else + let (b0 = bytes[0].and(0x7).shl(18)) + let (b1 = bytes[1].and(0x3f).shl(12)) + let (b2 = bytes[2].and(0x3f).shl(6)) + let (b3 = bytes[3].and(0x3f)) + _doPercentDecode(bytes.drop(4), ret + b0.or(b1).or(b2).or(b3).toChar()) + +/// Regex to parse URI's. +/// +/// From . +// language=RegExp +local URI_REGEX: Regex = Regex(#""" + ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))? + """#) + +// language=RegExp +local AUTHORITY_REGEX: Regex = Regex(#""" + (?:([^@]+)@)?([^:]*)(?::(\d+))? + """#) + +// noinspection TypeMismatch +function parseAuthority(authority: String): Dynamic = + let (matches = AUTHORITY_REGEX.findMatchesIn(authority)) + let (groups = matches[0].groups) + new { + userInfo = groups.getOrNull(1)?.value?.ifNonNull((it) -> percentDecode(it)) + host = groups.getOrNull(2)?.value?.ifNonNull((it) -> percentDecode(it)) + port = groups.getOrNull(3)?.value?.toInt() + } + +/// Parses the input string as a [URI]. +/// +/// If the input is not valid, returns `null`. +function parse(str: String): URI? = + let (matches = URI_REGEX.findMatchesIn(str)) + if (matches.isEmpty) null + else + let (groups = matches[0].groups) + let (schemePart = groups.getOrNull(2)?.value) + let (authorityPart = groups.getOrNull(4)?.value) + let (pathPart = groups[5].value) + let (queryPart = groups.getOrNull(7)?.value) + let (fragmentPart = groups.getOrNull(9)?.value) + new URI { + when (schemePart != null) { + scheme = percentDecode(schemePart) + } + when (authorityPart != null) { + ...parseAuthority(authorityPart) + } + path = pathPart + query = queryPart + when (fragmentPart != null) { + fragment = percentDecode(fragmentPart) + } + } diff --git a/assets/pkl/external/pkl-pantry/packages/pkl.table/table.pkl b/assets/pkl/external/pkl-pantry/packages/pkl.table/table.pkl index 84bb880..7647028 100644 --- a/assets/pkl/external/pkl-pantry/packages/pkl.table/table.pkl +++ b/assets/pkl/external/pkl-pantry/packages/pkl.table/table.pkl @@ -13,93 +13,304 @@ // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// -module pkl.table.tests.table +/// [TableRenderer] generates pretty tables! +@ModuleInfo { minPklVersion = "0.25.0" } +module pkl.table.table -amends "pkl:test" +import "pkl:math" -import "../table.pkl" +typealias ColumnKey = String +typealias ListLike = List|Listing|Dynamic(toMap().isEmpty) +const local listLikeDescription = "`List`s, `Listing`s, or `Dynamic`s with only elements" -local data = new Listing { - new { alpha = "123"; beta = 123; gamma = 123.h; delta = List("1", "2", "3") } - new { alpha = "456"; beta = 456; gamma = 456.h; delta = List("4", "5", "6") } - new { alpha = "789loooooong"; beta = 789; gamma = 789.h; delta = List("7", "8", "9") } +typealias HorizontalPosition = "left"|"inner"|"right" +typealias VerticalPosition = "top"|"inner"|"bottom" +typealias Alignment = "left"|"right" + +const function Column(_key: String): Column = new { key = _key } +class Column { + /// The key used to extract property values from rows + key: ColumnKey + + /// The column header title + title: String = key + + /// The direction to align and pad column contents within a cell + align: Alignment = "left" +} + +/// Used to provide fully custom content for an entire table row +/// One possible usage is to express colspan in HTML tables +open class RowDirective { + content: String + + function render(style: TableStyle): String = content } -examples { - ["simple"] { - new table.TableRenderer { - columns { "alpha"; "beta"; "gamma" } - }.renderDocument(data) +/// Style options used to draw the table +/// Default values produce a table consisting of simple ASCII characters +open class TableStyle { + /// Placeholder to use in place of null property values + nullPlaceholder: String = "null" + + /// Include a header in the table + includeHeader: Boolean = true + + /// Default character to use for horizontal rules + defaultHorizontal: String(!fixedWidth || length == 1) = "-" + + /// Indicates whether or not the table is expected to produce fixed-width output + /// Disabling this enables styles to produce markup languages like HTML + fixedWidth = true + + /// Characters to use for specific horizontal rules + horizontals: Mapping = new { + ["top"] = defaultHorizontal + ["inner"] = defaultHorizontal + ["bottom"] = defaultHorizontal } - ["fancy column"] { - new table.TableRenderer { - style { - nullPlaceholder = "" - } - columns { - (table.Column("alpha")) { - title = "a" - align = "right" - } - (table.Column("beta")) { - title = "b" - } - (table.Column("gamma")) { - title = "c" - } - "unknown" - } - }.renderDocument(data) + + /// Default character to use for vertical rules + defaultVertical: String(!fixedWidth || length == 1) = "|" + + /// Characters to use for specific horizontal rules + verticals: Mapping = new { + ["left"] = defaultVertical + ["inner"] = defaultVertical + ["right"] = defaultVertical } - ["row directive"] { - new table.TableRenderer { - style = table.htmlStyle - columns { table.Column("alpha"); table.Column("beta"); table.Column("gamma") } - }.renderDocument((data) { - new table.RowDirective { content = "\(table.htmlStyle.indent.repeat(2))this is a colspan" } - }) + + /// Characters to use for specific vertical rules in the header + headerVerticals: Mapping = verticals + + /// Default character to use for corners + defaultCorner: String(!fixedWidth || length == 1) = "+" + + /// Characters to use for specific corners + corners: Mapping> = new { + ["left"] { + ["top"] = defaultCorner + ["inner"] = defaultCorner + ["bottom"] = defaultCorner + } + ["inner"] { + ["top"] = defaultCorner + ["inner"] = defaultCorner + ["bottom"] = defaultCorner + } + ["right"] { + ["top"] = defaultCorner + ["inner"] = defaultCorner + ["bottom"] = defaultCorner + } } - ["coverter"] { - new table.TableRenderer { - columns { table.Column("alpha"); table.Column("beta"); table.Column("gamma"); table.Column("delta") } - converters { - [Duration] = (it) -> it.toUnit("s") - ["delta"] = (it) -> it.join(",") - } - }.renderDocument(data) +} + +/// Style options specific to HTML tables +/// Default values produce a table with two-space indentation +open class TableStyleHTML extends TableStyle { + /// Controls which character(s) are used for one indentation level + indent: String = " " + + /// Indent controls the baseline indent level applied to every line + baseIndent: String = "" + + local _style = this + + fixedWidth = false + defaultCorner = "" + horizontals { + ["top"] = "\(baseIndent)\n\(baseIndent)\(indent)<\(if (_style.includeHeader) "thead" else "tbody")>" + ["inner"] = "\(baseIndent)\(indent)\n\(baseIndent)\(indent)" + ["bottom"] = "\(baseIndent)\(indent)\n\(baseIndent)
" } - ["boxDrawingLight"] { - new table.TableRenderer { - style = table.boxDrawingLightStyle - columns { table.Column("alpha"); table.Column("beta"); table.Column("gamma") } - }.renderDocument(data) + verticals { + ["left"] = "\(baseIndent)\(indent.repeat(2))\n\(baseIndent)\(indent.repeat(3))" + ["inner"] = "\n\(baseIndent)\(indent.repeat(3))" + ["right"] = "\n\(baseIndent)\(indent.repeat(2))" } - ["boxDrawingHeavy"] { - new table.TableRenderer { - style = table.boxDrawingHeavyStyle - columns { table.Column("alpha"); table.Column("beta"); table.Column("gamma") } - }.renderDocument(data) + headerVerticals { + ["left"] = "\(baseIndent)\(indent.repeat(2))\n\(baseIndent)\(indent.repeat(3))" + ["inner"] = "\n\(baseIndent)\(indent.repeat(3))" + ["right"] = "\n\(baseIndent)\(indent.repeat(2))" } - ["markdown"] { - new table.TableRenderer { - style = table.markdownStyle - columns { table.Column("alpha"); table.Column("beta"); table.Column("gamma") } - }.renderDocument(data) +} + +local class InterimTable { + + style: TableStyle + converters: Mapping Any> + + columns: Listing + rows: List + + columnWidths: Mapping = new { + for (column in columns) { + [column.key] = math.max( + column.title.length, + renderedCells.fold(0, (acc, it) -> + math.max(acc, it.getOrNull(column.key)?.length ?? 0) + ) + ) as UInt + } } - ["html"] { - new table.TableRenderer { - style = table.htmlStyle - columns { table.Column("alpha"); table.Column("beta"); table.Column("gamma") } - }.renderDocument(data) + + renderedCells: Listing<*Mapping | RowDirective> = new { + for (row in rows) { + when (row is RowDirective) { + row + } else { + new { + for (column in columns) { + [column.key] = renderCell(column.key, row.getPropertyOrNull(column.key)) + } + } + } + } } - ["html with custom indent and no header"] { - new table.TableRenderer { - style = (table.htmlStyle) { - baseIndent = "indent:" - indent = "\t" - includeHeader = false + + rendered = new Listing { + when (style.horizontals["top"] != null) { + renderHorizontalRule("top") + } + when (style.includeHeader) { + renderRow(columns.toList().toMap((col) -> col.key, (col) -> col.title), true) + when (style.horizontals["inner"] != null) { + renderHorizontalRule("inner") + } + } + for (row in renderedCells) { + when (row is RowDirective) { + row.render(style) + } else { + renderRow(row.toMap(), false) + } + } + when (style.horizontals["bottom"] != null) { + renderHorizontalRule("bottom") + } + }.join("\n") + "\n" + + function renderCell(col: String, value: Any): String = + (converters.getOrNull(col)?.apply(value)?.toString()) ?? + (converters.getOrNull(value.getClass())?.apply(value)?.toString()) ?? + value?.toString() ?? + style.nullPlaceholder + + function renderRow(renderedCells: Map, inHeader: Boolean) = new Listing { + local verticals = if (inHeader) style.headerVerticals else style.verticals + verticals["left"] ?? "" + for (i, col in columns) { + when (style.fixedWidth) { + " " + if (col.align == "left") + renderedCells[col.key].padEnd(columnWidths[col.key], " ") + else if (col.align == "right") + renderedCells[col.key].padStart(columnWidths[col.key], " ") + else "" + " " + } else { + renderedCells[col.key] + } + verticals[if (i == columns.length - 1) "right" else "inner"] ?? "" + } + }.join("") + + function renderHorizontalRule(verticalPosition: VerticalPosition): String = new Listing { + style.corners["left"][verticalPosition] + when (style.fixedWidth) { + for (i, col in columns) { + style.horizontals[verticalPosition].repeat(columnWidths[col.key] + 2) + style.corners[if (i == columns.length - 1) "right" else "inner"][verticalPosition] + } + } else { + style.horizontals[verticalPosition] + } + }.join("") +} + +/// [pkl.ValueRenderer] subclass used to render pretty-printed tables +class TableRenderer extends ValueRenderer { + /// Style to use for table rendering + style: TableStyle + + /// Listing of properties that should be rendered as table columns + columns: Listing(!isEmpty || throw("TableRenderer must be configured to render at least one column")) + + local self = this + + /// Renders [value] as a pretty-printed table. + function renderDocument(value: Any): String = new InterimTable { + columns { + for (col in self.columns) { + if (col is Column) col else Column(col) } - columns { table.Column("alpha"); table.Column("beta"); table.Column("gamma") } - }.renderDocument(data) + } + rows = + if (value is ListLike?) value.toList() + else throw("Only \(listLikeDescription) values can be rendered as a table. Instead, found a \(value.getClass()).\n\nValue:\n\(value)") + style = self.style + converters = self.converters + }.rendered + + /// Unsupported. [renderCell] is used instead as the column name must be known to look up the relevant converter + function renderValue(_): String = throw("TableRenderer can only render documents, not values") +} + +/// [TableStyle] using unicode box drawing characters (light variants) +const boxDrawingLightStyle: TableStyle = new { + defaultHorizontal = "─" + defaultVertical = "│" + corners { + ["left"] { + ["top"] = "┌" + ["inner"] = "├" + ["bottom"] = "└" + } + ["inner"] { + ["top"] = "┬" + ["inner"] = "┼" + ["bottom"] = "┴" + } + ["right"] { + ["top"] = "┐" + ["inner"] = "┤" + ["bottom"] = "┘" + } } } + +/// [TableStyle] using unicode box drawing characters (heavy variants) +const boxDrawingHeavyStyle: TableStyle = new { + defaultHorizontal = "━" + defaultVertical = "┃" + corners { + ["left"] { + ["top"] = "┏" + ["inner"] = "┣" + ["bottom"] = "┗" + } + ["inner"] { + ["top"] = "┳" + ["inner"] = "╋" + ["bottom"] = "┻" + } + ["right"] { + ["top"] = "┓" + ["inner"] = "┫" + ["bottom"] = "┛" + } + } +} + +/// [TableStyle] that renders Markdown-compatible tables +const markdownStyle: TableStyle = new { + defaultCorner = "|" + horizontals { + ["top"] = null + ["bottom"] = null + } +} + +/// [TableStyle] that renders HTML tables +const htmlStyle: TableStyleHTML = new {} diff --git a/assets/pkl/external/pkl-pantry/packages/pkl.toml/toml.pkl b/assets/pkl/external/pkl-pantry/packages/pkl.toml/toml.pkl index ea624f4..b408219 100644 --- a/assets/pkl/external/pkl-pantry/packages/pkl.toml/toml.pkl +++ b/assets/pkl/external/pkl-pantry/packages/pkl.toml/toml.pkl @@ -13,18 +13,205 @@ // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// -module pkl.toml.tests.toml - -amends "pkl:test" - -examples { - ["basic.pkl"] { - import("../examples/basic.pkl").output.text - } - ["converters.pkl"] { - import("../examples/converters.pkl").output.text - } - ["dates.pkl"] { - import("../examples/dates.pkl").output.text - } +/// A renderer for [TOML](https://toml.io) configuration files. +/// +/// Basic usage: +/// ``` +/// import "package://pkg.pkl-lang.org/pantry/pkl.toml@1.0.0" +/// +/// output { +/// renderer = new toml.Renderer {} +/// } +/// ``` +/// +/// To render TOML dates and times, use [Date], [Time], and [DateTime]. +@ModuleInfo { minPklVersion = "0.25.0" } +module pkl.toml.toml + +abstract class AbstractDateTime { + value: String +} + +/// A TOML [Local Date](https://toml.io/en/v1.0.0#local-date) value. +class Date extends AbstractDateTime { + value: String(matches(Regex(#"(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])"#))) +} + +/// A TOML [Local Time](https://toml.io/en/v1.0.0#local-time) value. +class Time extends AbstractDateTime { + value: String(matches(Regex(#"([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?"#))) +} + +/// A TOML [Offset Date-Time](https://toml.io/en/v1.0.0#offset-date-time) +/// or [Local Date-Time](https://toml.io/en/v1.0.0#local-date-time) value. +class DateTime extends AbstractDateTime { + value: String(matches(Regex(#"(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])[T ]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(Z|[+-]([01][0-9]|2[0-3]):([0-5][0-9]))?"#))) +} + +/// Renders values as TOML. +class Renderer extends ValueRenderer { + /// Value converters to apply before values are rendered. + /// + /// For further information see [PcfRenderer.converters]. + /// For path converters, only "*" is supported. + converters: Mapping Any> + + function renderValue(value: Any) = + let (_value = getBasicValue(value, false)) + doRenderValue(_value, List()).trim() + + function renderDocument(value: Any) = + if (!isTableLike(value)) + throw(""" + Invalid input: TOML can only render object-types at the root level. Received: \(value) + """) + else + renderValue(value) + + local jsonRenderer = new JsonRenderer {} + + local function getConvertersForValue(value: Any): List<(Any) -> unknown> = new Listing { + when (convertersMap.containsKey(value.getClass())) { + convertersMap[value.getClass()] + } + when (convertersMap.containsKey("*")) { + convertersMap["*"] + } + }.toList() + + local function applyConverters(value: Any) = + let (converters = getConvertersForValue(value)) + converters.fold(value, (acc, converter) -> converter.apply(acc)) + + /// Traverses the object and casts it down to its basic type: Map, List, or the primitive value. Runs each + /// value through the converter if there is a match. + /// `skipConversion` is a helper flag to avoid infinite recursion in case the converter returns the same type. + local function getBasicValue(value: Any, skipConversion: Boolean) = + if (!skipConversion && !getConvertersForValue(value).isEmpty) + getBasicValue(applyConverters(value), true) + // If the value is Dynamic, and we have both elements and properties, it's ambiguous whether we should + // render as a table or an array. + else if (value is Dynamic && isTableLike(value) && isArrayLike(value)) + throw(""" + Cannot render object with both properties/entries and elements as TOML. Received: \(value) + """) + else if (isTableLike(value)) + getMap(value) + .mapValues((_, elem) -> getBasicValue(elem, false)) + else if (isArrayLike(value)) getList(value).map((elem) -> getBasicValue(elem, false)) + else value + + /// Underlying implementation for rendering values as toml + local function doRenderValue(value: Any, path: List): String = + if (isTableArray(value)) + renderTableArray(value, path) + else if (value is Map) + renderTable(value, path) + else + renderInlineValue(value) + + /// Determine whether an object is map-like. We'll consider any Dynamic that doesn't have any elements as map-like. + local function isTableLike(obj: Any) = !(obj is AbstractDateTime) && ((obj is Dynamic && obj.toList().isEmpty) || obj is MapLike) + + /// Determine whether an object is list-like. We'll consider any Dynamic that has elements as list-like. + local function isArrayLike(obj: Any) = (obj is Dynamic && !obj.toList().isEmpty) || obj is ListLike + + /// Convert an object to its Map representation. Toml doesn't include null so we should filter out null properties. + local function getMap(obj: MapLike|Dynamic) = (if (obj is Map) obj else obj.toMap()).filter((_, elem) -> elem != null) + + /// Convert an object to its List representation. + local function getList(obj: ListLike|Dynamic) = if (obj is List) obj else obj.toList() + + /// Determine if we should render this value as an array of tables or not. + /// A value is an array of tables if all of the inhabitants are table-like. + local function isTableArray(value: Any) = + value is List && value.every((elem) -> elem is Map) + + local function isTableTypeProp(value: Any) = value is Map || isTableArray(value) + + local convertersMap = converters.toMap() + + /// Render the value as an inline value (e.g. inline array, object, or primitive) + local function renderInlineValue(value: Any) = + if (value is Number && value.isNaN) + "nan" + else if (value == Infinity) + "inf" + else if (value is String) + renderString(value) + else if (value is Number|Boolean) + jsonRenderer.renderValue(value) + else if (value is AbstractDateTime) + value.value + else if (value is Map) + "{ " + new Listing { + for (k, v in value) { + "\(makeSingleKey(k)) = \(renderInlineValue(v))" + } + }.toList().join(", ") + " }" + else if (value is List) + "[ " + value.map((elem) -> renderInlineValue(elem)).join(", ") + " ]" + else + throw("Not sure how to render value: \(value). Try defining a converter for this type.") + + /// Render a string making sure multi-line use the """ multi-line syntax for better readability. + local function renderString(value: String) = + if (value.contains("\n")) + ("\"\"\"\n" + + value.split("\n") + .map((line) -> jsonRenderer.renderValue(line).drop(1).dropLast(1)) + .join("\n") + + "\"\"\"") + else jsonRenderer.renderValue(value) + + local function renderSingleTableArray(map: Map, path: List) = + let (nativeProps = map.filter((_, value) -> !isTableTypeProp(value))) + let (tableProps = map.filter((_, value) -> isTableTypeProp(value))) + new Listing { + """ + + [[\(makeKey(path))]] + """ + for (k, v in nativeProps) { + "\(makeSingleKey(k)) = \(renderInlineValue(v))" + } + for (k, v in tableProps) { + doRenderValue(v, path.add(k)) + } + }.toList().join("\n") + + local function renderTableArray(value: List, path: List) = + value.map((elem) -> renderSingleTableArray(getMap(elem), path)).join("\n") + + local function makeSingleKey(key: String) = if (key.matches(Regex(#"[A-Za-z0-9_-]+"#))) + key + else + jsonRenderer.renderValue(key) + + local function makeKey(path: List): String = path.map((k) -> makeSingleKey(k)).join(".") + + local function renderTable(m: Map, path: List): String = + let (nativeProps = m.filter((_, value) -> !isTableTypeProp(value))) + let (tableProps = m.filter((_, value) -> isTableTypeProp(value))) + new Listing { + // Only render the table header if we are in an object's context or empty map context + when (!path.isEmpty && (nativeProps.length > 0 || m.length == 0)) { + """ + + [\(makeKey(path))] + """ + } + for (k, v in nativeProps) { + "\(makeSingleKey(k)) = \(renderInlineValue(v))" + } + for (k, v in tableProps) { + doRenderValue(v, path.add(k)) + } + } + .toList() + .join("\n") } + +local typealias MapLike = Typed|Map|Mapping + +local typealias ListLike = List|Listing diff --git a/build.gradle.kts b/build.gradle.kts index b17a99a..6b5f9ad 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("org.pkl-lang") version "0.29.1" + id("org.pkl-lang") version "0.30.2" } val maybeVersion = System.getenv("VERSION") diff --git a/deps/pkl/APIServer.pkl b/deps/pkl/APIServer.pkl index ebcb37f..a7edf1b 100644 --- a/deps/pkl/APIServer.pkl +++ b/deps/pkl/APIServer.pkl @@ -4,7 +4,7 @@ /// server settings such as host IP and port number, as well as route definitions. The API server /// is designed to handle incoming requests and route them to the appropriate handlers, ensuring /// proper management of HTTP methods and deferred processing. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/api_server" } diff --git a/deps/pkl/APIServerRequest.pkl b/deps/pkl/APIServerRequest.pkl index 360525f..8646dd4 100644 --- a/deps/pkl/APIServerRequest.pkl +++ b/deps/pkl/APIServerRequest.pkl @@ -9,7 +9,7 @@ /// - Functions to decode Base64 encoded request data. /// - File management utilities like retrieving file types and paths. /// - Filtering files by MIME type. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/api_server_request" } diff --git a/deps/pkl/APIServerResponse.pkl b/deps/pkl/APIServerResponse.pkl index aa0bd2a..2179c0c 100644 --- a/deps/pkl/APIServerResponse.pkl +++ b/deps/pkl/APIServerResponse.pkl @@ -13,7 +13,7 @@ /// - [success]: A flag indicating the success or failure of the API request. /// - [file]: A URI pointing to any file returned by the server in the response. /// - [errors]: The error block containing details of the error if the request was unsuccessful. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/api_server_response" } diff --git a/deps/pkl/Data.pkl b/deps/pkl/Data.pkl index 4084e96..6c037a5 100644 --- a/deps/pkl/Data.pkl +++ b/deps/pkl/Data.pkl @@ -1,5 +1,5 @@ /// Abstractions for Data folder -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/data" } diff --git a/deps/pkl/Docker.pkl b/deps/pkl/Docker.pkl index 9458672..909480e 100644 --- a/deps/pkl/Docker.pkl +++ b/deps/pkl/Docker.pkl @@ -2,7 +2,7 @@ /// resources within the KDEPS framework. It allows for the specification /// of package management, including additional package repositories (PPAs) /// and models to be used within Docker containers. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/docker" } diff --git a/deps/pkl/Document.pkl b/deps/pkl/Document.pkl index 758ddeb..73d367e 100644 --- a/deps/pkl/Document.pkl +++ b/deps/pkl/Document.pkl @@ -1,7 +1,7 @@ /// Common parser and document renderer functions used across all resources. /// /// Tools for Parsing and Generating JSON, YAML and XML documents -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/document" } diff --git a/deps/pkl/Exec.pkl b/deps/pkl/Exec.pkl index e9d411d..4f080bb 100644 --- a/deps/pkl/Exec.pkl +++ b/deps/pkl/Exec.pkl @@ -3,7 +3,7 @@ /// standard output and error, as well as handling environment variables and /// exit codes. The module provides functionalities to retrieve and manage /// executable resources based on their identifiers. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/exec" } diff --git a/deps/pkl/HTTP.pkl b/deps/pkl/HTTP.pkl index 84b677d..9444e32 100644 --- a/deps/pkl/HTTP.pkl +++ b/deps/pkl/HTTP.pkl @@ -3,7 +3,7 @@ /// HTTP requests, including method specifications, request data, headers, /// and handling of responses. This module provides functionalities to /// retrieve and manage HTTP client resources based on their identifiers. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/http" } diff --git a/deps/pkl/Item.pkl b/deps/pkl/Item.pkl index a1dd53b..200456e 100644 --- a/deps/pkl/Item.pkl +++ b/deps/pkl/Item.pkl @@ -2,7 +2,7 @@ /// /// This module provides functions to interact with records representing iterations or elements in a for loop. /// The module supports retrieving, navigating, and listing records without requiring a specific identifier. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/item" } diff --git a/deps/pkl/Kdeps.pkl b/deps/pkl/Kdeps.pkl index 89e4a38..b41d3dd 100644 --- a/deps/pkl/Kdeps.pkl +++ b/deps/pkl/Kdeps.pkl @@ -1,5 +1,5 @@ /// Abstractions for Kdeps Configuration -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/kdeps" } diff --git a/deps/pkl/LLM.pkl b/deps/pkl/LLM.pkl index 62c2d17..67a3990 100644 --- a/deps/pkl/LLM.pkl +++ b/deps/pkl/LLM.pkl @@ -13,7 +13,7 @@ /// - Providing methods to retrieve various pieces of information related to the LLM interaction, /// such as the prompt text, response text, file paths, JSON keys, and whether image generation was /// involved. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/llm" } diff --git a/deps/pkl/Memory.pkl b/deps/pkl/Memory.pkl index 0413af1..53ebed7 100644 --- a/deps/pkl/Memory.pkl +++ b/deps/pkl/Memory.pkl @@ -1,5 +1,5 @@ /// Abstractions for Memory records -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/memory" } diff --git a/deps/pkl/Project.pkl b/deps/pkl/Project.pkl index 75bedff..2c02d57 100644 --- a/deps/pkl/Project.pkl +++ b/deps/pkl/Project.pkl @@ -4,7 +4,7 @@ /// configurations related to the API server, Docker agent settings, and security settings. These /// settings allow customization of the project's environment, such as enabling API server mode or /// configuring Docker and security parameters. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/project" } diff --git a/deps/pkl/Python.pkl b/deps/pkl/Python.pkl index 565b851..ccf4b44 100644 --- a/deps/pkl/Python.pkl +++ b/deps/pkl/Python.pkl @@ -3,7 +3,7 @@ /// capturing their standard output, standard error, and handling environment /// variables as well as exit codes. The module provides utilities for retrieving /// and managing executable resources identified by unique resource actionIDs. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/python" } diff --git a/deps/pkl/Resource.pkl b/deps/pkl/Resource.pkl index be61713..bba408a 100644 --- a/deps/pkl/Resource.pkl +++ b/deps/pkl/Resource.pkl @@ -4,7 +4,7 @@ /// including actions that can be performed on these resources, validation checks, /// and error handling mechanisms. Each resource can define its actionID, name, description, /// category, dependencies, and how it runs. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/resource" } diff --git a/deps/pkl/Session.pkl b/deps/pkl/Session.pkl index b294108..7d2f48d 100644 --- a/deps/pkl/Session.pkl +++ b/deps/pkl/Session.pkl @@ -1,5 +1,5 @@ /// Abstractions for Session records -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/session" } diff --git a/deps/pkl/Skip.pkl b/deps/pkl/Skip.pkl index 0793a92..01b8813 100644 --- a/deps/pkl/Skip.pkl +++ b/deps/pkl/Skip.pkl @@ -1,7 +1,7 @@ /// Skip condition functions used across all resources. /// /// Tools for creating skip logic validations -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/skip" } diff --git a/deps/pkl/Tool.pkl b/deps/pkl/Tool.pkl index f8f0b0b..4d38e0e 100644 --- a/deps/pkl/Tool.pkl +++ b/deps/pkl/Tool.pkl @@ -1,5 +1,5 @@ /// Abstractions for Tool records -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/tool" } diff --git a/deps/pkl/Utils.pkl b/deps/pkl/Utils.pkl index 7afc2fb..f3f7dd5 100644 --- a/deps/pkl/Utils.pkl +++ b/deps/pkl/Utils.pkl @@ -1,7 +1,7 @@ /// Tools for Kdeps Resources /// /// This module includes tools for interacting with Kdeps -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/utils" } diff --git a/deps/pkl/WebServer.pkl b/deps/pkl/WebServer.pkl index d814440..d813a06 100644 --- a/deps/pkl/WebServer.pkl +++ b/deps/pkl/WebServer.pkl @@ -4,7 +4,7 @@ /// server binding details (host and port) and route configurations. The server /// handles HTTP requests, routing them to appropriate handlers based on defined /// paths and server types. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/web_server" } diff --git a/deps/pkl/Workflow.pkl b/deps/pkl/Workflow.pkl index cf3590e..9f211ec 100644 --- a/deps/pkl/Workflow.pkl +++ b/deps/pkl/Workflow.pkl @@ -7,7 +7,7 @@ /// /// This module also ensures the proper structure of workflows using validation checks for names, /// workflow references, action formats, and versioning patterns. -@ModuleInfo { minPklVersion = "0.29.1" } +@ModuleInfo { minPklVersion = "0.30.2" } @go.Package { name = "github.com/kdeps/schema/gen/workflow" } diff --git a/go.mod b/go.mod index 05e534c..0c454d2 100644 --- a/go.mod +++ b/go.mod @@ -4,25 +4,25 @@ go 1.23.7 toolchain go1.24.4 -require github.com/apple/pkl-go v0.11.1 +require github.com/apple/pkl-go v0.12.1 retract ( - v0.3.0 - v0.3.1 - v0.3.2 - v0.3.3 - v0.3.4 - v0.3.5 - v0.3.6 - v0.3.7 - v0.3.8 - v0.4.0 - v0.4.1 - v0.4.2 - v0.4.3 - v0.4.4 - v0.4.5 v0.4.6 + v0.4.5 + v0.4.4 + v0.4.3 + v0.4.2 + v0.4.1 + v0.4.0 + v0.3.8 + v0.3.7 + v0.3.6 + v0.3.5 + v0.3.4 + v0.3.3 + v0.3.2 + v0.3.1 + v0.3.0 ) require ( diff --git a/go.sum b/go.sum index f6a23e0..dde48bb 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,5 @@ -github.com/apple/pkl-go v0.8.0 h1:GRcBvFWeXjT9rc7A5gHK89qrel2wGZ3/a7ge4rPlT5M= -github.com/apple/pkl-go v0.8.0/go.mod h1:5Hwil5tyZGrOekh7JXLZJvIAcGHb4gT19lnv4WEiKeI= -github.com/apple/pkl-go v0.8.1 h1:C7Z2MJAJX8bVPmk4ZL8I7tpMnuw+tLcwQ6jeR7/BYXs= -github.com/apple/pkl-go v0.8.1/go.mod h1:5Hwil5tyZGrOekh7JXLZJvIAcGHb4gT19lnv4WEiKeI= -github.com/apple/pkl-go v0.9.0 h1:aA4Bh+WQ797p8nEnQhHzCahVuQP2HJ40ffSQWlAR5es= -github.com/apple/pkl-go v0.9.0/go.mod h1:5Hwil5tyZGrOekh7JXLZJvIAcGHb4gT19lnv4WEiKeI= -github.com/apple/pkl-go v0.11.1 h1:Rq9/x8mHMZS5taG/YbhImZyXEdQFRlbpbEd2zaW2+9Y= -github.com/apple/pkl-go v0.11.1/go.mod h1:EDQmYVtFBok/eLI+9rT0EoBBXNtMM1THwR+rwBcAH3I= +github.com/apple/pkl-go v0.12.1 h1:4G8vAAx7eMVOdUuzyCesbHcYBMbzDyRfS00+wA/LOM0= +github.com/apple/pkl-go v0.12.1/go.mod h1:EDQmYVtFBok/eLI+9rT0EoBBXNtMM1THwR+rwBcAH3I= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/pr_body.md b/pr_body.md new file mode 100644 index 0000000..0a6c75f --- /dev/null +++ b/pr_body.md @@ -0,0 +1,39 @@ +## Auto-update Dependencies + +This PR automatically updates PKL and pkl-go dependencies to their latest versions. + +### PKL Update + +**Version:** `0.29.1` → `0.30.2` + +**Changes:** +- Updated `versions.json` +- Updated `build.gradle.kts` +- Updated `minPklVersion` in all .pkl files (deps/pkl and assets/pkl) +- Downloaded updated PKL dependencies +- Updated import paths +- Updated embedded assets + +**Release Notes:** https://github.com/apple/pkl/releases/tag/v0.30.2 + +### pkl-go Update + +**Version:** `0.11.1` → `0.12.1` + +**Changes:** +- Updated `versions.json` +- Updated `go.mod` and `go.sum` +- Downloaded updated pkl-go dependencies +- Updated import paths +- Updated embedded assets + +**Release Notes:** https://github.com/apple/pkl-go/releases/tag/v0.12.1 + + +### Test Results + +All Go tests passed successfully after the update. + + +--- +*This PR was automatically created by the auto-update-dependencies workflow.* diff --git a/versions.json b/versions.json index cfa43d1..50f09d7 100644 --- a/versions.json +++ b/versions.json @@ -1,12 +1,12 @@ { "pkl": { - "version": "0.29.1", + "version": "0.30.2", "url": "https://github.com/apple/pkl/releases" }, "dependencies": { "pkl-go": { - "version": "0.11.1", - "url": "package://pkg.pkl-lang.org/pkl-go/pkl.golang@0.11.1", + "version": "0.12.1", + "url": "package://pkg.pkl-lang.org/pkl-go/pkl.golang@0.12.1", "files": [ "go.pkl" ]