From 2d5c913e6cfde554ff480d087250b5dfb1c7738c Mon Sep 17 00:00:00 2001 From: Cam Hutchison Date: Sat, 28 Dec 2024 18:04:33 +1100 Subject: [PATCH 1/3] tools: Hermitise repo and pin versions required Hermitise this repository as the version of golangci-lint used is not compatible with more recent Go releases so we will either need to pin to an older version of Go or upgrade golangci-lint. The former is tricky to do simply, and the latter is painful with the churn in golangci-lint. The alternative is to use hermit and pin the versions currently required. This is what is done here: hermit init --no-git hermit install go-1.13.5 hermit install golangci-lint-1.37.0 Remove the custom version login for golangci-lint from the Makefile, and add the hermit auto-activation. Note that the only 1.13 version of Go in hermit is 1.13.5, while the latest point release is 1.13.15. I chose 1.13 as that is what is in the `go.mod` file. Subsequent commits will bump these versions and adjust the code to work with newer golangci-lint errors. Update GitHub CI to rely on hermit for installing the right versions instead of using specific actions. Also run on all PRs, not just those to be merged to master. This allows CI to run for stacked PRs. --- .github/workflows/ci.yaml | 8 +------ Makefile | 22 ++++++++---------- bin/.go-1.13.5.pkg | 1 + bin/.golangci-lint-1.37.0.pkg | 1 + bin/README.hermit.md | 7 ++++++ bin/activate-hermit | 21 +++++++++++++++++ bin/go | 1 + bin/gofmt | 1 + bin/golangci-lint | 1 + bin/hermit | 43 +++++++++++++++++++++++++++++++++++ bin/hermit.hcl | 1 + 11 files changed, 88 insertions(+), 19 deletions(-) create mode 120000 bin/.go-1.13.5.pkg create mode 120000 bin/.golangci-lint-1.37.0.pkg create mode 100644 bin/README.hermit.md create mode 100755 bin/activate-hermit create mode 120000 bin/go create mode 120000 bin/gofmt create mode 120000 bin/golangci-lint create mode 100755 bin/hermit create mode 100644 bin/hermit.hcl diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2cc3051..e0a1ec5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -7,7 +7,6 @@ on: push: branches: [ master ] pull_request: - branches: [ master ] jobs: test: @@ -15,9 +14,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions/setup-go@v2 - with: - go-version: '^1.14' - run: make test - run: make cover lint: @@ -25,6 +21,4 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: golangci/golangci-lint-action@v2 - with: - version: v1.37.0 + - run: make lint diff --git a/Makefile b/Makefile index 682b9ba..f635373 100644 --- a/Makefile +++ b/Makefile @@ -48,19 +48,9 @@ FAIL_COVERAGE = { echo '$(COLOUR_RED)FAIL - Coverage below $(COVERAGE)%$(COLOUR_ .PHONY: check-coverage cover test # --- Lint --------------------------------------------------------------------- -GOLINT_VERSION = 1.37.0 -GOLINT_INSTALLED_VERSION = $(or $(word 4,$(shell golangci-lint --version 2>/dev/null)),0.0.0) -GOLINT_USE_INSTALLED = $(filter $(GOLINT_INSTALLED_VERSION),v$(GOLINT_VERSION) $(GOLINT_VERSION)) -GOLINT = $(if $(GOLINT_USE_INSTALLED),golangci-lint,golangci-lint-v$(GOLINT_VERSION)) -GOBIN ?= $(firstword $(subst :, ,$(or $(GOPATH),$(HOME)/go)))/bin - -lint: $(if $(GOLINT_USE_INSTALLED),,$(GOBIN)/$(GOLINT)) ## Lint go source code - $(GOLINT) run - -$(GOBIN)/$(GOLINT): - GOBIN=/tmp go install github.com/golangci/golangci-lint/cmd/golangci-lint@v$(GOLINT_VERSION) && \ - mv /tmp/golangci-lint $@ +lint: ## Lint go source code + golangci-lint run .PHONY: lint @@ -77,3 +67,11 @@ $(O): @mkdir -p $@ .PHONY: help + +define nl + + +endef +ifndef ACTIVE_HERMIT +$(eval $(subst \n,$(nl),$(shell bin/hermit env -r | sed 's/^\(.*\)$$/export \1\\n/'))) +endif diff --git a/bin/.go-1.13.5.pkg b/bin/.go-1.13.5.pkg new file mode 120000 index 0000000..383f451 --- /dev/null +++ b/bin/.go-1.13.5.pkg @@ -0,0 +1 @@ +hermit \ No newline at end of file diff --git a/bin/.golangci-lint-1.37.0.pkg b/bin/.golangci-lint-1.37.0.pkg new file mode 120000 index 0000000..383f451 --- /dev/null +++ b/bin/.golangci-lint-1.37.0.pkg @@ -0,0 +1 @@ +hermit \ No newline at end of file diff --git a/bin/README.hermit.md b/bin/README.hermit.md new file mode 100644 index 0000000..e889550 --- /dev/null +++ b/bin/README.hermit.md @@ -0,0 +1,7 @@ +# Hermit environment + +This is a [Hermit](https://github.com/cashapp/hermit) bin directory. + +The symlinks in this directory are managed by Hermit and will automatically +download and install Hermit itself as well as packages. These packages are +local to this environment. diff --git a/bin/activate-hermit b/bin/activate-hermit new file mode 100755 index 0000000..fe28214 --- /dev/null +++ b/bin/activate-hermit @@ -0,0 +1,21 @@ +#!/bin/bash +# This file must be used with "source bin/activate-hermit" from bash or zsh. +# You cannot run it directly +# +# THIS FILE IS GENERATED; DO NOT MODIFY + +if [ "${BASH_SOURCE-}" = "$0" ]; then + echo "You must source this script: \$ source $0" >&2 + exit 33 +fi + +BIN_DIR="$(dirname "${BASH_SOURCE[0]:-${(%):-%x}}")" +if "${BIN_DIR}/hermit" noop > /dev/null; then + eval "$("${BIN_DIR}/hermit" activate "${BIN_DIR}/..")" + + if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ]; then + hash -r 2>/dev/null + fi + + echo "Hermit environment $("${HERMIT_ENV}"/bin/hermit env HERMIT_ENV) activated" +fi diff --git a/bin/go b/bin/go new file mode 120000 index 0000000..d3e6925 --- /dev/null +++ b/bin/go @@ -0,0 +1 @@ +.go-1.13.5.pkg \ No newline at end of file diff --git a/bin/gofmt b/bin/gofmt new file mode 120000 index 0000000..d3e6925 --- /dev/null +++ b/bin/gofmt @@ -0,0 +1 @@ +.go-1.13.5.pkg \ No newline at end of file diff --git a/bin/golangci-lint b/bin/golangci-lint new file mode 120000 index 0000000..cf27348 --- /dev/null +++ b/bin/golangci-lint @@ -0,0 +1 @@ +.golangci-lint-1.37.0.pkg \ No newline at end of file diff --git a/bin/hermit b/bin/hermit new file mode 100755 index 0000000..7fef769 --- /dev/null +++ b/bin/hermit @@ -0,0 +1,43 @@ +#!/bin/bash +# +# THIS FILE IS GENERATED; DO NOT MODIFY + +set -eo pipefail + +export HERMIT_USER_HOME=~ + +if [ -z "${HERMIT_STATE_DIR}" ]; then + case "$(uname -s)" in + Darwin) + export HERMIT_STATE_DIR="${HERMIT_USER_HOME}/Library/Caches/hermit" + ;; + Linux) + export HERMIT_STATE_DIR="${XDG_CACHE_HOME:-${HERMIT_USER_HOME}/.cache}/hermit" + ;; + esac +fi + +export HERMIT_DIST_URL="${HERMIT_DIST_URL:-https://github.com/cashapp/hermit/releases/download/stable}" +HERMIT_CHANNEL="$(basename "${HERMIT_DIST_URL}")" +export HERMIT_CHANNEL +export HERMIT_EXE=${HERMIT_EXE:-${HERMIT_STATE_DIR}/pkg/hermit@${HERMIT_CHANNEL}/hermit} + +if [ ! -x "${HERMIT_EXE}" ]; then + echo "Bootstrapping ${HERMIT_EXE} from ${HERMIT_DIST_URL}" 1>&2 + INSTALL_SCRIPT="$(mktemp)" + # This value must match that of the install script + INSTALL_SCRIPT_SHA256="180e997dd837f839a3072a5e2f558619b6d12555cd5452d3ab19d87720704e38" + if [ "${INSTALL_SCRIPT_SHA256}" = "BYPASS" ]; then + curl -fsSL "${HERMIT_DIST_URL}/install.sh" -o "${INSTALL_SCRIPT}" + else + # Install script is versioned by its sha256sum value + curl -fsSL "${HERMIT_DIST_URL}/install-${INSTALL_SCRIPT_SHA256}.sh" -o "${INSTALL_SCRIPT}" + # Verify install script's sha256sum + openssl dgst -sha256 "${INSTALL_SCRIPT}" | \ + awk -v EXPECTED="$INSTALL_SCRIPT_SHA256" \ + '$2!=EXPECTED {print "Install script sha256 " $2 " does not match " EXPECTED; exit 1}' + fi + /bin/bash "${INSTALL_SCRIPT}" 1>&2 +fi + +exec "${HERMIT_EXE}" --level=fatal exec "$0" -- "$@" diff --git a/bin/hermit.hcl b/bin/hermit.hcl new file mode 100644 index 0000000..29b6a5c --- /dev/null +++ b/bin/hermit.hcl @@ -0,0 +1 @@ +manage-git = false From 4c3d4e2bba5b29e78c698b25045f81d1990d0c3f Mon Sep 17 00:00:00 2001 From: Cam Hutchison Date: Tue, 13 Jul 2021 21:11:11 +1000 Subject: [PATCH 2/3] cmd: Rename jx to jnx and split out flag version Rename `jx` to `jnx` as the jx binary name is already taken by jenkinsx. But jnx is also a better name since it sounds like jinx, and that's what you say when someone duplicates a word at the same time as you. jsonnet is about de-duplicating through templating, so jnx works. It can also be pronounced as a single syllable, which is also nice. Split the Go flag version of jnx into a completely separate program rather than having a `parseCLI()` function selected by build tags. The only reason for the flag version is to use the flag code path of jsonnext to demonstrate its use and to verify it works. jnx itself is going to evolve in future to contain subcommands which I do not intend to support in the flag version of jnx. So split it out so jnx can evolve on its own. Keep `jx.jsonnet` for now for backwards compatibility. It will be removed when I know I've removed all existing uses under that name. --- Makefile | 7 +- cmd/{jx => jnx}/doc.go | 4 +- cmd/jnx/jnxflag/main.go | 88 +++++++++++++++++++++++ cmd/{jx => jnx}/main.go | 12 ++-- cmd/jx/cli_flag.go | 26 ------- cmd/jx/cli_kong.go | 15 ---- kong/example_test.go | 8 +-- kong/kong_conformance_test.go | 4 +- lib/array_test.jsonnet | 2 +- lib/jnx.jsonnet | 9 +++ lib/{jx_test.jsonnet => jnx_test.jsonnet} | 12 ++-- lib/jx.jsonnet | 11 +-- lib/object_test.jsonnet | 2 +- lib/op_test.jsonnet | 2 +- lib/string_test.jsonnet | 2 +- lib/value_test.jsonnet | 2 +- 16 files changed, 128 insertions(+), 78 deletions(-) rename cmd/{jx => jnx}/doc.go (93%) create mode 100644 cmd/jnx/jnxflag/main.go rename cmd/{jx => jnx}/main.go (71%) delete mode 100644 cmd/jx/cli_flag.go delete mode 100644 cmd/jx/cli_kong.go create mode 100644 lib/jnx.jsonnet rename lib/{jx_test.jsonnet => jnx_test.jsonnet} (59%) diff --git a/Makefile b/Makefile index f635373..7775854 100644 --- a/Makefile +++ b/Makefile @@ -14,12 +14,11 @@ clean:: ## Remove generated files build: | $(O) ## Build binaries of directories in ./cmd to out/ go build -o $(O) ./cmd/... - go build -tags flag -o $(O)/jx-flag ./cmd/jx install: ## Build and install binaries in $GOBIN or $GOPATH/bin go install ./cmd/... -$(O)/jx: build +$(O)/jnx: build .PHONY: build install @@ -33,8 +32,8 @@ test: test-go test-jsonnet ## Run tests and generate a coverage file test-go: | $(O) go test -coverprofile=$(COVERFILE) ./... -test-jsonnet: $(O)/jx - $(O)/jx -J $(JSONNET_UNIT) lib/jx_test.jsonnet +test-jsonnet: $(O)/jnx + $(O)/jnx -J $(JSONNET_UNIT) lib/jnx_test.jsonnet check-coverage: test ## Check that test coverage meets the required level @go tool cover -func=$(COVERFILE) | $(CHECK_COVERAGE) || $(FAIL_COVERAGE) diff --git a/cmd/jx/doc.go b/cmd/jnx/doc.go similarity index 93% rename from cmd/jx/doc.go rename to cmd/jnx/doc.go index 8ebeeac..29d28ca 100644 --- a/cmd/jx/doc.go +++ b/cmd/jnx/doc.go @@ -1,6 +1,6 @@ -// jx evaluates a jsonnet file and outputs it as JSON. +// jnx evaluates a jsonnet file and outputs it as JSON. // -// Usage: jx [] +// Usage: jnx [] // // Arguments: // [] File to evaluate. stdin is used if omitted or "-" diff --git a/cmd/jnx/jnxflag/main.go b/cmd/jnx/jnxflag/main.go new file mode 100644 index 0000000..f28ca7f --- /dev/null +++ b/cmd/jnx/jnxflag/main.go @@ -0,0 +1,88 @@ +// jnxflag evaluates a jsonnet file and outputs it as JSON. +// +// Usage of ./jnxflag: +// -A var[=str] +// Add top-level arg var[=str] (from environment if is omitted) +// -J dir +// Add a library search dir +// -V var[=str] +// Add extVar var[=str] (from environment if is omitted) +// -ext-code var[=code] +// Add extVar var[=code] (from environment if is omitted) +// -ext-code-file var=file +// Add extVar var=file code from a file +// -ext-str var[=str] +// Add extVar var[=str] (from environment if is omitted) +// -ext-str-file var=file +// Add extVar var=file string from a file +// -jpath dir +// Add a library search dir +// -tla-code var[=code] +// Add top-level arg var[=code] (from environment if is omitted) +// -tla-code-file var=file +// Add top-level arg var=file code from a file +// -tla-str var=[=str] +// Add top-level arg var=[=str] (from environment if is omitted) +// -tla-str-file var=file +// Add top-level arg var=file string from a file +// +// This program exists just to implement the standard Go flag package parsing. +// The full jnx program uses the kong library and has more features. + +package main + +import ( + "flag" + "fmt" + "os" + + "foxygo.at/jsonnext" + jsonnet "github.com/google/go-jsonnet" +) + +type config struct { + jsonnext.Config + Filename string `arg:"" optional:"" help:"File to evaluate. stdin is used if omitted or \"-\""` +} + +func main() { + cli := parseCLI() + vm := cli.Config.MakeVM("JNXPATH") + + out, err := run(vm, cli.Filename) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + fmt.Print(out) +} + +// Parse CLI using Go's flag package and the helpers in jsonnext. +func parseCLI() *config { + c := &config{} + c.Config = *jsonnext.ConfigFlags(flag.CommandLine) + + flag.Parse() + if flag.NArg() > 1 { + flag.Usage() + os.Exit(1) + } else if flag.NArg() == 1 { + c.Filename = flag.Args()[0] + } + + return c +} + +func run(vm *jsonnet.VM, filename string) (string, error) { + node, _, err := vm.ImportAST("", filename) + if err != nil { + return "", err + } + + out, err := vm.Evaluate(node) + if err != nil { + return "", err + } + + return out, nil +} diff --git a/cmd/jx/main.go b/cmd/jnx/main.go similarity index 71% rename from cmd/jx/main.go rename to cmd/jnx/main.go index 2a086d4..d46eb71 100644 --- a/cmd/jx/main.go +++ b/cmd/jnx/main.go @@ -4,20 +4,22 @@ import ( "fmt" "os" - jxkong "foxygo.at/jsonnext/kong" + jnxkong "foxygo.at/jsonnext/kong" + "github.com/alecthomas/kong" jsonnet "github.com/google/go-jsonnet" ) type config struct { - jxkong.Config + jnxkong.Config Filename string `arg:"" optional:"" help:"File to evaluate. stdin is used if omitted or \"-\""` } func main() { - cli := parseCLI() - vm := cli.Config.MakeVM("JXPATH") + c := &config{Config: *jnxkong.NewConfig()} + kong.Parse(c) + vm := c.Config.MakeVM("JNXPATH") - out, err := run(vm, cli.Filename) + out, err := run(vm, c.Filename) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) diff --git a/cmd/jx/cli_flag.go b/cmd/jx/cli_flag.go deleted file mode 100644 index 3b0756b..0000000 --- a/cmd/jx/cli_flag.go +++ /dev/null @@ -1,26 +0,0 @@ -// +build flag - -package main - -import ( - "flag" - "os" - - "foxygo.at/jsonnext" -) - -// Parse CLI using Go's flag package and the helpers in jsonnext. -func parseCLI() *config { - c := &config{} - c.Config.Config = jsonnext.ConfigFlags(flag.CommandLine) - - flag.Parse() - if flag.NArg() > 1 { - flag.Usage() - os.Exit(1) - } else if flag.NArg() == 1 { - c.Filename = flag.Args()[0] - } - - return c -} diff --git a/cmd/jx/cli_kong.go b/cmd/jx/cli_kong.go deleted file mode 100644 index c4f9969..0000000 --- a/cmd/jx/cli_kong.go +++ /dev/null @@ -1,15 +0,0 @@ -// +build !flag - -package main - -import ( - jxkong "foxygo.at/jsonnext/kong" - "github.com/alecthomas/kong" -) - -// Parse CLI using Kong. -func parseCLI() *config { - c := &config{Config: *jxkong.NewConfig()} - kong.Parse(c) - return c -} diff --git a/kong/example_test.go b/kong/example_test.go index a0a4af3..d932603 100644 --- a/kong/example_test.go +++ b/kong/example_test.go @@ -4,19 +4,19 @@ import ( "fmt" "os" - jxkong "foxygo.at/jsonnext/kong" + jnxkong "foxygo.at/jsonnext/kong" "github.com/alecthomas/kong" ) func Example() { - // Define kong CLI struct embedding jxkong.Config, adding your own + // Define kong CLI struct embedding jnxkong.Config, adding your own // application-specific flags and args. cli := struct { - jxkong.Config + jnxkong.Config Verbose bool Filename string `arg:""` }{ - Config: *jxkong.NewConfig(), // foxygo.at/jsonnext/kong imported as jxkong + Config: *jnxkong.NewConfig(), // foxygo.at/jsonnext/kong imported as jnxkong } // Simulate command line arguments diff --git a/kong/kong_conformance_test.go b/kong/kong_conformance_test.go index bc76f5d..32b7eb4 100644 --- a/kong/kong_conformance_test.go +++ b/kong/kong_conformance_test.go @@ -7,13 +7,13 @@ import ( "foxygo.at/jsonnext" "foxygo.at/jsonnext/conformance" - jxkong "foxygo.at/jsonnext/kong" + jnxkong "foxygo.at/jsonnext/kong" ) type suite struct{} func (s *suite) Parse(t *testing.T, args []string) (*jsonnext.Config, error) { - kcfg := jxkong.NewConfig() + kcfg := jnxkong.NewConfig() parser, err := kong.New(kcfg) if err != nil { return nil, err diff --git a/lib/array_test.jsonnet b/lib/array_test.jsonnet index 1e6f058..c7540ba 100755 --- a/lib/array_test.jsonnet +++ b/lib/array_test.jsonnet @@ -1,4 +1,4 @@ -#!/usr/bin/env -S jx -J //github.com/yugui/jsonnetunit/raw/master +#!/usr/bin/env -S jnx -J //github.com/yugui/jsonnetunit/raw/master local array = import 'array.jsonnet'; local test = import 'jsonnetunit/test.libsonnet'; diff --git a/lib/jnx.jsonnet b/lib/jnx.jsonnet new file mode 100644 index 0000000..98ec4a2 --- /dev/null +++ b/lib/jnx.jsonnet @@ -0,0 +1,9 @@ +// Package jnx is a top-level package to import the subpackages of the +// jnx library. +{ + array:: import 'array.jsonnet', + object:: import 'object.jsonnet', + op:: import 'op.jsonnet', + string:: import 'string.jsonnet', + value:: import 'value.jsonnet', +} diff --git a/lib/jx_test.jsonnet b/lib/jnx_test.jsonnet similarity index 59% rename from lib/jx_test.jsonnet rename to lib/jnx_test.jsonnet index b58c923..b04926f 100755 --- a/lib/jx_test.jsonnet +++ b/lib/jnx_test.jsonnet @@ -1,22 +1,22 @@ -#!/usr/bin/env -S jx -J //github.com/yugui/jsonnetunit/raw/master +#!/usr/bin/env -S jnx -J //github.com/yugui/jsonnetunit/raw/master // // Roll-up all the tests in this directory // +local jnx = import 'jnx.jsonnet'; local test = import 'jsonnetunit/test.libsonnet'; -local jx = import 'jx.jsonnet'; -local jx_test = { name: 'jx.jsonnet test' } + test.suite({ +local jnx_test = { name: 'jnx.jsonnet test' } + test.suite({ testImport: { - // Force evaluation of imports in `jx.jsonnet` by using std.prune over + // Force evaluation of imports in `jnx.jsonnet` by using std.prune over // all fields. This will cause a failure if a file cannot be imported. - actual: std.prune(std.objectValuesAll(jx)), + actual: std.prune(std.objectValuesAll(jnx)), expect: [], // empty because all fields are hidden and thus pruned. }, }); [ - jx_test, + jnx_test, import 'array_test.jsonnet', import 'object_test.jsonnet', import 'op_test.jsonnet', diff --git a/lib/jx.jsonnet b/lib/jx.jsonnet index 03c531b..cb2a53d 100644 --- a/lib/jx.jsonnet +++ b/lib/jx.jsonnet @@ -1,9 +1,2 @@ -// Package jx is a top-level package to import the subpackages of the -// jx library. -{ - array:: import 'array.jsonnet', - object:: import 'object.jsonnet', - op:: import 'op.jsonnet', - string:: import 'string.jsonnet', - value:: import 'value.jsonnet', -} +// Kept for backwards compatibilty for now. Will be removed in the future. +import 'jnx.jsonnet' diff --git a/lib/object_test.jsonnet b/lib/object_test.jsonnet index 9316c68..8bd2bcb 100755 --- a/lib/object_test.jsonnet +++ b/lib/object_test.jsonnet @@ -1,4 +1,4 @@ -#!/usr/bin/env -S jx -J //github.com/yugui/jsonnetunit/raw/master +#!/usr/bin/env -S jnx -J //github.com/yugui/jsonnetunit/raw/master local test = import 'jsonnetunit/test.libsonnet'; local object = import 'object.jsonnet'; diff --git a/lib/op_test.jsonnet b/lib/op_test.jsonnet index fa27a1d..93cc87b 100755 --- a/lib/op_test.jsonnet +++ b/lib/op_test.jsonnet @@ -1,4 +1,4 @@ -#!/usr/bin/env -S jx -J //github.com/yugui/jsonnetunit/raw/master +#!/usr/bin/env -S jnx -J //github.com/yugui/jsonnetunit/raw/master local test = import 'jsonnetunit/test.libsonnet'; local op = import 'op.jsonnet'; diff --git a/lib/string_test.jsonnet b/lib/string_test.jsonnet index d23d826..570c45a 100755 --- a/lib/string_test.jsonnet +++ b/lib/string_test.jsonnet @@ -1,4 +1,4 @@ -#!/usr/bin/env -S jx -J //github.com/yugui/jsonnetunit/raw/master +#!/usr/bin/env -S jnx -J //github.com/yugui/jsonnetunit/raw/master local test = import 'jsonnetunit/test.libsonnet'; local string = import 'string.jsonnet'; diff --git a/lib/value_test.jsonnet b/lib/value_test.jsonnet index 51cb397..175ecbe 100755 --- a/lib/value_test.jsonnet +++ b/lib/value_test.jsonnet @@ -1,4 +1,4 @@ -#!/usr/bin/env -S jx -J //github.com/yugui/jsonnetunit/raw/master +#!/usr/bin/env -S jnx -J //github.com/yugui/jsonnetunit/raw/master local test = import 'jsonnetunit/test.libsonnet'; local value = import 'value.jsonnet'; From 3ecb20a552c1a92d5990547826374a26b9a34072 Mon Sep 17 00:00:00 2001 From: Cam Hutchison Date: Sat, 28 Dec 2024 21:38:01 +1100 Subject: [PATCH 3/3] lib: Add string.find and object.{map,asNamedArray} Add some functions to the jnx library: * string.find: Returns the index of a substring within a string or -1 if not present. * map: Apply a function to each [k, v] pair of an object and return an array of values of the result of that function. * asNamedArray: Converts an object to an array of values from that object where each object has a field added with the key of that field (defaults to the 'name' field). --- lib/object.jsonnet | 12 ++++++++++++ lib/object_test.jsonnet | 30 ++++++++++++++++++++++++++++++ lib/string.jsonnet | 13 +++++++++++++ lib/string_test.jsonnet | 13 +++++++++++++ 4 files changed, 68 insertions(+) diff --git a/lib/object.jsonnet b/lib/object.jsonnet index f3fcfb3..a5e7e15 100644 --- a/lib/object.jsonnet +++ b/lib/object.jsonnet @@ -46,6 +46,11 @@ // `[key, value]` pairs. kvHidden(obj):: [[k, obj[k]] for k in object.keysHidden(obj)], + // map applies a function taking a key and value to each non-hidden field + // of a the given object to return an array of values returned by each + // invocation of the given function. + map(fn, obj):: [fn(kv[0], kv[1]) for kv in object.kv(obj)], + // transform calls @fn(key, value) on each field of @obj passing it the key // and the value of each field and using the result of @fn to build the // result of transform. It can remove fields, rename fields or change the @@ -94,4 +99,11 @@ invertAll(obj):: __invert(obj, object.keysAll), local __invert(obj, selector) = array.accumulate([{ [obj[k]]+: [k] } for k in selector(obj)]), + + // asNamedArray transforms the object @obj into an array of the values in the + // object, adding a name field of the value's key if it does not have a name + // field. The default @nameField is 'name', but can be overridden. + asNamedArray(obj, nameField='name', valueField=null):: + local mkobj(v) = if valueField == null then v else { [valueField]: v }; + [{ [nameField]: kv[0] } + mkobj(kv[1]) for kv in object.kv(obj)], } diff --git a/lib/object_test.jsonnet b/lib/object_test.jsonnet index 8bd2bcb..9cbc1bf 100755 --- a/lib/object_test.jsonnet +++ b/lib/object_test.jsonnet @@ -88,6 +88,15 @@ test.suite({ expect: [], }, + testMap: { + actual: object.map(function(k, v) [k, v], { a: 1, b: 2 }), + expect: [['a', 1], ['b', 2]], + }, + testMapEmpty: { + actual: object.map(function(k, v) [k, v], {}), + expect: [], + }, + testMake: { actual: object.make([['a', 1], ['b', 2]]), expect: o, // only the visible fields are compared. @@ -160,4 +169,25 @@ test.suite({ actual: object.invertAll({ a: 'foo', b: 'bar', c:: 'bar', d:: 'baz' }), expect: { foo: ['a'], bar: ['b', 'c'], baz: ['d'] }, }, + + testAsNamedArray: { + actual: object.asNamedArray({ a: {}, b: {} }), + expect: [{ name: 'a' }, { name: 'b' }], + }, + testAsNamedArrayEmpty: { + actual: object.asNamedArray({}), + expect: [], + }, + testAsNamedArrayWithNameField: { + actual: object.asNamedArray({ a: {}, b: {} }, nameField='foo'), + expect: [{ foo: 'a' }, { foo: 'b' }], + }, + testAsNamedArrayPreserveName: { + actual: object.asNamedArray({ a: {}, b: { name: 'foo' } }), + expect: [{ name: 'a' }, { name: 'foo' }], + }, + testAsNamedArrayNonObject: { + actual: object.asNamedArray({ a: 'hello', b: 'world' }, valueField='value'), + expect: [{ name: 'a', value: 'hello' }, { name: 'b', value: 'world' }], + }, }) diff --git a/lib/string.jsonnet b/lib/string.jsonnet index faea6db..f13c09a 100644 --- a/lib/string.jsonnet +++ b/lib/string.jsonnet @@ -16,4 +16,17 @@ true else std.strReplace(str, substr, '') != str, + + find(str, char):: + local _find(str, char, pos) = + if std.length(str) == 0 then -1 + else if std.startsWith(str, char) then pos + else _find(str[1:], char, pos + 1); + if !std.isString(str) then + error ('string.find first param must be a string, got ' + std.type(str)) + else if !std.isString(char) then + error ('string.find secomd param must be a string, got ' + std.type(char)) + else if std.length(char) == 0 then -1 // empty string is not in string + else _find(str, char, 0), + } diff --git a/lib/string_test.jsonnet b/lib/string_test.jsonnet index 570c45a..00dcaf2 100755 --- a/lib/string_test.jsonnet +++ b/lib/string_test.jsonnet @@ -29,4 +29,17 @@ test.suite({ actual: string.contains('', ''), expect: true, }, + + testFind: { + actual: string.find('hello', 'e'), + expect: 1, + }, + testFindNotFound: { + actual: string.find('hello', 'x'), + expect: -1, + }, + testFullMatch: { + actual: string.find('hello', 'hello'), + expect: 0, + }, })