From 3fd5dd2f74618a82c7ca9636e3c908463e8ef341 Mon Sep 17 00:00:00 2001 From: gwaredd Date: Wed, 2 Feb 2022 13:16:19 +0000 Subject: [PATCH] Fix for function parameter parsing --- .gitignore | 1 + Assets/Editor/Unit Tests/TestPath.cs | 81 ++++++++++ Assets/Editor/Unit Tests/TestPath.cs.meta | 11 ++ Assets/Unium/GQL/Query/Path.cs | 188 ++++++++++++++++------ 4 files changed, 230 insertions(+), 51 deletions(-) create mode 100644 Assets/Editor/Unit Tests/TestPath.cs create mode 100644 Assets/Editor/Unit Tests/TestPath.cs.meta diff --git a/.gitignore b/.gitignore index 7967db1..abb8513 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ Assets/AssetStoreTools* # Visual Studio 2015 cache directory .vs/ .vscode/ +.vsconfig # Autogenerated VS/MD/Consulo solution and project files ExportedObj/ diff --git a/Assets/Editor/Unit Tests/TestPath.cs b/Assets/Editor/Unit Tests/TestPath.cs new file mode 100644 index 0000000..d0a87da --- /dev/null +++ b/Assets/Editor/Unit Tests/TestPath.cs @@ -0,0 +1,81 @@ +// Copyright (c) 2017 Gwaredd Mountain, https://opensource.org/licenses/MIT +#if !UNIUM_DISABLE && ( DEVELOPMENT_BUILD || UNITY_EDITOR || UNIUM_ENABLE ) + +using NUnit.Framework; +using gw.gql; + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +public class TestPath +{ + //---------------------------------------------------------------------------------------------------- + + [Test] + public void Basic() + { + Path p; + + // quick tests + p = new Path( "/root" ); + Assert.AreEqual( 1, p.Length ); + Assert.AreEqual( "root", p[ 0 ].Select ); + Assert.AreEqual( Query.Action.Get, p.Action ); + + p = new Path( "/root/obj[3].attr" ); + Assert.AreEqual( 3, p.Length ); + + p = new Path( "/root/obj.attr" ); + Assert.AreEqual( 3, p.Length ); + + Assert.AreEqual( "root", p[ 0 ].Select ); + Assert.AreEqual( "obj", p[ 1 ].Select ); + Assert.AreEqual( "attr", p[ 2 ].Select ); + + Assert.IsTrue( p[ 0 ].NodeType == Path.Segment.Type.Children ); + Assert.IsTrue( p[ 1 ].NodeType == Path.Segment.Type.Children ); + Assert.IsTrue( p[ 2 ].NodeType == Path.Segment.Type.Attribute ); + + // set value + p = new Path( "/root/obj.attr.val=abc,def" ); + Assert.AreEqual( 3, p.Length ); + Assert.AreEqual( Query.Action.Set, p.Action ); + Assert.AreEqual( "val", p.Target ); + Assert.AreEqual( 1, p.Arguments.Length ); + Assert.AreEqual( "abc,def", p.Arguments[ 0 ] ); + + // function call + p = new Path( "/root/obj.attr.func(123)" ); + Assert.AreEqual( 3, p.Length ); + Assert.AreEqual( Query.Action.Invoke, p.Action ); + Assert.AreEqual( "func", p.Target ); + Assert.AreEqual( 1, p.Arguments.Length ); + Assert.AreEqual( "123", p.Arguments[ 0 ] ); + } + + //---------------------------------------------------------------------------------------------------- + + [Test] + public void Args() + { + Assert.AreEqual( new string[] { }, Path.SplitArgs( "" ) ); + Assert.AreEqual( new string[] { }, Path.SplitArgs( " " ) ); + + Assert.AreEqual( new string[] { "a" }, Path.SplitArgs( "a" ) ); + Assert.AreEqual( new string[] { "a" }, Path.SplitArgs( " a " ) ); + Assert.AreEqual( new string[] { "a", "b", "c" }, Path.SplitArgs( "a,b,c" ) ); + Assert.AreEqual( new string[] { "a", "b", "c" }, Path.SplitArgs( " a , b , c " ) ); + Assert.AreEqual( new string[] { "12.3", "45", "abc" }, Path.SplitArgs( "12.3,45,abc" ) ); + + Assert.AreEqual( new string[] { "a", "b", "c" }, Path.SplitArgs( " \"a\" , \"b\", \"c\" " ) ); + Assert.AreEqual( new string[] { "ab,c, 7", "0" }, Path.SplitArgs( "\"ab,c, 7\", 0" ) ); + Assert.AreEqual( new string[] { " ab,c\", 7 ", "0" }, Path.SplitArgs( "\" ab,c\\\", 7 \", 0" ) ); + + Assert.AreEqual( new string[] { "{}" }, Path.SplitArgs( " {} " ) ); + Assert.AreEqual( new string[] { "{\"name\":\"g\",\"v\":{x:1,y:1,z:1}}" }, Path.SplitArgs( " {\"name\":\"g\",\"v\":{x:1,y:1,z:1}} " ) ); + Assert.AreEqual( new string[] { "{\"name\":\"g\",\"v\":{x:1,y:1,z:1}}", "123" }, Path.SplitArgs( " {\"name\":\"g\",\"v\":{x:1,y:1,z:1}} , 123 " ) ); + + Assert.AreEqual( new string[] { "[{x:1},{a:1,b:1},{c:[1,2,3]}]" }, Path.SplitArgs( "[{x:1},{a:1,b:1},{c:[1,2,3]}]" ) ); + } +} + +#endif diff --git a/Assets/Editor/Unit Tests/TestPath.cs.meta b/Assets/Editor/Unit Tests/TestPath.cs.meta new file mode 100644 index 0000000..d9d2a94 --- /dev/null +++ b/Assets/Editor/Unit Tests/TestPath.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ee17d7fcd2da4294b9ce86332fd073bd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Unium/GQL/Query/Path.cs b/Assets/Unium/GQL/Query/Path.cs index a459829..98f1bd2 100644 --- a/Assets/Unium/GQL/Query/Path.cs +++ b/Assets/Unium/GQL/Query/Path.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Text; using System.Text.RegularExpressions; namespace gw.gql @@ -42,6 +43,134 @@ public enum Type { Children, Attribute }; } + //---------------------------------------------------------------------------------------------------- + // GQL function parameters are JSON objects, separated by commas + // SplitArgs - split a given parameters string into then separate argument + + private static readonly HashSet sPunctuation = new HashSet( "\":,{}[] " ); + + static public string[] SplitArgs( string str ) + { + var args = new List(); + var value = new StringBuilder(); + var depth = 0; + var begin = 0; + + for( int i = 0; i < str.Length; i++ ) + { + // get next token + + var token = str[i]; + + // ignore whitespace (and control characters) + + if( token <= ' ' ) + { + continue; + } + + // depth == -1 means we have just created an argument + // therefore the only valid next token is a comma + + if( depth == -1 ) + { + if( token != ',' ) + { + throw new FormatException( "GQL::Query - bad function parameters" ); + } + + depth = 0; + continue; + } + + switch( token ) + { + // ignore data structure tokens - we are not a validating parser + + case ':': + case ',': + break; + + // keep track of data structure 'nesting' so we can figure out the end. + // NB: there is no validation here, we just keep track of the brackets + + case '{': + case '[': + { + if( ++depth == 1 ) + { + begin = i; // the start of a JSON data structure + } + } + break; + + case '}': + case ']': + { + if( --depth < 0 ) + { + throw new FormatException( "GQL::Query - bad function parameters" ); + } + } + break; + + // get string + + case '\"': + { + value.Clear(); + + var escape = false; + + while( ++i < str.Length && ( escape || str[i] != token ) ) + { + escape = !escape && str[i] == '\\'; + + if( !escape ) + { + value.Append( str[i] ); + } + } + } + break; + + // get value + + default: + { + token = 'V'; + + var start = i; + while( ++i < str.Length && !sPunctuation.Contains( str[i] ) ); + + value.Clear(); + value.Append( str, start, i - start ); + --i; + } + break; + } + + + // process token + + if( depth == 0 ) + { + if( token == 'V' || token == '\"' ) + { + args.Add( value.ToString() ); + depth = -1; // want comma + } + else if( token == '}' || token == ']' ) + { + args.Add( str.Substring( begin, i - begin + 1 ) ); + depth = -1; // want comma + } + } + } + + return args.ToArray(); + } + //---------------------------------------------------------------------------------------------------- // q := /some/node.attr[x>3]/child.value // q := /some/node.attr[x>3]/child.value=value @@ -61,6 +190,8 @@ public enum Type { Children, Attribute }; #endif ); + + public void Parse( string query ) { // parse path @@ -93,7 +224,7 @@ public void Parse( string query ) else { - // being recrusive find ... + // being recursive find ... // only available on child nodes because we don't list attr's @@ -152,53 +283,8 @@ public void Parse( string query ) throw new FormatException( "GQL::Query - failed to parse invoke, end of arguments not found" ); } - Action = Query.Action.Invoke; - - var argsStr = p.Substring( 0, p.Length - 1 ); - - var args = new List(); - - for( int pos = 0; pos < argsStr.Length; pos++ ) - { - var ch = argsStr[ pos ]; - - if( char.IsWhiteSpace( ch ) ) - { - continue; - } - - var start = pos; - - if( ch == '\'' || ch == '{' || ch == '"' ) - { - var end = ch == '{' ? '}' : ch; - - while( ++pos < argsStr.Length ) - { - if( argsStr[ pos ] == end ) - { - args.Add( argsStr.Substring( start, pos - start + 1 ) ); - break; - } - } - } - else - { - do - { - if( pos == argsStr.Length || argsStr[ pos ] == ',' || char.IsWhiteSpace( argsStr[ pos ] ) ) - { - args.Add( argsStr.Substring( start, pos - start ) ); - break; - } - - pos++; - } - while( true ); - } - } - - Arguments = args.ToArray(); + Action = Query.Action.Invoke; + Arguments = SplitArgs( p.Substring( 0, p.Length - 1 ) ); } // an action can only occur on the last segment @@ -214,11 +300,11 @@ public void Parse( string query ) // wildcard name match? - #if NET_2_0 +#if NET_2_0 Regex name_match = name.Contains( "*" ) ? new Regex( name.Replace( "*", ".*?" ), RegexOptions.Compiled ) : null; - #else +#else Regex name_match = name.Contains( "*" ) ? new Regex( name.Replace( "*", ".*?" ) ) : null; - #endif +#endif // add section to path