Skip to content

Commit

Permalink
Give jk transform a flag for reading from stdin
Browse files Browse the repository at this point in the history
The new flag --stdin-format instructs `jk transform` to read from
stdin (as well as any files that are given as arguments). The possible
values are `json` meaning expect JSON values, and `yaml` meaning
expect a YAML stream.

An alternative would be the conventional `-` denoting stdin; however,
you would still need to provide a format. Defaulting to YAML would
read a JSON value equally well, but (crucially) not multiple JSON
values.
  • Loading branch information
squaremo committed Jun 2, 2020
1 parent 676c9a5 commit bc053bc
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 49 deletions.
34 changes: 2 additions & 32 deletions std/cmd/generate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as std from '../index';
import { WriteOptions } from '../write';
import { splitPath, formatFromPath } from '../read';
import { ValidateFn } from './validate';
import { normaliseResult, formatError } from '../validation';

Expand Down Expand Up @@ -105,37 +106,6 @@ const nth = (n: number): string => {
return n + (s[mod(v - 20, 10)] || s[v] || s[0]);
};

function splitExtension(path: string): [string, string] {
const parts = path.split('.');
const ext = parts.pop()
// When there's no extension, either there will be a single part (no
// dots anywhere), or a path separator in the last part (a dot
// somewhere before the last path segment)
if (parts.length == 0 || ext.includes('/')) {
return [ext, '']
}
return [parts.join(''), ext]
}

function extension(path: string): string {
return splitExtension(path)[1]
}

function formatFromPath(path: string): std.Format {
switch (extension(path)) {
case 'yaml':
case 'yml':
return std.Format.YAML;
case 'json':
return std.Format.JSON;
case 'hcl':
case 'tf':
return std.Format.HCL;
default:
return std.Format.JSON;
}
}

const isString = (s: any): boolean => typeof s === 'string' || s instanceof String;

// represents a file spec that has its promise resolved, if necessary
Expand Down Expand Up @@ -218,7 +188,7 @@ function forceFormat(forced: OutputFormat, files: RealisedFile[]) {
file.format = outputFormatToFormat[forced];
break;
}
const [p, ext] = splitExtension(path);
const [p, ext] = splitPath(path);
if (ext !== '') {
file.path = [p, forced].join('.');
}
Expand Down
23 changes: 16 additions & 7 deletions std/cmd/transform.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { Format, Overwrite } from '../index';
import { Format, Overwrite, read, stdin } from '../index';
import * as host from '@jkcfg/std/internal/host'; // magic module
import * as param from '../param';
import { generate, File, GenerateParams, maybeSetFormat } from './generate';
import { valuesFormatFromPath } from '../read';
import { valuesFormatFromPath, valuesFormatFromExtension } from '../read';

type TransformFn = (value: any) => any | void;

