Merge old Neo4j graph generation POC

This was never completed and will be able to be deleted entirely, but I
didn't want to lose this history by having it sit out in a branch.  Joe is
working on something better.
master
Mike Gerwitz 2020-05-13 00:45:01 -04:00
commit 18d87a6b00
5 changed files with 1053 additions and 0 deletions

View File

@ -154,6 +154,12 @@ c1map: $(dest_c1map)
$(TAME_TS)
$(TAME) dot $< $@
%.neo4j: %.xmlo
$(TAME) neo4j $< $@
%.neo4je: %.xmle
$(TAME) neo4j $< $@
%.svg: %.dote
dot -Tsvg "$<" > "$@"
%.svg: %.dot

View File

@ -0,0 +1,328 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<!--
Produces CREATE statements to export package dependency graph into Neo4j
Copyright (C) 2014-2019 Ryan Specialty Group, LLC.
This file is part of TAME.
TAME is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see
<http://www.gnu.org/licenses/>.
Unlike the dot exporter, this is merciless: it'll export all the
information that it can, with some minor cleanup to hide some unnecessary
compiler implementation deatils (like classification splitting). You are
expected to filter out what you're interested in when querying Neo4j.
This also produces more realtionships than the dot format, including
relationships between packges. So Neo4j can be used as a static analysis
tool down to the lowest levels of Tame.
-->
<stylesheet version="2.0"
xmlns="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:lv="http://www.lovullo.com/rater"
xmlns:l="http://www.lovullo.com/rater/linker"
xmlns:neo="http://www.lovullo.com/calc/dot"
xmlns:st="http://www.lovullo.com/liza/proguic/util/struct"
xmlns:preproc="http://www.lovullo.com/rater/preproc">
<include href="util/serialize.xsl" />
<include href="include/preproc/path.xsl" />
<output method="text" />
<!--
Newline character
-->
<variable name="nl" select="'&#10;'" />
<variable name="fqn-prefix" as="xs:string"
select="'mega-demo/'" />
<variable name="neo:orig-root" as="document-node( element( lv:package ) )"
select="/" />
<variable name="neo:orig-package" as="element( lv:package )"
select="$neo:orig-root/lv:package" />
<!--
Linked symbols
This is expected to be executed in Neo4j _after_ package-level data has
been added to the graph.
-->
<template match="lv:package[ l:dep ]" priority="7">
<sequence select="concat(
':param refs => ',
st:to-neo4j-attrs(
st:array( neo:linked-syms( l:dep ) ) ),
';', $nl )" />
</template>
<function name="neo:linked-syms" as="element( st:item )+">
<param name="deps" as="element( l:dep )" />
<variable name="pkg-path" as="xs:string"
select="preproc:get-path( $deps/ancestor::lv:package/@name )" />
<sequence select="for $dep in $deps/preproc:sym
return st:item(
preproc:resolv-path(
concat( $fqn-prefix, '/',
$pkg-path, '/',
$dep/@src, '/', $dep/@name ) ) )" />
</function>
<template match="lv:package" priority="5">
<variable name="sym-deps" select="preproc:sym-deps" />
<sequence select="concat(
':param syms => ',
st:to-neo4j-attrs(
st:array(
for $sym in neo:get-package-syms( preproc:symtable )
return st:item( neo:dict-from-sym( $sym ) ) ) ),
';',
$nl,
'CREATE (pkg:TamePackage ',
st:to-neo4j-attrs(
st:dict( (
st:item( concat( $fqn-prefix, '/', @name ),
'fqn' ),
st:items-from-attrs( @* ) ) ) ),
')',
$nl,
'WITH pkg',
$nl,
'UNWIND $syms AS symdata ',
'MERGE (sym:TameSymbol {fqn: symdata.fqn}) ',
'SET sym += symdata ',
'CREATE (pkg)-[:DEFINES]->(sym) ',
';', $nl,
':commit', $nl,
':begin', $nl,
':param deps => ',
st:to-neo4j-attrs(
st:array(
for $dep in neo:get-package-deps( . )
return st:item( neo:gen-dep-data( $dep ) ) ) ),
$nl,
'UNWIND $deps AS depdata ',
'MATCH (from:TameSymbol {fqn: depdata.from}) ',
'UNWIND depdata.to AS toname ',
'MERGE (to:TameSymbol {fqn: toname}) ',
'CREATE (from)-[:USES]->(to)',
';', $nl )" />
<!--
<apply-templates select="preproc:sym-deps/preproc:sym-dep" />
<apply-templates select="preproc:sym-deps/preproc:sym-dep/preproc:sym-ref" />
-->
</template>
<function name="neo:sym-src" as="xs:string">
<param name="sym" as="element( preproc:sym )" />
<sequence select="if ( $sym/@src and not( $sym/@src = '' ) ) then
neo:package-lookup( $sym/@src )/@name
else
$sym/ancestor::lv:package/@name" />
</function>
<function name="neo:sym-fqn" as="xs:string">
<param name="sym" as="element( preproc:sym )" />
<sequence select="concat( $fqn-prefix, '/',
neo:sym-src( $sym ), '/', $sym/@name )" />
</function>
<function name="neo:dict-from-sym" as="element( st:dict )">
<param name="sym" as="element( preproc:sym )" />
<variable name="attrs" as="attribute()+">
<sequence select="$sym/@*" />
<attribute name="fqn" select="neo:sym-fqn( $sym )" />
</variable>
<sequence select="st:dict( st:items-from-attrs( $attrs ) )" />
</function>
<function name="neo:package-lookup" as="element( lv:package )">
<param name="src" as="xs:string?" />
<sequence select="if ( $src and not( $src = '' ) ) then
document( concat( $src, '.xmlo' ), $neo:orig-root )/lv:*
else
$neo:orig-package" />
</function>
<function name="neo:is-mergable" as="xs:boolean">
<param name="sym" as="element( preproc:sym )" />
<!-- externs aren't resolved until linking, so we cannot hope to merge
them here -->
<sequence select="( exists( $sym/@preproc:generated-from )
or $sym/@type = 'cgen'
or ( $sym/@type = 'tpl'
and $sym/@preproc:generated = 'true' ) )
and not( $sym/@extern = 'true' )" />
</function>
<function name="neo:is-mergable-ref" as="xs:boolean">
<param name="sym-ref" as="element( preproc:sym-ref )" />
<variable name="name" as="xs:string"
select="$sym-ref/@name" />
<variable name="sym" as="element( preproc:sym )"
select="$sym-ref/ancestor::lv:package/preproc:symtable
/preproc:sym[ @name = $name ]" />
<sequence select="neo:is-mergable( $sym )" />
</function>
<function name="neo:is-mergable-dep" as="xs:boolean">
<param name="sym-dep" as="element( preproc:sym-dep )" />
<variable name="name" as="xs:string"
select="$sym-dep/@name" />
<variable name="sym" as="element( preproc:sym )"
select="$sym-dep/ancestor::lv:package/preproc:symtable
/preproc:sym[ @name = $name ]" />
<sequence select="neo:is-mergable( $sym )" />
</function>
<function name="neo:get-package-syms" as="element( preproc:sym )*">
<param name="symtable" as="element( preproc:symtable )" />
<sequence select="$symtable/preproc:sym[
not( @src )
and not( neo:is-mergable( . ) ) ]" />
</function>
<function name="neo:get-package-deps" as="element( preproc:sym-dep )*">
<param name="package" as="element( lv:package )" />
<sequence select="$package/preproc:sym-deps/preproc:sym-dep[
not( neo:is-mergable-dep( . ) ) ]" />
</function>
<function name="neo:get-sym-deps-by-ref" as="element( preproc:sym-ref )*">
<param name="sym-ref" as="element( preproc:sym-ref )" />
<variable name="name" as="xs:string"
select="$sym-ref/@name" />
<variable name="local-deps" as="element( preproc:sym-dep )?"
select="$sym-ref/ancestor::preproc:sym-deps/preproc:sym-dep[
@name = $name ]" />
<variable name="deps" as="element( preproc:sym-dep )"
select="if ( exists( $local-deps ) ) then
$local-deps
else
neo:package-lookup-by-ref( $sym-ref )
/preproc:sym-deps/preproc:sym-dep[ @name = $name ]" />
<sequence select="neo:get-sym-deps( $deps )" />
</function>
<function name="neo:package-lookup-by-ref" as="element( lv:package )">
<param name="sym-ref" as="element( preproc:sym-ref )" />
<variable name="name" as="xs:string"
select="$sym-ref/@name" />
<variable name="sym" as="element( preproc:sym )"
select="$sym-ref/ancestor::lv:package/preproc:symtable
/preproc:sym[ @name = $name ]" />
<sequence select="neo:package-lookup( $sym/@src )" />
</function>
<function name="neo:is-ignorable-ref" as="xs:boolean">
<param name="sym-ref" as="element( preproc:sym-ref )" />
<!-- much quicker check than using neo:is-mergable -->
<sequence select="$sym-ref/@name = '_CMATCH_'" />
</function>
<function name="neo:get-sym-deps" as="element( preproc:sym-ref )*">
<param name="sym-dep" as="element( preproc:sym-dep )" />
<sequence select="for $ref in $sym-dep/preproc:sym-ref[
not( neo:is-ignorable-ref( . ) ) ]
return if ( neo:is-mergable-ref( $ref ) ) then
neo:get-sym-deps-by-ref( $ref )
else
$ref" />
</function>
<function name="neo:gen-dep-data" as="element( st:dict )">
<param name="dep" as="element( preproc:sym-dep )" />
<variable name="refs" as="element( preproc:sym-ref )*"
select="neo:get-sym-deps( $dep )" />
<variable name="sym" as="element( preproc:sym )"
select="$dep/ancestor::lv:package/preproc:symtable
/preproc:sym[ @name = $dep/@name ]" />
<sequence select="st:dict( (
st:item( neo:sym-fqn( $sym ), 'from' ),
st:item(
st:array(
for $ref in $refs
return st:item( neo:sym-ref-fqn( $ref ) ) ),
'to' ) ) )" />
</function>
<function name="neo:sym-ref-fqn" as="xs:string">
<param name="sym-ref" as="element( preproc:sym-ref )" />
<variable name="sym" as="element( preproc:sym )"
select="$sym-ref/ancestor::lv:package/preproc:symtable
/preproc:sym[ @name = $sym-ref/@name ]" />
<sequence select="neo:sym-fqn( $sym )" />
</function>
</stylesheet>

