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..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) @@ -48,19 +47,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 +66,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 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.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 9316c68..9cbc1bf 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'; @@ -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/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.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 d23d826..00dcaf2 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'; @@ -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, + }, }) 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';