Path: blob/main/operations/river-jsonnet/manifest.jsonnet
4093 views
local utils = import './internal/utils.jsonnet'; // parseField parses a field name from a Jsonnet object and determines if it's // supposed to be a River attribute or block. local parseField(name) = ( local parts = std.split(name, ' '); local numParts = std.length(parts); if numParts == 1 then { type: 'attr', name: parts[0], index: 0, orig: name, } else if numParts > 1 && numParts <= 4 && parts[0] == 'block' then { type: 'block', index: std.parseInt(parts[1]), name: parts[2], label: if numParts == 4 then parts[3] else '', orig: name, } else ( error 'invalid field name %s' % name ) ); // isRiverExpr returns true if value was constructed with river.expr(). local isRiverExpr(value) = std.isObject(value) && std.length(value) == 1 && utils.exprMarker in value; // linePadding returns number of spaces to indent a line with given a specific // indentation level. local linePadding(indent) = std.repeat(' ', indent); local manifester(indent=0) = { local padding = linePadding(indent), local this = $, // body manifests a River body to text, manifesting both attributes and // blocks. body(value): ( // First we need to look at each public field of the value and parse it as // an attribute or block. local parsedFields = std.sort( std.map(function(field) parseField(field), std.objectFields(value)), function(field) field.index, ); // Now we can accumulate all of the fields into a single string. Each field // will be separated by exactly one newline. std.foldl(function(acc, field) ( // Determine the name to use for the field. local name = ( if field.type == 'attr' then field.name else if field.type == 'block' && field.label == '' then field.name else '%s "%s"' % [field.name, field.label] ); if field.type == 'attr' then ( // Manifest the value to text. local attr_value = this.value(value[field.orig]); // Attributes are printed as <attribute_name> = <rendered value> acc + padding + ('%s = %s' % [name, attr_value]) + '\n' ) else if field.type == 'block' && std.isObject(value[field.orig]) then ( local block_header = '%s {\n' % name; local block_body = manifester(indent + 1).body(value[field.orig]); // The block body ends in a newline, so the block trailer must start // with line padding. local block_trailer = padding + '}'; acc + padding + block_header + block_body + block_trailer + '\n' ) else if field.type == 'block' && std.isArray(value[field.orig]) then ( // List of blocks. local block_header = '%s {\n' % name; local block_trailer = padding + '}'; std.foldl(function(acc, block) ( local block_body = manifester(indent + 1).body(block); acc + padding + block_header + block_body + block_trailer + '\n' ), value[field.orig], acc) ) else ( error 'invalid field type' // This should never happen ) ), parsedFields, '') ), // value manifests a River value to text. value(value): ( if value == null then ( 'null' ) else if isRiverExpr(value) then ( local lines = std.split(value[utils.exprMarker], '\n'); // When injecting literals, each line after the first should have the // current padding appended to it. std.join('\n', std.mapWithIndex(function(index, line) ( if index > 0 then padding + line else line ), lines)) ) else if std.isString(value) then ( '"%s"' % value ) else if std.isBoolean(value) then ( std.toString(value) ) else if std.isNumber(value) then ( std.toString(value) ) else if std.isArray(value) then ( // To manifest an array, we convert all of the elements into text and // separate them by commas. // First pair the elements with their index and value so we know when we // need commas. local elements = std.mapWithIndex(function(index, elem) { index: index, value: elem, }, value); // Finally, construct the body to insert in between the square brackets. local body = std.foldl(function(acc, elem) ( // We need to put a comma at the end if we're not the last element. local suffix = if elem.index + 1 < std.length(elements) then ', ' else ''; local text = this.value(elem.value); acc + text + suffix ), elements, ''); '[%s]' % body ) else if std.isObject(value) then ( // To manifest an object, we convert all of the fields into text and add // a comma and a newline after each. Unlike arrays, this always has a // trailing comma so we don't need to track the index. // Create an inner manifester with a higher indentation level for // printing the fields. local next = manifester(indent + 1); local body = std.foldl(function(acc, field) ( // TODO(rfratto): detect whether it's necessary to wrap the field name // in quotes. This is only necessary if it's not a valid identifier. local text = '"%s" = %s,\n' % [field, next.value(value[field])]; // Make sure the attribute itself is given the padding from the next // indentation level, not the current one. acc + linePadding(indent + 1) + text ), std.objectFields(value), ''); // Last check at the end: if the body is empty, we can return an empty object. if body == '' then '{}' else ( '{\n' + body + padding + '}' ) ) else error 'unsupported value type %s' % std.type(value) ), }; { // manifestRiver returns a pretty-printed River file from the Jsonnet value. // value must be an object. manifestRiver(value):: ( assert std.isObject(value) : 'manifestRiver must be called with object'; manifester().body(value) ), manifestRiverValue(value):: ( manifester().value(value) ), }