View File

@ -0,0 +1,60 @@
:commit
// it's easier just to create these labels after-the-fact rather than muddy
// the query generation
:begin
MATCH (n:TameSymbol {type: "class"}) SET n:TameClass;
MATCH (n:TameSymbol {type: "rate"}) SET n:TameRate;
MATCH (n:TameSymbol {type: "gen"}) SET n:TameGen;
MATCH (n:TameSymbol {type: "cgen"}) SET n:TameCgen;
MATCH (n:TameSymbol {type: "type"}) SET n:TameType;
MATCH (n:TameSymbol {type: "const"}) SET n:TameConst;
MATCH (n:TameSymbol {type: "func"}) SET n:TameFunc;
MATCH (n:TameSymbol {type: "param"}) SET n:TameParam;
MATCH (n:TameSymbol {type: "map"}) SET n:TameMapInput;
MATCH (n:TameSymbol {type: "retmap"}) SET n:TameMapReturn;
MATCH (n:TameSymbol {extern: "true"}) SET n:TameExtern;
MATCH (n:TameSymbol {generated: "true"}) SET n:TameSymbolGenerated;
MATCH (n:TamePackage {program: "true"}) SET n:TameProgram;
:commit
// these are created in post.neo4j beacuse (a) they're used only for
// querying and (b) they're more convenient to define with the new labels
:begin
CREATE INDEX ON :TameSymbol(name);
CREATE INDEX ON :TameConst(value);
CREATE INDEX ON :TameClass(orig_name);
:commit
// cleanup that's easier to do here than in XSLT
:begin
// typedefs define constants, not use them
MATCH (c:TameConst)<-[r:USES]-(t:TameType)
CREATE UNIQUE (c)<-[:DEFINES]-(t)
DELETE r;
// same with unions
MATCH (t:TameType)<-[r:USES]-(u:TameType)
CREATE UNIQUE (t)<-[:DEFINES]-(u)
DELETE r;
// maps map, not use
MATCH (n:TameMapInput)-[r:USES]->(p:TameParam)
CREATE (n)-[:MAPS]->(p)
DELETE r;
:commit
// for convenience in querying; framework-wide conventions
:begin
MATCH (n:TameClass) WHERE n.orig_name STARTS WITH "submit-" SET n:SubmitRule;
MATCH (n:TamePackage {program: "true"}) WHERE n.name STARTS WITH "suppliers/" SET n:Supplier;
:commit
// megarater-specific
:begin
MATCH (c:TameConst)<-[*0..3]-(:TameType {name: "classCode"}) SET c:ClassCode;
:commit

