feat!: some section parse issue
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
|
||||
CREATE SCHEMA hemar;
|
||||
|
||||
-- Define the parse_text_with_hectic function that uses hectic library
|
||||
-- Define the template rendering functions using hectic library
|
||||
-- Expected usage:
|
||||
-- ```sql
|
||||
-- SELECT "hemar"."render"(
|
||||
@@ -20,11 +20,12 @@ CREATE SCHEMA hemar;
|
||||
-- $hemar$
|
||||
-- );
|
||||
-- ```
|
||||
--CREATE FUNCTION "hemar"."render"("declare" jsonb, "template" text)
|
||||
--RETURNS text
|
||||
--LANGUAGE C STRICT
|
||||
--AS 'hemar', 'pg_render';
|
||||
CREATE FUNCTION "hemar"."render"("declare" jsonb, "template" text)
|
||||
RETURNS text
|
||||
LANGUAGE C STRICT
|
||||
AS 'hemar', 'pg_render';
|
||||
|
||||
-- Parse function returns the structure of a template for debugging
|
||||
CREATE FUNCTION "hemar"."parse"("template" text)
|
||||
RETURNS text
|
||||
LANGUAGE C STRICT
|
||||
|
||||
@@ -11,6 +11,23 @@
|
||||
#include "utils/elog.h"
|
||||
#include "utils/memutils.h"
|
||||
#include "mb/pg_wchar.h"
|
||||
#include "fmgr.h"
|
||||
#include "utils/builtins.h"
|
||||
#include "lib/stringinfo.h"
|
||||
#include "utils/jsonb.h"
|
||||
#include "catalog/pg_type.h"
|
||||
#include "executor/spi.h"
|
||||
#include "funcapi.h"
|
||||
|
||||
/* Forward declarations */
|
||||
static char *get_jsonb_path_value(Datum jsonb_context, const char *path, bool *found);
|
||||
static Datum get_jsonb_array(Datum jsonb_context, const char *path, bool *found);
|
||||
static int get_jsonb_array_length(Datum jsonb_array);
|
||||
static Datum get_jsonb_array_element(Datum jsonb_array, int index);
|
||||
static Datum create_iterator_context(Datum parent_context, const char *iterator_name, Datum item_context);
|
||||
static Datum get_jsonb_include_template(Datum jsonb_context, const char *key, bool *found);
|
||||
static void get_include_data(Datum include_data, char **template_out, Datum *context_out);
|
||||
static void template_node_to_string(TemplateNode *node, StringInfo result, int indent);
|
||||
|
||||
/* Helper function to skip whitespace */
|
||||
static const char *
|
||||
@@ -515,6 +532,8 @@ template_parse(MemoryContext context, const char **s, const TemplateConfig *conf
|
||||
const char *start;
|
||||
TemplateNode *root, *current, *tag_node;
|
||||
bool current_node_filled = false;
|
||||
const char *tag_prefix;
|
||||
size_t text_len;
|
||||
|
||||
if (!template_validate_config(config, error_code))
|
||||
{
|
||||
@@ -550,14 +569,14 @@ template_parse(MemoryContext context, const char **s, const TemplateConfig *conf
|
||||
*current->value = init_template_value(TEMPLATE_NODE_TEXT);
|
||||
}
|
||||
|
||||
size_t text_len = *s - start;
|
||||
text_len = *s - start;
|
||||
current->value->text.content = MemoryContextStrdup(context, pnstrdup(start, text_len));
|
||||
current_node_filled = true;
|
||||
}
|
||||
|
||||
/* Parse the tag */
|
||||
tag_node = NULL;
|
||||
const char *tag_prefix = *s + strlen(config->Syntax.Braces.open);
|
||||
tag_prefix = *s + strlen(config->Syntax.Braces.open);
|
||||
tag_prefix = skip_whitespace(tag_prefix);
|
||||
|
||||
/* Determine tag type by prefix */
|
||||
@@ -626,7 +645,7 @@ template_parse(MemoryContext context, const char **s, const TemplateConfig *conf
|
||||
*current->value = init_template_value(TEMPLATE_NODE_TEXT);
|
||||
}
|
||||
|
||||
size_t text_len = *s - start;
|
||||
text_len = *s - start;
|
||||
current->value->text.content = MemoryContextStrdup(context, pnstrdup(start, text_len));
|
||||
current_node_filled = true;
|
||||
}
|
||||
@@ -705,3 +724,936 @@ template_free_node(TemplateNode *node)
|
||||
current = next;
|
||||
}
|
||||
}
|
||||
|
||||
/* Render a template with the given context */
|
||||
static char *
|
||||
template_render(MemoryContext context, TemplateNode *node, Datum jsonb_context, bool *error)
|
||||
{
|
||||
StringInfoData result;
|
||||
TemplateNode *current;
|
||||
|
||||
if (!node || !context || error == NULL)
|
||||
{
|
||||
if (error)
|
||||
*error = true;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
*error = false;
|
||||
initStringInfo(&result);
|
||||
current = node;
|
||||
|
||||
while (current)
|
||||
{
|
||||
switch (current->type)
|
||||
{
|
||||
case TEMPLATE_NODE_TEXT:
|
||||
if (current->value->text.content)
|
||||
appendStringInfoString(&result, current->value->text.content);
|
||||
break;
|
||||
|
||||
case TEMPLATE_NODE_INTERPOLATE:
|
||||
{
|
||||
char *value = NULL;
|
||||
bool found = false;
|
||||
|
||||
if (current->value->interpolate.key)
|
||||
{
|
||||
/* Extract value from JSONB context */
|
||||
value = get_jsonb_path_value(jsonb_context, current->value->interpolate.key, &found);
|
||||
|
||||
if (found && value)
|
||||
{
|
||||
appendStringInfoString(&result, value);
|
||||
pfree(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case TEMPLATE_NODE_SECTION:
|
||||
{
|
||||
/* Handle sections (loops) */
|
||||
char *collection_path = current->value->section.collection;
|
||||
Datum array_value;
|
||||
bool found = false;
|
||||
int array_length;
|
||||
int i;
|
||||
|
||||
if (collection_path && current->value->section.body)
|
||||
{
|
||||
/* Get array from context */
|
||||
array_value = get_jsonb_array(jsonb_context, collection_path, &found);
|
||||
|
||||
if (found)
|
||||
{
|
||||
array_length = get_jsonb_array_length(array_value);
|
||||
|
||||
/* Handle empty arrays gracefully */
|
||||
if (array_length <= 0)
|
||||
break;
|
||||
|
||||
for (i = 0; i < array_length; i++)
|
||||
{
|
||||
Datum item_context = get_jsonb_array_element(array_value, i);
|
||||
Datum merged_context;
|
||||
char *item_result;
|
||||
bool item_error = false;
|
||||
|
||||
if (item_context == (Datum) 0)
|
||||
{
|
||||
/* Create an empty object for null array elements */
|
||||
JsonbParseState *parse_state = NULL;
|
||||
JsonbValue *empty_obj;
|
||||
|
||||
pushJsonbValue(&parse_state, WJB_BEGIN_OBJECT, NULL);
|
||||
empty_obj = pushJsonbValue(&parse_state, WJB_END_OBJECT, NULL);
|
||||
item_context = PointerGetDatum(JsonbValueToJsonb(empty_obj));
|
||||
}
|
||||
|
||||
/* Create context with iterator variable */
|
||||
merged_context = create_iterator_context(jsonb_context, current->value->section.iterator, item_context);
|
||||
|
||||
if (merged_context == (Datum) 0)
|
||||
continue;
|
||||
|
||||
/* Render section body with new context */
|
||||
item_result = template_render(context, current->value->section.body, merged_context, &item_error);
|
||||
|
||||
if (!item_error && item_result)
|
||||
{
|
||||
appendStringInfoString(&result, item_result);
|
||||
pfree(item_result);
|
||||
}
|
||||
else if (item_error)
|
||||
{
|
||||
*error = true;
|
||||
return result.data;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case TEMPLATE_NODE_INCLUDE:
|
||||
{
|
||||
/* Handle includes */
|
||||
char *template_key = current->value->include.key;
|
||||
Datum include_data;
|
||||
bool found = false;
|
||||
|
||||
if (template_key)
|
||||
{
|
||||
/* Find include template in context */
|
||||
include_data = get_jsonb_include_template(jsonb_context, template_key, &found);
|
||||
|
||||
if (found)
|
||||
{
|
||||
char *include_template = NULL;
|
||||
Datum include_context = (Datum) 0;
|
||||
char *include_result;
|
||||
bool include_error = false;
|
||||
|
||||
/* Extract template and context */
|
||||
get_include_data(include_data, &include_template, &include_context);
|
||||
|
||||
/* Parse and render included template */
|
||||
if (include_template)
|
||||
{
|
||||
TemplateConfig config = template_default_config(context);
|
||||
TemplateErrorCode error_code;
|
||||
const char *template_str = include_template;
|
||||
TemplateNode *include_node = template_parse(context, &template_str, &config, false, &error_code);
|
||||
|
||||
if (include_node && error_code == TEMPLATE_ERROR_NONE)
|
||||
{
|
||||
include_result = template_render(context, include_node,
|
||||
include_context != (Datum) 0 ? include_context : jsonb_context,
|
||||
&include_error);
|
||||
|
||||
if (!include_error && include_result)
|
||||
{
|
||||
appendStringInfoString(&result, include_result);
|
||||
pfree(include_result);
|
||||
}
|
||||
else
|
||||
{
|
||||
*error = true;
|
||||
}
|
||||
|
||||
template_free_node(include_node);
|
||||
}
|
||||
else
|
||||
{
|
||||
*error = true;
|
||||
}
|
||||
|
||||
pfree(include_template);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case TEMPLATE_NODE_EXECUTE:
|
||||
/* Execute is not implemented in this version */
|
||||
break;
|
||||
|
||||
default:
|
||||
/* Unknown node type */
|
||||
break;
|
||||
}
|
||||
|
||||
if (*error)
|
||||
break;
|
||||
|
||||
current = current->next;
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
|
||||
/* Helper functions for JSONB handling */
|
||||
static char *
|
||||
get_jsonb_path_value(Datum jsonb_context, const char *path, bool *found)
|
||||
{
|
||||
Jsonb *jb = (Jsonb *) DatumGetPointer(jsonb_context);
|
||||
JsonbValue *jbv_result;
|
||||
JsonbValue key;
|
||||
JsonbIterator *it;
|
||||
JsonbIteratorToken token;
|
||||
char *result = NULL;
|
||||
|
||||
*found = false;
|
||||
|
||||
if (!jb || !path)
|
||||
return NULL;
|
||||
|
||||
/* Handle simple top-level key */
|
||||
if (strchr(path, '.') == NULL && strchr(path, '[') == NULL)
|
||||
{
|
||||
key.type = jbvString;
|
||||
key.val.string.val = (char *) path;
|
||||
key.val.string.len = strlen(path);
|
||||
|
||||
jbv_result = findJsonbValueFromContainer(&jb->root, JB_FOBJECT, &key);
|
||||
|
||||
if (jbv_result)
|
||||
{
|
||||
*found = true;
|
||||
|
||||
if (jbv_result->type == jbvString)
|
||||
{
|
||||
result = pnstrdup(jbv_result->val.string.val, jbv_result->val.string.len);
|
||||
}
|
||||
else if (jbv_result->type == jbvNumeric)
|
||||
{
|
||||
Numeric num = jbv_result->val.numeric;
|
||||
result = DatumGetCString(DirectFunctionCall1(numeric_out, NumericGetDatum(num)));
|
||||
}
|
||||
else if (jbv_result->type == jbvBool)
|
||||
{
|
||||
result = pstrdup(jbv_result->val.boolean ? "true" : "false");
|
||||
}
|
||||
else if (jbv_result->type == jbvNull)
|
||||
{
|
||||
result = pstrdup("");
|
||||
}
|
||||
else if (jbv_result->type == jbvBinary)
|
||||
{
|
||||
/* Convert binary type to string representation */
|
||||
StringInfoData buf;
|
||||
JsonbValue v;
|
||||
|
||||
initStringInfo(&buf);
|
||||
|
||||
it = JsonbIteratorInit((JsonbContainer *)&jbv_result->val.binary);
|
||||
|
||||
while ((token = JsonbIteratorNext(&it, &v, false)) != WJB_DONE)
|
||||
{
|
||||
if (token == WJB_VALUE)
|
||||
{
|
||||
if (v.type == jbvString)
|
||||
{
|
||||
appendBinaryStringInfo(&buf, v.val.string.val, v.val.string.len);
|
||||
}
|
||||
else if (v.type == jbvNumeric)
|
||||
{
|
||||
Numeric num = v.val.numeric;
|
||||
char *numstr = DatumGetCString(DirectFunctionCall1(numeric_out, NumericGetDatum(num)));
|
||||
appendStringInfoString(&buf, numstr);
|
||||
pfree(numstr);
|
||||
}
|
||||
else if (v.type == jbvBool)
|
||||
{
|
||||
appendStringInfoString(&buf, v.val.boolean ? "true" : "false");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = buf.data;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Handle nested paths using a JSON path expression */
|
||||
/* This is a simplified implementation and would need to be expanded for a full solution */
|
||||
char *current_path = pstrdup(path);
|
||||
char *token;
|
||||
char *saveptr;
|
||||
JsonbValue *current_jbv;
|
||||
|
||||
token = strtok_r(current_path, ".", &saveptr);
|
||||
while (token != NULL)
|
||||
{
|
||||
key.type = jbvString;
|
||||
key.val.string.val = token;
|
||||
key.val.string.len = strlen(token);
|
||||
|
||||
current_jbv = findJsonbValueFromContainer(&jb->root, JB_FOBJECT, &key);
|
||||
|
||||
if (!current_jbv)
|
||||
{
|
||||
pfree(current_path);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
token = strtok_r(NULL, ".", &saveptr);
|
||||
|
||||
/* If there are more path segments, current value must be an object */
|
||||
if (token != NULL)
|
||||
{
|
||||
if (current_jbv->type != jbvBinary)
|
||||
{
|
||||
pfree(current_path);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
jb = (Jsonb *) DatumGetPointer(JsonbValueToJsonb(current_jbv));
|
||||
}
|
||||
else
|
||||
{
|
||||
/* We found the value */
|
||||
*found = true;
|
||||
|
||||
if (current_jbv->type == jbvString)
|
||||
{
|
||||
result = pnstrdup(current_jbv->val.string.val, current_jbv->val.string.len);
|
||||
}
|
||||
else if (current_jbv->type == jbvNumeric)
|
||||
{
|
||||
Numeric num = current_jbv->val.numeric;
|
||||
result = DatumGetCString(DirectFunctionCall1(numeric_out, NumericGetDatum(num)));
|
||||
}
|
||||
else if (current_jbv->type == jbvBool)
|
||||
{
|
||||
result = pstrdup(current_jbv->val.boolean ? "true" : "false");
|
||||
}
|
||||
else if (current_jbv->type == jbvNull)
|
||||
{
|
||||
result = pstrdup("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pfree(current_path);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static Datum
|
||||
get_jsonb_array(Datum jsonb_context, const char *path, bool *found)
|
||||
{
|
||||
Jsonb *jb = (Jsonb *) DatumGetPointer(jsonb_context);
|
||||
JsonbValue *jbv_result;
|
||||
JsonbValue key;
|
||||
Datum result = (Datum) 0;
|
||||
|
||||
*found = false;
|
||||
|
||||
if (!jb || !path)
|
||||
return result;
|
||||
|
||||
/* Handle simple top-level key */
|
||||
if (strchr(path, '.') == NULL)
|
||||
{
|
||||
key.type = jbvString;
|
||||
key.val.string.val = (char *) path;
|
||||
key.val.string.len = strlen(path);
|
||||
|
||||
jbv_result = findJsonbValueFromContainer(&jb->root, JB_FOBJECT, &key);
|
||||
|
||||
if (jbv_result && jbv_result->type == jbvBinary)
|
||||
{
|
||||
JsonbIterator *it;
|
||||
JsonbValue v;
|
||||
JsonbIteratorToken token;
|
||||
|
||||
it = JsonbIteratorInit((JsonbContainer *)&jbv_result->val.binary);
|
||||
token = JsonbIteratorNext(&it, &v, false);
|
||||
|
||||
if (token == WJB_BEGIN_ARRAY)
|
||||
{
|
||||
*found = true;
|
||||
result = PointerGetDatum(JsonbValueToJsonb(jbv_result));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Handle nested paths */
|
||||
/* This is a simplified implementation */
|
||||
char *current_path = pstrdup(path);
|
||||
char *token;
|
||||
char *saveptr;
|
||||
JsonbValue *current_jbv;
|
||||
|
||||
token = strtok_r(current_path, ".", &saveptr);
|
||||
while (token != NULL)
|
||||
{
|
||||
key.type = jbvString;
|
||||
key.val.string.val = token;
|
||||
key.val.string.len = strlen(token);
|
||||
|
||||
current_jbv = findJsonbValueFromContainer(&jb->root, JB_FOBJECT, &key);
|
||||
|
||||
if (!current_jbv)
|
||||
{
|
||||
pfree(current_path);
|
||||
return result;
|
||||
}
|
||||
|
||||
token = strtok_r(NULL, ".", &saveptr);
|
||||
|
||||
/* If there are more path segments, current value must be an object */
|
||||
if (token != NULL)
|
||||
{
|
||||
if (current_jbv->type != jbvBinary)
|
||||
{
|
||||
pfree(current_path);
|
||||
return result;
|
||||
}
|
||||
|
||||
jb = (Jsonb *) DatumGetPointer(JsonbValueToJsonb(current_jbv));
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Check if the final value is an array */
|
||||
if (current_jbv->type == jbvBinary)
|
||||
{
|
||||
JsonbIterator *it;
|
||||
JsonbValue v;
|
||||
JsonbIteratorToken token;
|
||||
|
||||
it = JsonbIteratorInit((JsonbContainer *)¤t_jbv->val.binary);
|
||||
token = JsonbIteratorNext(&it, &v, false);
|
||||
|
||||
if (token == WJB_BEGIN_ARRAY)
|
||||
{
|
||||
*found = true;
|
||||
result = PointerGetDatum(JsonbValueToJsonb(current_jbv));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pfree(current_path);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static int
|
||||
get_jsonb_array_length(Datum jsonb_array)
|
||||
{
|
||||
Jsonb *jb = (Jsonb *) DatumGetPointer(jsonb_array);
|
||||
JsonbIterator *it;
|
||||
JsonbValue v;
|
||||
JsonbIteratorToken token;
|
||||
int count = 0;
|
||||
|
||||
if (!jb)
|
||||
return 0;
|
||||
|
||||
it = JsonbIteratorInit((JsonbContainer *)&jb->root);
|
||||
|
||||
/* Skip the WJB_BEGIN_ARRAY token */
|
||||
token = JsonbIteratorNext(&it, &v, false);
|
||||
|
||||
if (token != WJB_BEGIN_ARRAY)
|
||||
return 0;
|
||||
|
||||
/* Count array elements */
|
||||
while ((token = JsonbIteratorNext(&it, &v, false)) != WJB_DONE)
|
||||
{
|
||||
if (token == WJB_ELEM)
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
static Datum
|
||||
get_jsonb_array_element(Datum jsonb_array, int index)
|
||||
{
|
||||
Jsonb *jb = (Jsonb *) DatumGetPointer(jsonb_array);
|
||||
JsonbIterator *it;
|
||||
JsonbValue v;
|
||||
JsonbIteratorToken token;
|
||||
int current_index = 0;
|
||||
Datum result = (Datum) 0;
|
||||
JsonbParseState *parse_state = NULL;
|
||||
JsonbValue *jbv_result;
|
||||
|
||||
if (!jb || index < 0)
|
||||
return result;
|
||||
|
||||
it = JsonbIteratorInit((JsonbContainer *)&jb->root);
|
||||
|
||||
/* Skip the WJB_BEGIN_ARRAY token */
|
||||
token = JsonbIteratorNext(&it, &v, false);
|
||||
|
||||
if (token != WJB_BEGIN_ARRAY)
|
||||
return result;
|
||||
|
||||
/* Find the element at the specified index */
|
||||
while ((token = JsonbIteratorNext(&it, &v, false)) != WJB_DONE)
|
||||
{
|
||||
if (token == WJB_ELEM)
|
||||
{
|
||||
if (current_index == index)
|
||||
{
|
||||
/* Found the element */
|
||||
if (v.type == jbvBinary)
|
||||
{
|
||||
/* For binary values, just convert directly */
|
||||
result = PointerGetDatum(JsonbValueToJsonb(&v));
|
||||
}
|
||||
else
|
||||
{
|
||||
/* For scalar values, we need to create a proper JSON value */
|
||||
pushJsonbValue(&parse_state, WJB_BEGIN_OBJECT, NULL);
|
||||
|
||||
/* Add a dummy key "value" */
|
||||
JsonbValue key;
|
||||
key.type = jbvString;
|
||||
key.val.string.val = "value";
|
||||
key.val.string.len = 5;
|
||||
|
||||
pushJsonbValue(&parse_state, WJB_KEY, &key);
|
||||
|
||||
/* Add the value */
|
||||
pushJsonbValue(&parse_state, WJB_VALUE, &v);
|
||||
|
||||
/* Finish the object */
|
||||
jbv_result = pushJsonbValue(&parse_state, WJB_END_OBJECT, NULL);
|
||||
|
||||
/* Convert to Jsonb */
|
||||
result = PointerGetDatum(JsonbValueToJsonb(jbv_result));
|
||||
}
|
||||
break;
|
||||
}
|
||||
current_index++;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static Datum
|
||||
create_iterator_context(Datum parent_context, const char *iterator_name, Datum item_context)
|
||||
{
|
||||
Jsonb *parent_jb = (Jsonb *) DatumGetPointer(parent_context);
|
||||
Jsonb *item_jb = (Jsonb *) DatumGetPointer(item_context);
|
||||
JsonbParseState *parse_state = NULL;
|
||||
JsonbValue *result;
|
||||
JsonbIterator *it;
|
||||
JsonbValue v;
|
||||
JsonbIteratorToken token;
|
||||
JsonbValue key;
|
||||
JsonbValue val;
|
||||
bool is_scalar = false;
|
||||
|
||||
if (!parent_jb || !item_jb || !iterator_name)
|
||||
return parent_context;
|
||||
|
||||
/* Start with a copy of the parent context */
|
||||
pushJsonbValue(&parse_state, WJB_BEGIN_OBJECT, NULL);
|
||||
|
||||
/* Copy all fields from parent context */
|
||||
it = JsonbIteratorInit((JsonbContainer *)&parent_jb->root);
|
||||
|
||||
/* Skip the WJB_BEGIN_OBJECT token */
|
||||
token = JsonbIteratorNext(&it, &v, false);
|
||||
|
||||
if (token != WJB_BEGIN_OBJECT)
|
||||
return parent_context;
|
||||
|
||||
/* Copy all key-value pairs from parent */
|
||||
while ((token = JsonbIteratorNext(&it, &v, false)) != WJB_DONE)
|
||||
{
|
||||
if (token == WJB_KEY)
|
||||
{
|
||||
/* Copy the key */
|
||||
key.type = jbvString;
|
||||
key.val.string.val = pnstrdup(v.val.string.val, v.val.string.len);
|
||||
key.val.string.len = v.val.string.len;
|
||||
|
||||
pushJsonbValue(&parse_state, WJB_KEY, &key);
|
||||
|
||||
/* Get and copy the value */
|
||||
token = JsonbIteratorNext(&it, &val, false);
|
||||
pushJsonbValue(&parse_state, WJB_VALUE, &val);
|
||||
|
||||
pfree(key.val.string.val);
|
||||
}
|
||||
}
|
||||
|
||||
/* Add the iterator variable */
|
||||
key.type = jbvString;
|
||||
key.val.string.val = (char *) iterator_name;
|
||||
key.val.string.len = strlen(iterator_name);
|
||||
|
||||
pushJsonbValue(&parse_state, WJB_KEY, &key);
|
||||
|
||||
/* Check if the item is a scalar value wrapped in an object */
|
||||
it = JsonbIteratorInit((JsonbContainer *)&item_jb->root);
|
||||
token = JsonbIteratorNext(&it, &v, false);
|
||||
|
||||
if (token == WJB_BEGIN_OBJECT)
|
||||
{
|
||||
/* Check if it's our special scalar wrapper with "value" key */
|
||||
token = JsonbIteratorNext(&it, &v, false);
|
||||
if (token == WJB_KEY && v.type == jbvString &&
|
||||
v.val.string.len == 5 && strncmp(v.val.string.val, "value", 5) == 0)
|
||||
{
|
||||
/* Get the scalar value */
|
||||
token = JsonbIteratorNext(&it, &v, false);
|
||||
if (token == WJB_VALUE)
|
||||
{
|
||||
is_scalar = true;
|
||||
pushJsonbValue(&parse_state, WJB_VALUE, &v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* If not a scalar, use the whole item as is */
|
||||
if (!is_scalar)
|
||||
{
|
||||
it = JsonbIteratorInit((JsonbContainer *)&item_jb->root);
|
||||
token = JsonbIteratorNext(&it, &v, false);
|
||||
pushJsonbValue(&parse_state, WJB_VALUE, &v);
|
||||
}
|
||||
|
||||
/* Finalize the new context object */
|
||||
result = pushJsonbValue(&parse_state, WJB_END_OBJECT, NULL);
|
||||
|
||||
return PointerGetDatum(JsonbValueToJsonb(result));
|
||||
}
|
||||
|
||||
static Datum
|
||||
get_jsonb_include_template(Datum jsonb_context, const char *key, bool *found)
|
||||
{
|
||||
Jsonb *jb = (Jsonb *) DatumGetPointer(jsonb_context);
|
||||
JsonbValue *jbv_result;
|
||||
JsonbValue search_key;
|
||||
Datum result = (Datum) 0;
|
||||
char *include_key;
|
||||
|
||||
*found = false;
|
||||
|
||||
if (!jb || !key)
|
||||
return result;
|
||||
|
||||
/* Construct the include key */
|
||||
include_key = psprintf("include %s", key);
|
||||
|
||||
/* Look for the include template in the context */
|
||||
search_key.type = jbvString;
|
||||
search_key.val.string.val = include_key;
|
||||
search_key.val.string.len = strlen(include_key);
|
||||
|
||||
jbv_result = findJsonbValueFromContainer(&jb->root, JB_FOBJECT, &search_key);
|
||||
|
||||
if (jbv_result && jbv_result->type == jbvBinary)
|
||||
{
|
||||
*found = true;
|
||||
result = PointerGetDatum(JsonbValueToJsonb(jbv_result));
|
||||
}
|
||||
|
||||
pfree(include_key);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void
|
||||
get_include_data(Datum include_data, char **template_out, Datum *context_out)
|
||||
{
|
||||
Jsonb *jb = (Jsonb *) DatumGetPointer(include_data);
|
||||
JsonbIterator *it;
|
||||
JsonbValue v;
|
||||
JsonbIteratorToken token;
|
||||
JsonbValue template_key, context_key;
|
||||
JsonbValue *template_jbv, *context_jbv;
|
||||
|
||||
*template_out = NULL;
|
||||
*context_out = (Datum) 0;
|
||||
|
||||
if (!jb)
|
||||
return;
|
||||
|
||||
/* Check if it's an array of include objects */
|
||||
it = JsonbIteratorInit((JsonbContainer *)&jb->root);
|
||||
token = JsonbIteratorNext(&it, &v, false);
|
||||
|
||||
if (token != WJB_BEGIN_ARRAY)
|
||||
return;
|
||||
|
||||
/* Get the first element (we only support one include for now) */
|
||||
token = JsonbIteratorNext(&it, &v, false);
|
||||
|
||||
if (token != WJB_ELEM || v.type != jbvBinary)
|
||||
return;
|
||||
|
||||
/* Get the include object */
|
||||
Jsonb *include_obj = (Jsonb *) DatumGetPointer(JsonbValueToJsonb(&v));
|
||||
|
||||
/* Look for template and context keys */
|
||||
template_key.type = jbvString;
|
||||
template_key.val.string.val = "template";
|
||||
template_key.val.string.len = strlen("template");
|
||||
|
||||
context_key.type = jbvString;
|
||||
context_key.val.string.val = "context";
|
||||
context_key.val.string.len = strlen("context");
|
||||
|
||||
template_jbv = findJsonbValueFromContainer(&include_obj->root, JB_FOBJECT, &template_key);
|
||||
|
||||
/* Extract template string */
|
||||
if (template_jbv && template_jbv->type == jbvString)
|
||||
{
|
||||
*template_out = pnstrdup(template_jbv->val.string.val, template_jbv->val.string.len);
|
||||
}
|
||||
|
||||
context_jbv = findJsonbValueFromContainer(&include_obj->root, JB_FOBJECT, &context_key);
|
||||
|
||||
/* Extract context object */
|
||||
if (context_jbv && context_jbv->type == jbvBinary)
|
||||
{
|
||||
*context_out = PointerGetDatum(JsonbValueToJsonb(context_jbv));
|
||||
}
|
||||
}
|
||||
|
||||
/* PostgreSQL function wrappers */
|
||||
PG_MODULE_MAGIC;
|
||||
|
||||
PG_FUNCTION_INFO_V1(pg_template_parse);
|
||||
Datum
|
||||
pg_template_parse(PG_FUNCTION_ARGS)
|
||||
{
|
||||
text *template_text = PG_GETARG_TEXT_PP(0);
|
||||
char *template_str = text_to_cstring(template_text);
|
||||
const char *template_ptr = template_str;
|
||||
MemoryContext old_context;
|
||||
MemoryContext parse_context;
|
||||
TemplateConfig config;
|
||||
TemplateNode *root = NULL;
|
||||
TemplateErrorCode error_code = TEMPLATE_ERROR_NONE;
|
||||
text *result_text = NULL;
|
||||
StringInfoData result;
|
||||
|
||||
/* Create a memory context for parsing */
|
||||
parse_context = AllocSetContextCreate(CurrentMemoryContext,
|
||||
"Template Parse Context",
|
||||
ALLOCSET_DEFAULT_SIZES);
|
||||
|
||||
/* Switch to the new context for parsing */
|
||||
old_context = MemoryContextSwitchTo(parse_context);
|
||||
|
||||
/* Initialize default config */
|
||||
config = template_default_config(parse_context);
|
||||
|
||||
PG_TRY();
|
||||
{
|
||||
/* Parse the template */
|
||||
root = template_parse(parse_context, &template_ptr, &config, false, &error_code);
|
||||
|
||||
/* Check for parsing errors */
|
||||
if (error_code != TEMPLATE_ERROR_NONE || !root)
|
||||
{
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_SYNTAX_ERROR),
|
||||
errmsg("Template parsing error: %s", template_error_to_string(error_code))));
|
||||
}
|
||||
|
||||
/* Convert the parsed template to a string representation for debugging */
|
||||
initStringInfo(&result);
|
||||
appendStringInfo(&result, "Template parsed successfully. Structure:\n");
|
||||
template_node_to_string(root, &result, 0);
|
||||
|
||||
/* Switch back to the original memory context */
|
||||
MemoryContextSwitchTo(old_context);
|
||||
|
||||
/* Return the result */
|
||||
result_text = cstring_to_text(result.data);
|
||||
pfree(result.data);
|
||||
}
|
||||
PG_CATCH();
|
||||
{
|
||||
/* Switch back to the original memory context for error handling */
|
||||
MemoryContextSwitchTo(old_context);
|
||||
|
||||
/* Clean up */
|
||||
if (template_str)
|
||||
pfree(template_str);
|
||||
|
||||
/* Delete the parse context */
|
||||
MemoryContextDelete(parse_context);
|
||||
|
||||
PG_RE_THROW();
|
||||
}
|
||||
PG_END_TRY();
|
||||
|
||||
/* Clean up */
|
||||
MemoryContextDelete(parse_context);
|
||||
pfree(template_str);
|
||||
|
||||
PG_RETURN_TEXT_P(result_text);
|
||||
}
|
||||
|
||||
PG_FUNCTION_INFO_V1(pg_render);
|
||||
Datum
|
||||
pg_render(PG_FUNCTION_ARGS)
|
||||
{
|
||||
text *template_text = PG_GETARG_TEXT_PP(1);
|
||||
Jsonb *context_jsonb = PG_GETARG_JSONB_P(0);
|
||||
char *template_str = text_to_cstring(template_text);
|
||||
const char *template_ptr = template_str;
|
||||
MemoryContext old_context;
|
||||
MemoryContext render_context;
|
||||
TemplateConfig config;
|
||||
TemplateNode *root = NULL;
|
||||
TemplateErrorCode error_code = TEMPLATE_ERROR_NONE;
|
||||
bool render_error = false;
|
||||
char *rendered_result = NULL;
|
||||
text *result_text = NULL;
|
||||
|
||||
/* Create a memory context for rendering */
|
||||
render_context = AllocSetContextCreate(CurrentMemoryContext,
|
||||
"Template Render Context",
|
||||
ALLOCSET_DEFAULT_SIZES);
|
||||
|
||||
/* Switch to the new context for parsing and rendering */
|
||||
old_context = MemoryContextSwitchTo(render_context);
|
||||
|
||||
/* Initialize default config */
|
||||
config = template_default_config(render_context);
|
||||
|
||||
PG_TRY();
|
||||
{
|
||||
/* Parse the template */
|
||||
root = template_parse(render_context, &template_ptr, &config, false, &error_code);
|
||||
|
||||
/* Check for parsing errors */
|
||||
if (error_code != TEMPLATE_ERROR_NONE || !root)
|
||||
{
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_SYNTAX_ERROR),
|
||||
errmsg("Template parsing error: %s", template_error_to_string(error_code))));
|
||||
}
|
||||
|
||||
/* Render the template with the provided context */
|
||||
rendered_result = template_render(render_context, root, PointerGetDatum(context_jsonb), &render_error);
|
||||
|
||||
/* Check for rendering errors */
|
||||
if (render_error || !rendered_result)
|
||||
{
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_INTERNAL_ERROR),
|
||||
errmsg("Template rendering error")));
|
||||
}
|
||||
|
||||
/* Convert the result to a text datum in the original memory context */
|
||||
MemoryContextSwitchTo(old_context);
|
||||
result_text = cstring_to_text(rendered_result);
|
||||
}
|
||||
PG_CATCH();
|
||||
{
|
||||
/* Switch back to the original memory context for error handling */
|
||||
MemoryContextSwitchTo(old_context);
|
||||
|
||||
/* Clean up */
|
||||
if (template_str)
|
||||
pfree(template_str);
|
||||
|
||||
/* Delete the render context */
|
||||
MemoryContextDelete(render_context);
|
||||
|
||||
PG_RE_THROW();
|
||||
}
|
||||
PG_END_TRY();
|
||||
|
||||
/* Clean up */
|
||||
MemoryContextDelete(render_context);
|
||||
pfree(template_str);
|
||||
|
||||
PG_RETURN_TEXT_P(result_text);
|
||||
}
|
||||
|
||||
/* Helper function to convert template node to string for debugging */
|
||||
static void
|
||||
template_node_to_string(TemplateNode *node, StringInfo result, int indent)
|
||||
{
|
||||
TemplateNode *current = node;
|
||||
int i;
|
||||
|
||||
while (current)
|
||||
{
|
||||
/* Add indentation */
|
||||
for (i = 0; i < indent; i++)
|
||||
appendStringInfoChar(result, ' ');
|
||||
|
||||
/* Add node type */
|
||||
switch (current->type)
|
||||
{
|
||||
case TEMPLATE_NODE_TEXT:
|
||||
appendStringInfo(result, "TEXT: \"%s\"\n",
|
||||
current->value->text.content ? current->value->text.content : "");
|
||||
break;
|
||||
|
||||
case TEMPLATE_NODE_INTERPOLATE:
|
||||
appendStringInfo(result, "INTERPOLATE: \"%s\"\n",
|
||||
current->value->interpolate.key ? current->value->interpolate.key : "");
|
||||
break;
|
||||
|
||||
case TEMPLATE_NODE_SECTION:
|
||||
appendStringInfo(result, "SECTION: iterator=\"%s\", collection=\"%s\"\n",
|
||||
current->value->section.iterator ? current->value->section.iterator : "",
|
||||
current->value->section.collection ? current->value->section.collection : "");
|
||||
|
||||
if (current->value->section.body)
|
||||
{
|
||||
template_node_to_string(current->value->section.body, result, indent + 2);
|
||||
}
|
||||
break;
|
||||
|
||||
case TEMPLATE_NODE_EXECUTE:
|
||||
appendStringInfo(result, "EXECUTE: \"%s\"\n",
|
||||
current->value->execute.code ? current->value->execute.code : "");
|
||||
break;
|
||||
|
||||
case TEMPLATE_NODE_INCLUDE:
|
||||
appendStringInfo(result, "INCLUDE: \"%s\"\n",
|
||||
current->value->include.key ? current->value->include.key : "");
|
||||
break;
|
||||
|
||||
default:
|
||||
appendStringInfo(result, "UNKNOWN NODE TYPE: %d\n", current->type);
|
||||
break;
|
||||
}
|
||||
|
||||
current = current->next;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user