const inputParams: GenerateParams = {
const generateParams: GenerateParams = {
stdout: param.Boolean('jk.transform.stdout', false),
overwrite: param.Boolean('jk.transform.overwrite', false) ? Overwrite.Write : Overwrite.Err,
};
maybeSetFormat(inputParams, param.String('jk.generate.format', undefined)); // NB jk.generate. param
maybeSetFormat(generateParams, param.String('jk.generate.format', undefined)); // NB jk.generate. param

// If we're told to overwrite, we need to be able to write to the
// files mentioned on the command-line; but not otherwise.
if (inputParams.overwrite == Overwrite.Write) {
inputParams.writeFile = host.write;
if (generateParams.overwrite == Overwrite.Write) {
generateParams.writeFile = host.write;
}

function transform(fn: TransformFn): void {
Expand All @@ -28,6 +28,15 @@ function transform(fn: TransformFn): void {

const inputFiles = param.Object('jk.transform.input', {});
const outputs = [];

const stdinFormat = param.String('jk.transform.stdin.format', '');
if (stdinFormat !== '') {
const format = valuesFormatFromExtension(stdinFormat);
const path = `stdin.${stdinFormat}`; // path is a stand-in
const value = read(stdin, { format }).then(v => v.map(transformOne));
outputs.push({ path, value, format });
}

for (const path of Object.keys(inputFiles)) {
const format = valuesFormatFromPath(path);
outputs.push(host.read(path, { format }).then((obj): File => {
Expand All @@ -44,7 +53,7 @@ function transform(fn: TransformFn): void {
}
}));
}
generate(Promise.all(outputs), inputParams);
generate(Promise.all(outputs), generateParams);
}

export default transform;
58 changes: 51 additions & 7 deletions std/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,47 @@ export interface ReadOptions {
module?: string;
}

// valuesFormatFromPath guesses, for a path, the format that will
// return all values in a file. In other words, it prefers YAML
// streams and concatenated JSON. You may need to treat the read value
// differently depending on the format you got here, since YAMLStream
// and JSONStream will both result in an array of values.
export function valuesFormatFromPath(path: string): Format {
const ext = path.split('.').pop();
// splitPath returns [all-but-extension, extension] for a path. If a
// path does not end with an extension, it will be an empty string.
export function splitPath(path: string): [string, string] {
const parts = path.split('.');
const ext = parts.pop();
// When there's no extension, either there will be a single part (no
// dots anywhere), or a path separator in the last part (a dot
// somewhere before the last path segment)
if (parts.length === 0 || ext.includes('/')) {
return [ext, ''];
}
return [parts.join(''), ext];
}

function extension(path: string): string {
return splitPath(path)[1];
}

// formatFromPath guesses, for a file path, the format in which to
// read the file. It will assume one value per file, so if you have
// files that may have multiple values (e.g., YAML streams), it's
// better to use `valuesFormatFromPath` and be prepared to get
// multiple values.
export function formatFromPath(path: string): Format {
switch (extension(path)) {
case 'yaml':
case 'yml':
return Format.YAML;
case 'json':
return Format.JSON;
case 'hcl':
case 'tf':
return Format.HCL;
default:
return Format.JSON;
}
}

// valuesFormatFromExtension returns the format implied by a
// particular file extension.
export function valuesFormatFromExtension(ext: string): Format {
switch (ext) {
case 'yaml':
case 'yml':
Expand All @@ -47,6 +81,16 @@ export function valuesFormatFromPath(path: string): Format {
}
}

// valuesFormatFromPath guesses, for a path, the format that will
// return all values in a file. In other words, it prefers YAML
// streams and concatenated JSON. You may need to treat the read value
// differently depending on the format you got here, since YAMLStream
// and JSONStream will both result in an array of values.
export function valuesFormatFromPath(path: string): Format {
const ext = extension(path);
return valuesFormatFromExtension(ext);
}

type ReadPath = string | typeof stdin;

// read requests the path and returns a promise that will be resolved
Expand Down
20 changes: 17 additions & 3 deletions transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,14 @@ const transformExamples = `
var transformOptions struct {
vmOptions
scriptOptions
stdout bool // print everything to stdout
overwrite bool // permit the overwriting of input files
format string // force the format of the output
// print everything to stdout
stdout bool
// permit the overwriting of input files
overwrite bool
// force the format of the output
format string
// read from stdin, expecting a stream of values in the format given
stdinFormat string
}

func init() {
Expand All @@ -38,6 +43,7 @@ func init() {
transformCmd.PersistentFlags().BoolVar(&transformOptions.stdout, "stdout", false, "print the resulting values to stdout")
transformCmd.PersistentFlags().BoolVar(&transformOptions.overwrite, "overwrite", false, "allow input file(s) to be overwritten by output file(s); otherwise, an error will be thrown")
transformCmd.PersistentFlags().StringVar(&transformOptions.format, "format", "", "force all values to this format")
transformCmd.PersistentFlags().StringVar(&transformOptions.stdinFormat, "stdin-format", "", "read values from stdin, assuming this format; implies --stdout")
jk.AddCommand(transformCmd)
}

Expand Down Expand Up @@ -68,6 +74,14 @@ func transform(cmd *cobra.Command, args []string) {
vm.parameters.Set("jk.transform.overwrite", transformOptions.overwrite)
setGenerateFormat(transformOptions.format, vm)

switch transformOptions.stdinFormat {
case "json", "yaml":
vm.parameters.Set("jk.transform.stdin.format", transformOptions.stdinFormat)
vm.parameters.Set("jk.transform.stdout", true)
default:
break
}

var module string
switch {
case transformOptions.inline:
Expand Down

0 comments on commit bc053bc

Please sign in to comment.