Summary

Class:Xrm.Oss.XTL.Interpreter.XTLInterpreter
Assembly:Xrm.Oss.XTL.Templating
File(s):D:\Entwicklung\Xrm-Templating-Language\src\lib\Xrm.Oss.XTL.Interpreter\XTLInterpreter.cs
Covered lines:293
Uncovered lines:4
Coverable lines:297
Total lines:443
Line coverage:98.6% (293 of 297)
Covered branches:118
Total branches:124
Branch coverage:95.1% (118 of 124)

Metrics

MethodCyclomatic complexity NPath complexity Sequence coverage Branch coverage Crap Score
.ctor(...)22100%100%6
GetChar(...)616100%100%42
Expected(...)10100%100%2
IsEof()10100%100%2
SkipWhiteSpace()34100%100%12
Match(...)2287.5%100%6
GetName()4893.33%100%20
Expression(...)222097152100%100%506
ApplyExpression(...)516100%100%30
Formula(...)24419430497.44%91.11%600
Produce()44100%80%20

File(s)

D:\Entwicklung\Xrm-Templating-Language\src\lib\Xrm.Oss.XTL.Interpreter\XTLInterpreter.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Globalization;
 4using System.IO;
 5using System.Linq;
 6using Microsoft.Xrm.Sdk;
 7
 8namespace Xrm.Oss.XTL.Interpreter
 9{
 10    public class XTLInterpreter
 11    {
 16212        private StringReader _reader = null;
 13        private int _position;
 14        private string _input;
 15        private char _previous;
 16        private char _current;
 17
 18        private Entity _primary;
 19        private IOrganizationService _service;
 20        private ITracingService _tracing;
 21        private OrganizationConfig _organizationConfig;
 22
 23        public delegate ValueExpression FunctionHandler(Entity primary, IOrganizationService service, ITracingService tr
 24
 16225        private Dictionary<string, FunctionHandler> _handlers = new Dictionary<string, FunctionHandler>
 16226        {
 16227            { "And", FunctionHandlers.And },
 16228            { "Array", FunctionHandlers.Array },
 16229            { "Concat", FunctionHandlers.Concat },
 16230            { "ConvertDateTime", FunctionHandlers.ConvertDateTime },
 16231            { "DateTimeNow", FunctionHandlers.DateTimeNow },
 16232            { "DateTimeUtcNow", FunctionHandlers.DateTimeUtcNow },
 16233            { "DateToString", FunctionHandlers.DateToString },
 16234            { "Fetch", FunctionHandlers.Fetch },
 16235            { "First", FunctionHandlers.First },
 16236            { "Format", FunctionHandlers.Format },
 16237            { "If", FunctionHandlers.If },
 16238            { "IndexOf", FunctionHandlers.IndexOf },
 16239            { "IsEqual", FunctionHandlers.IsEqual },
 16240            { "IsGreater", FunctionHandlers.IsGreater },
 16241            { "IsGreaterEqual", FunctionHandlers.IsGreaterEqual },
 16242            { "IsLess", FunctionHandlers.IsLess },
 16243            { "IsLessEqual", FunctionHandlers.IsLessEqual },
 16244            { "IsNull", FunctionHandlers.IsNull },
 16245            { "Join", FunctionHandlers.Join },
 16246            { "Last", FunctionHandlers.Last},
 16247            { "Map", FunctionHandlers.Map },
 16248            { "NewLine", FunctionHandlers.NewLine },
 16249            { "Not", FunctionHandlers.Not },
 16250            { "Or", FunctionHandlers.Or },
 16251            { "OrganizationUrl", FunctionHandlers.GetOrganizationUrl },
 16252            { "PrimaryRecord", FunctionHandlers.GetPrimaryRecord },
 16253            { "RecordId", FunctionHandlers.GetRecordId },
 16254            { "RecordLogicalName", FunctionHandlers.GetRecordLogicalName },
 16255            { "RecordTable", FunctionHandlers.RenderRecordTable },
 16256            { "RecordUrl", FunctionHandlers.GetRecordUrl },
 16257            { "Replace", FunctionHandlers.Replace },
 16258            { "RetrieveAudit", FunctionHandlers.RetrieveAudit },
 16259            { "Snippet", FunctionHandlers.Snippet },
 16260            { "Sort", FunctionHandlers.Sort },
 16261            { "Static", FunctionHandlers.Static },
 16262            { "Substring", FunctionHandlers.Substring },
 16263            { "Union", FunctionHandlers.Union },
 16264            { "Value", FunctionHandlers.GetValue }
 16265        };
 66
 16267        public XTLInterpreter(string input, Entity primary, OrganizationConfig organizationConfig, IOrganizationService 
 16268        {
 16269            _primary = primary;
 16270            _service = service;
 16271            _tracing = tracing;
 16272            _organizationConfig = organizationConfig;
 16273            _input = input;
 16274            _position = 0;
 75
 16276            _reader = new StringReader(input ?? string.Empty);
 16277            GetChar();
 16278            SkipWhiteSpace();
 16279        }
 80
 81        /// <summary>
 82        /// Reads the next character and sets it as current. Old current char becomes previous.
 83        /// </summary>
 84        /// <returns>True if read succeeded, false if end of input</returns>
 85        private void GetChar(int? index = null)
 1336986        {
 1336987            if (index != null)
 1488            {
 89                // Initialize a new reader to move back to the beginning
 1490                _reader = new StringReader(_input);
 1491                _position = index.Value;
 92
 93                // Skip to searched index
 430694                for (var i = 0; i < index; i++)
 213995                {
 213996                    _reader.Read();
 213997                }
 1498            }
 99
 13369100            _previous = _current;
 13369101            var character = _reader.Read();
 13369102            _current = (char)character;
 103
 13369104            if (character != -1)
 13209105            {
 13209106                _position++;
 13209107            }
 13369108        }
 109
 110        private void Expected(string expected)
 3111        {
 3112            throw new InvalidPluginExecutionException($"{expected} expected after '{_previous}' at position {_position},
 113        }
 114
 115        private bool IsEof()
 11701116        {
 11701117            return _current == '\uffff';
 11701118        }
 119
 120        private void SkipWhiteSpace()
 4168121        {
 6020122            while(char.IsWhiteSpace(_current) && !IsEof()) {
 926123                GetChar();
 926124            }
 4168125        }
 126
 127        private void Match (char c)
 1029128        {
 1029129            if (_current != c)
 2130            {
 2131                Expected(c.ToString(CultureInfo.InvariantCulture));
 0132            }
 133
 1027134            GetChar();
 1027135            SkipWhiteSpace();
 1027136        }
 137
 138        private string GetName()
 523139        {
 523140            SkipWhiteSpace();
 141
 524142            if (!char.IsLetter(_current)) {
 1143                Expected($"Identifier");
 0144            }
 145
 522146            var name = string.Empty;
 147
 7386148            while (char.IsLetterOrDigit(_current) && !IsEof()) {
 3432149                name += _current;
 3432150                GetChar();
 3432151            }
 152
 522153            SkipWhiteSpace();
 522154            return name;
 522155        }
 156
 157        private List<ValueExpression> Expression(char[] terminators, Dictionary<string, ValueExpression> formulaArgs)
 443158        {
 443159            var returnValue = new List<ValueExpression>();
 160
 161            do
 963162            {
 1223163                if (_current == ',') {
 260164                    GetChar();
 260165                }
 703166                else if (_current == '"' || _current == '\'')
 313167                {
 313168                    var delimiter = _current;
 313169                    var stringConstant = string.Empty;
 170
 171                    // Skip opening quote
 313172                    GetChar();
 173
 174                    // Allow to escape quotes by backslashes
 7049175                    while ((_current != delimiter || _previous == '\\') && !IsEof())
 6736176                    {
 6736177                        stringConstant += _current;
 6736178                        GetChar();
 6736179                    }
 180
 181                    // Skip closing quote
 313182                    GetChar();
 313183                    returnValue.Add(new ValueExpression(stringConstant, stringConstant));
 313184                }
 390185                else if (char.IsDigit(_current) || _current == '-')
 68186                {
 68187                    var digit = 0;
 68188                    var fractionalPart = 0;
 68189                    var processingFractionalPart = false;
 190
 191                    // Multiply by -1 for negative numbers
 68192                    var multiplicator = 1;
 193
 194                    do
 153195                    {
 153196                        if (_current == '-')
 3197                        {
 3198                            multiplicator = -1;
 3199                        }
 150200                        else if (_current != '.')
 121201                        {
 121202                            if (processingFractionalPart)
 34203                            {
 34204                                fractionalPart = fractionalPart * 10 + int.Parse(_current.ToString(CultureInfo.Invariant
 34205                            }
 206                            else
 87207                            {
 87208                                digit = digit * 10 + int.Parse(_current.ToString(CultureInfo.InvariantCulture));
 87209                            }
 121210                        }
 211                        else
 29212                        {
 29213                            processingFractionalPart = true;
 29214                        }
 215
 153216                        GetChar();
 306217                    } while ((char.IsDigit(_current) || _current == '.') && !IsEof());
 218
 68219                    switch(_current)
 220                    {
 221                        case 'd':
 15222                            double doubleValue = multiplicator * (digit + fractionalPart / Math.Pow(10, (fractionalPart.
 15223                            returnValue.Add(new ValueExpression(doubleValue.ToString(CultureInfo.InvariantCulture), doub
 15224                            GetChar();
 15225                            break;
 226                        case 'm':
 15227                            decimal decimalValue = multiplicator * (digit + fractionalPart / (decimal) Math.Pow(10, (fra
 15228                            returnValue.Add(new ValueExpression(decimalValue.ToString(CultureInfo.InvariantCulture), dec
 15229                            GetChar();
 15230                            break;
 231                        default:
 38232                            if (processingFractionalPart)
 1233                            {
 1234                                throw new InvalidDataException("For defining numbers with fractional parts, please appen
 235                            }
 37236                            var value = digit * multiplicator;
 237
 37238                            returnValue.Add(new ValueExpression(value.ToString(CultureInfo.InvariantCulture), value));
 37239                            break;
 240                    }
 241
 67242                }
 322243                else if (terminators.Contains(_current))
 19244                {
 245                    // Parameterless function or empty array encountered
 19246                }
 247                // The first char of a function must not be a digit
 248                else
 303249                {
 303250                    var value = Formula(formulaArgs);
 251
 302252                    if (value != null)
 298253                    {
 298254                        returnValue.Add(value);
 298255                    }
 302256                }
 257
 961258                SkipWhiteSpace();
 1922259            } while (!terminators.Contains(_current) && !IsEof());
 260
 441261            return returnValue;
 441262        }
 263
 264        private ValueExpression ApplyExpression (string name, List<ValueExpression> parameters, Dictionary<string, Value
 385265        {
 386266            if (!_handlers.ContainsKey(name)) {
 1267                throw new InvalidPluginExecutionException($"Function {name} is not known!");
 268            }
 269
 270            // In this case we're only stepping through in the initial interpreting of the lambda
 419271            if (formulaArgs != null && formulaArgs.Any(a => a.Value == null))
 11272            {
 11273                return new ValueExpression(null);
 274            }
 275
 373276            var lazyExecution = new Lazy<ValueExpression>(() =>
 737277            {
 737278                _tracing.Trace($"Processing handler {name}");
 737279                var result = _handlers[name](_primary, _service, _tracing, _organizationConfig, parameters);
 731280                _tracing.Trace($"Successfully processed handler {name}");
 373281
 731282                return result;
 731283            });
 284
 373285            return new ValueExpression(lazyExecution);
 384286        }
 287
 288        private ValueExpression Formula(Dictionary<string, ValueExpression> args)
 564289        {
 564290            SkipWhiteSpace();
 291
 583292            if (_current == '[') {
 19293                Match('[');
 19294                var arrayParameters = Expression(new[] { ']' }, args);
 19295                Match(']');
 296
 19297                return ApplyExpression("Array", arrayParameters);
 298            }
 545299            else if(_current == '(')
 4300            {
 301                // Match arrow functions in style of (param) => Convert(param)
 4302                Match('(');
 303
 4304                var variableNames = new List<string>();
 305
 306                do
 7307                {
 7308                    SkipWhiteSpace();
 7309                    variableNames.Add(GetName());
 7310                    SkipWhiteSpace();
 311
 7312                    if (_current == ',')
 3313                    {
 3314                        GetChar();
 3315                    }
 14316                } while (_current != ')');
 317
 318                // Initialize variables as null
 18319                var formulaArgs = variableNames.ToDictionary(n => n, v => (ValueExpression) null);
 4320                Match(')');
 321
 4322                var usedReservedWords = variableNames
 11323                    .Where(n => new List<string> { "true", "false", "null" }.Concat(_handlers.Keys).Contains(n))
 4324                    .ToList();
 325
 4326                if (usedReservedWords.Count > 0)
 0327                {
 0328                    throw new InvalidPluginExecutionException($"Your variable names {string.Join(", ", usedReservedWords
 329                }
 330
 4331                SkipWhiteSpace();
 4332                Match('=');
 4333                Match('>');
 4334                SkipWhiteSpace();
 335
 4336                var lambdaPosition = this._position - 1;
 337
 4338                var lazyExecution = new Func<List<ValueExpression>, ValueExpression>((lambdaArgs) =>
 11339                {
 11340                    var currentIndex = this._position;
 11341                    GetChar(lambdaPosition);
 4342
 11343                    var arguments = formulaArgs.ToList();
 48344                    for (var i = 0; i < lambdaArgs.Count; i++) {
 14345                        if (i < formulaArgs.Count)
 14346                        {
 14347                            var parameterName = arguments[i].Key;
 14348                            formulaArgs[parameterName] = lambdaArgs[i];
 14349                        }
 14350                    }
 4351
 11352                    var result = Formula(formulaArgs);
 11353                    GetChar(currentIndex - 1);
 4354
 11355                    return result;
 11356                });
 357
 358                // Run only for skipping the formula part
 4359                Formula(formulaArgs);
 360
 4361                return new ValueExpression(lazyExecution, formulaArgs);
 362            }
 541363            else if (_current == '{')
 60364            {
 60365                Match('{');
 60366                var dictionary = new Dictionary<string, object>();
 60367                var firstRunPassed = false;
 368
 369                do
 90370                {
 90371                    SkipWhiteSpace();
 372
 90373                    if (firstRunPassed)
 30374                    {
 30375                        Match(',');
 30376                        SkipWhiteSpace();
 30377                    }
 378                    else
 60379                    {
 60380                        firstRunPassed = true;
 60381                    }
 382
 90383                    var name = GetName();
 384
 89385                    SkipWhiteSpace();
 89386                    Match(':');
 89387                    SkipWhiteSpace();
 388
 89389                    dictionary[name] = Formula(args)?.Value;
 390
 89391                    SkipWhiteSpace();
 178392                } while (_current != '}');
 393
 59394                Match('}');
 395
 148396                return new ValueExpression(string.Join(", ", dictionary.Select(p => $"{p.Key}: {p.Value}")), dictionary)
 397            }
 481398            else if (char.IsDigit(_current) || _current == '"' || _current == '\'' || _current == '-')
 55399            {
 400                // This is only called in object initializers / dictionaries. Only one value should be entered here
 55401                return Expression(new[] { '}', ',' }, args).First();
 402            }
 426403            else {
 426404                var name = GetName();
 405
 426406                if (args != null && args.ContainsKey(name))
 19407                {
 19408                    return args[name];
 409                }
 410
 407411                switch(name)
 412                {
 413                    case "true":
 26414                        return new ValueExpression(bool.TrueString, true);
 415                    case "false":
 10416                        return new ValueExpression(bool.FalseString, false);
 417                    case "null":
 1418                        return new ValueExpression( null );
 419                    default:
 370420                        Match('(');
 369421                        var parameters = Expression(new[] { ')' }, args);
 367422                        Match(')');
 423
 366424                        return ApplyExpression(name, parameters, args);
 425                }
 426            }
 558427        }
 428
 429        public string Produce()
 162430        {
 162431            _tracing.Trace($"Initiating interpreter");
 432
 163433            if (string.IsNullOrWhiteSpace(_input)) {
 1434                _tracing.Trace("No formula passed, exiting");
 1435                return string.Empty;
 436            }
 437
 161438            var output = Formula(new Dictionary<string, ValueExpression> { });
 439
 156440            return output?.Text;
 151441        }
 442    }
 443}