View File

@ -0,0 +1,8 @@
// some of the indexes are created in post.neo4j
:begin
CREATE CONSTRAINT ON (n:TamePackage) ASSERT n.fqn IS UNIQUE;
CREATE CONSTRAINT ON (n:TameSymbol) ASSERT n.fqn IS UNIQUE;
:commit
:begin

View File

@ -0,0 +1,651 @@
<!--
Serialization utility functions
Copyright (C) 2017, 2018 R-T Specialty, LLC.
This file is part of the Liza Program UI Compiler.
liza-proguic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see
<http://www.gnu.org/licenses/>.
-->
<stylesheet version="2.0"
xmlns="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:struct="http://www.lovullo.com/liza/proguic/util/struct"
xmlns:f="http://mikegerwitz.com/hoxsl/apply"
xmlns:_struct="http://www.lovullo.com/liza/proguic/util/struct/_priv">
<import href="../hoxsl/src/apply.xsl" />
<!--
@node Serialization
@section Serialization
@cindex Serialization
@luic{} uses a primitive API for representing and serializing objects,
most notably JSON (@pxref{JSON Transformation}).
This avoids having to handle string generation
(and couple with an implementation) in various systems.
-->
<variable name="struct:error-qname" as="xs:QName"
select="QName(
'http://www.lovullo.com/liza/proguic/util/struct/error',
'err:BADSTRUCT' )" />
<!--
@cindex Array
An @dfn{array} is an untyped list of items.
Usually,
this provides @math{O(n)} lookups.
It is ideal for linear processing of data.
The term ``array'' is abused in certain languages;
if you are looking for a key/value store, use
@ref{struct:dict#1,,@code{struct-dict}}.
-->
<!--
Generate an empty array.
-->
<function name="struct:array" as="element( struct:array )">
<struct:array />
</function>
<!--
Generate an untyped array of values.
Arrays must contain only @xmlnode{struct:item} elements,
but unlike dictionaries,
they @emph{must not} contain a@tie{}@xmlattr{key}.
-->
<function name="struct:array" as="element( struct:array )">
<param name="values" as="element( struct:item )*" />
<struct:array>
<sequence select="$values" />
</struct:array>
</function>
<!--
@cindex Dictionary
A @dfn{dictionary} is a key/value store.
Like arrays,
dictionaries contain items,
but they are indexed by keys.
Usually,
languages implement this as a hash table,
providing @math{O(1)} lookups.
-->
<!--
Create an empty dictionary.
-->
<function name="struct:dict" as="element( struct:dict )">
<struct:dict />
</function>
<!--
Create a dictionary of values.
This is a key-value store containing only @xmlnode{struct:item}
elements with @xmlattr{key} attributes (@pxref{struct:item#2}).
-->
<function name="struct:dict" as="element( struct:dict )">
<param name="values" as="element( struct:item )*" />
<struct:dict>
<sequence select="$values" />
</struct:dict>
</function>
<!--
@cindex Item
An @dfn{item} can be either @dfn{keyed} or @dfn{unkeyed}:
the former is suitable only for dictionaries,
while the latter is suitable only for arrays.
@devnotice{Item type metadata should be added;
otherwise, we can only serialize as a string.}
-->
<!--
Associate a value with a key in a dictionary.
A key value may be a primitive value or another structure.
Keyed items must be children of a@tie{}dictionary
(@pxref{struct:dict#1,,@code{struct:dict}}).
Attribute values are converted into strings.
-->
<function name="struct:item" as="element( struct:item )">
<param name="value" />
<param name="id" as="xs:string" />
<struct:item key="{$id}">
<sequence select="if ( $value instance of attribute() ) then
string( $value )
else
$value" />
</struct:item>
</function>
<!--
Create a keyless item.
A key value may be a primitive value or another structure.
Keyless items must be children of a@tie{}array
(@pxref{struct:array#1,,@code{struct:array}}).
Attribute values are converted into strings.
-->
<function name="struct:item" as="element( struct:item )">
<param name="value" />
<struct:item>
<sequence select="if ( $value instance of attribute() ) then
string( $value )
else
$value" />
</struct:item>
</function>
<!--
Since deriving item values from attributes is common,
they will automatically be convered into strings.@footnote{
Really, it makes no sense to permit attributes,
since that will result in the attribute being assigned to the
@xmlnode{struct:item} itself,
which does not make any sense
(and could corrupt internal state depending on what attribute
was set).}
-->
<!--
@subsection Auto-Generating Structures
It's common (and natural) to want to serialize key/value pairs from
attributes.
Two functions provide this convenience:
-->
<!--
Generate keys from attributes.
A key/value pair will be created for each attribute in @var{attrs}
using the attribute's local name as the@tie{}key. Whitespace in attribute
values will be normalized.
-->
<function name="struct:items-from-attrs" as="element( struct:item )*">
<param name="attrs" as="attribute()*" />
<sequence select="for $attr in $attrs
return struct:item( normalize-space( $attr ),
$attr/local-name() )" />
</function>
<!--
Convert an element into a dictinary using its attributes as
key/value pairs.
The name of the element is not used.
The attributes of the node are passed to
@ref{struct:item#1,,@code{struct:item}}.
-->
<function name="struct:dict-from-attrs" as="element( struct:dict )">
<param name="element" as="element()" />
<sequence select="struct:dict(
struct:items-from-attrs( $element/@* ) )" />
</function>
<!--
Convert a sequence of elements into an array of dictionaries using element
attributes as dictionary key/value pairs.
Each element is processed using
@ref{struct:dict-from-attrs#1,,@code{struct:dict-from-attrs}}.
-->
<function name="struct:dict-array-from-elements" as="element( struct:array )">
<param name="elements" as="element()*" />
<sequence select="struct:array(
for $element in $elements
return struct:item(
struct:dict-from-attrs( $element ) ) )" />
</function>
<!--
Another function allows allows using one of the attibutes as
a@tie{}key to recursively generate a dictionary of multiple
elements,
provided that those elements have unquie keys.
-->
<!--
Recurisvely generate dictionary using an attribute as a key.
This generates a new dictionary with @code{n}@tie{}entires where the
value of the key is another dictionary containing the key/value
representation of the remaining attributes,
where @code{n = count($element)}.
If @var{$recf} is non-empty,
it will be applied to each element that generates an item in the
parent dictionary;
this allows for recursive processing.
The function is applied within the context of the dictionary and
should therefore return one or more @xmlnode{struct:item}s.
Beware: the given key @var{$key} is compared only by @code{local-name}.
@emph{No check is performed to ensure they all keys are unique in
the toplevel dictionary.}
Conflicts result in undefined behavior dependent on the serializer.
For example,
when serialized to JSON,
the latter key takes precedence and the former keys are overwritten.
@devnotice{Should probably handle more gracefully a situation where the
key attribute does not exist on one of the elements.}
-->
<function name="struct:dict-from-keyed-elements" as="element( struct:dict )">
<param name="key" as="xs:string" />
<param name="elements" as="element()*" />
<param name="recf" as="item()*" />
<sequence select="
struct:dict(
for $element in $elements
return struct:item(
struct:dict(
( struct:items-from-attrs(
$element/@*[ not( local-name() = $key ) ] ),
if ( $recf ) then
f:apply( $recf, $element )
else
() ) ),
$element/@*[ local-name() = $key ] ) )" />
</function>
<!--
Recurisvely generate dictionary using an attribute as a key.
This two-argument version simply invokes
@coderef{struct:dict-from-keyed-elements#3} without a child function.
-->
<function name="struct:dict-from-keyed-elements" as="element( struct:dict )">
<param name="key" as="xs:string" />
<param name="elements" as="element()*" />
<sequence select="struct:dict-from-keyed-elements( $key, $elements, () )" />
</function>
<!--
An example usage of this function is provided in
@ref{f:dict-from-keyed-elements}.
@float Figure, f:dict-from-keyed-elements
Given some document:
@example
<meta>
<field id="foo" desc="Has nested" type="string">
<nested name="n1" />
</field>
<field id="bar" desc="No nested" type="boolean" />
</meta>
@end example
With function:
@example
<function name="nestedf" as="element( struct:item )+">
<param name="field" as="element( field )" />
<sequence select="struct:item( $field/nested/@@name, 'nestedf' )" />
</function>
@end example
Transformed with:
@example
<sequence select="struct:dict-from-keyed-elements( 'id', meta, nestedf() )" />
@end example
Results in:
@example
<struct:dict>
<struct:item key="foo">
<struct:dict>
<struct:item key="desc">Has nested</struct:item>
<struct:item key="type">string</struct:item>
<struct:item key="nestedf">n1</struct:item>
</struct:dict>
</struct:item>
<struct:item key="bar">
<struct:dict>
<struct:item key="desc">No nested</struct:item>
<struct:item key="type">boolean</struct:item>
<struct:item key="nestedf"></struct:item>
</struct:dict>
</struct:item>
</struct:dict>
@end example
@caption{Generating a dictionary from keyed elements.}
@end float
Extracting key/value pairs from element attributes is also a common
operation:
-->
<!--
Generate keyed items for each element in @var{$elements} using one
attribute @var{$key}@tie{}as the key and another attribute
@var{$value}@tie{}as the value.
Beware: the given key @var{$key} is compared only by @code{local-name}.
The arguments are ordered such that this is useful as a partially
applied function for processing lists of elements with lambdas.
-->
<function name="struct:items-from-keyed-elements" as="element( struct:item )*">
<param name="key" as="xs:string" />
<param name="value" as="xs:string" />
<param name="elements" as="element()*" />
<sequence select="for $element in $elements
return struct:item(
$element/@*[ local-name() = $value ],
$element/@*[ local-name() = $key ] )" />
</function>
<!--
When generating dictionary items in a loop from numerous elements,
it can be inconvenient keeping track of unique keys.
If the goal is to create an array of items grouped by unique keys,
you're in luck:
-->
<!--
Group keyed items into arrays indexed by their respective keys.
Every unique key@tie{}@code{k} will result in an array@mdash{
}indexed by@tie{}@code{k}@mdash{
}containing each respective item.
@emph{Items without keys will not be retained!}
-->
<function name="struct:group-items-by-key" as="element( struct:item )*">
<param name="items" as="element( struct:item )*" />
<for-each-group select="$items" group-by="@key">
<struct:item key="{current-grouping-key()}">
<struct:array>
<sequence select="for $item in current-group()
return struct:item( $item/node() )" />
</struct:array>
</struct:item>
</for-each-group>
</function>
<!--
@menu
* JSON Transformation:: Serializing to JSON.
@end menu
-->
<!--
@node JSON Transformation
@subsection JSON Transformation
@cindex JSON
The recommended way to serialize a structure as JSON is to apply
@ref{struct:to-neo4j-attrs#1,,@code{struct:to-neo4j-attrs}}.
-->
<!--
Transform structure into JSON.
-->
<function name="struct:to-neo4j-attrs" as="xs:string">
<param name="struct" as="element()" />
<variable name="result" as="xs:string*">
<apply-templates mode="struct:to-neo4j-attrs"
select="$struct" />
</variable>
<!-- force to a single string rather than a sequence of them -->
<sequence select="string-join( $result, '' )" />
</function>
<!--
We assume that the structure is already well-formed;@footnote{
That might not necessarily be assured by this implementation,
but validations belong there (@pxref{Serialization}), not here.}
this makes serialization a trivial task.
We proceed by recursive descent.
Let's start with arrays.
@subsubsection Array Serialization
An array simply encapsulates items in square brackets:
-->
<!--
Transform array into JSON.
-->
<template mode="struct:to-neo4j-attrs" priority="5"
match="struct:array">
<text>[</text>
<apply-templates mode="struct:to-neo4j-attrs"
select="node()" />
<text>]</text>
</template>
<!--
Items are simple too,
since we don't have to deal with keys.
If the item contains an element,
we consider it to be a nested structure and recurse:
-->
<!--
Transform nested structure into JSON.
-->
<template mode="struct:to-neo4j-attrs" priority="5"
match="struct:item[ element() ]">
<sequence select="struct:to-neo4j-attrs( ./element() )" />
<if test="following-sibling::struct:item">
<sequence select="','" />
</if>
</template>
<!--
Otherwise, we consider it to be a primitive.
At this point,
items are untyped,
so we have no choice but to serialize as a string:
-->
<!--
Transform primitive data into JSON.
Until items are typed, we have no choice but to serialize all items
as strings.
-->
<template mode="struct:to-neo4j-attrs" priority="4"
match="struct:item">
<sequence select="concat(
'&quot;',
_struct:json-escape-str( . ),
'&quot;' )" />
<if test="following-sibling::struct:item">
<sequence select="','" />
</if>
</template>
<!--
Note that we took care to escape the provided value so that double
quotes do not break out of the serialized string.
-->
<!--
Escape double quotes within a string.
-->
<function name="_struct:json-escape-str" as="xs:string">
<param name="str" as="xs:string" />
<sequence select="replace(
replace( $str, '\\', '\\\\' ), '&quot;', '\\&quot;' )" />
</function>
<!--
@subsubsection Dictionary Serialization
Dictionaries are serialized similarly.
In JSON,
we represent them as objects:
-->
<!--
Transform dictionary into JSON object.
-->
<template mode="struct:to-neo4j-attrs" priority="5"
match="struct:dict">
<text>{</text>
<apply-templates mode="struct:to-neo4j-attrs-dict"
select="node()" />
<text>}</text>
</template>
<!--
Since dictionaries are key/value,
every item needs to be assigned to a field on the object,
where field name is specified by @xmlattr{key}.
Otherwise,
serialization proceeds the same way as arrays:
-->
<!--
Transform dictionary key into JSON field on an object.
The field name is specified by @xmlnode{struct:item/@@key}.
Until items are typed, we have no choice but to serialize all items
as strings.
-->
<template mode="struct:to-neo4j-attrs-dict" priority="5"
match="struct:item[ @key ]">
<sequence select="concat(_struct:neo4j-translate-prop( @key ),
':' )" />
<apply-templates mode="struct:to-neo4j-attrs"
select="." />
</template>
<function name="_struct:neo4j-translate-prop" as="xs:string">
<param name="name" as="xs:string" />
<sequence select="replace( $name, '-', '_' )" />
</function>
<!--
Note that we escape the field.
At a lower priority,
we have a catch-all that will fail if it encounters a non-keyed
structure:
-->
<!--
Error on unrecognized structures during dictionary JSON
transformation.
-->
<template mode="struct:to-neo4j-attrs-dict" priority="1"
match="node()">
<sequence select="error( $struct:error-qname,
concat( 'Unexpected non-key structure: ',
name( . ) ),
. )"/>
</template>
<!--
@subsubsection Miscellaneous
We use comments in test cases to annotate structures.
It's unlikely that they will be used in practice,
but since they are nodes too,
we need to make sure we don't consider them to be errors:
-->
<!--
Ignore comments during processing.
-->
<template mode="struct:to-neo4j-attrs" priority="2"
match="comment()">
</template>
<!--
Everything else we don't know about during processing results in an
error:
-->
<!--
Error on unrecognized structures during JSON transformation.
-->
<template mode="struct:to-neo4j-attrs" priority="1"
match="node()">
<sequence select="error( $struct:error-qname,
concat( 'Unexpected structure: ',
string( . ) ),
. )"/>
</template>
<!--
And we're done.
-->
</stylesheet>