/// <summary> /// Adds the (not yet resolved) attributes of the specified entity to the list. /// Throws an error if any attribute has children (must be a flat structure). /// /// Returns true if the entity is a repeating entity. /// </summary> private bool AddAttributes(List <ResolvedValue> attributes, string parentName, Entity entity, string entityName = "") { string fullName; if (parentName != null) { fullName = parentName + "." + entity.Name; } else { fullName = entity.Name; } if (entityName.Length == 0) { entityName = entity.Name; } if (entity.Type == Entity.Types.PARENT || entity.Type == Entity.Types.ARRAY || entity.Type == Entity.Types.REPEAT) { if (attributes.Count == 0 && entity.Type == Entity.Types.PARENT) { foreach (var childName in entity.ChildOrder) { Entity child = entity.ChildEntities[childName]; AddAttributes(attributes, fullName, child); } } else if (attributes.Count == 0 && entity.Type == Entity.Types.REPEAT) { return(true); } else { throw new Exception($"Cannot output attribute '{entity.Name}' in SQL or CSV format as it has children. " + "Only flat structures can be output in these formats."); } } else if (entity.Type == Entity.Types.REF) { Entity refEntity; try { refEntity = this.resolver.FindEntity(entity.Value); } catch (Exception) { throw new Exception($"Attribute '{entityName}' references unknown entity '{entity.Value}'"); } AddAttributes(attributes, null, refEntity, entityName); } else { var attribute = new ResolvedValue(fullName, entity.Type, entity.Value); attributes.Add(attribute); } return(false); }
public void TestResolveRef() { string yaml = @" entity: test: STR, This is a test myref: REF, test alias: myalias: test "; var apiService = this.yamlParser.Load(yaml); this.resolver.Init(apiService); var entity = this.resolver.FindEntity("myref"); Assert.NotNull(entity); var resolving = new ResolvedValue("test", entity.Type, entity.Value); this.resolver.Resolve(resolving, new Cache()); Assert.Equal("This is a test", resolving.Value); var entity2 = this.resolver.FindEntity("myalias"); Assert.NotNull(entity2); var resolving2 = new ResolvedValue("test2", entity2.Type, entity2.Value); this.resolver.Resolve(resolving2, new Cache()); Assert.Equal("This is a test", resolving2.Value); }
private ResolvedValue GetRefValue(string refName, Cache cache, string parent, IResolver resolver, IFormatter formatter = null) { if (refName.Length > 1 && refName[0] == '~') { refName = refName.Substring(1); } string fullRefName = parent + "." + refName; Entity refAttrib; try { refAttrib = resolver.FindEntity(fullRefName); var refValue = new ResolvedValue(fullRefName, refAttrib.Type, refAttrib.Value); try { resolver.Resolve(refValue, cache); } catch (Exception e) { throw new Exception($"references unresolvable attribute '{refName}': {e.Message}"); } return(refValue); } catch (Exception) { // Allow an unknown attribute if it's in the cache (might be a temporary attribute created by a mod) try { return(cache.GetValue(fullRefName)); } catch { // do nothing } if (formatter != null) { // Might be a complete entity rather than an attribute Entity refEntity; try { refEntity = resolver.FindEntity(refName); // The value is the entity in JSON format string strValue = formatter.EntityToJson(refEntity, cache); var refValue = new ResolvedValue("", Entity.Types.OBJ, strValue); refValue.SetValue(strValue); return(refValue); } catch (Exception) { throw new Exception($"references unknown attribute or entity '{refName}'"); } } throw new Exception($"references unknown attribute '{refName}'"); } }
/// <summary> /// Resolves an unresolved value and adds it to the cache. /// If any other values need to be resolved before this one /// (dependencies) then they are also resolved and added to /// the cache. /// /// The value and any dependencies are added to the cache /// once resolved. The cache provides a consistent view with /// consistent genders across a complete entity. When resolving /// a new entity pass in a new cache. /// </summary> public void Resolve(ResolvedValue resolving, Cache cache, IFormatter formatter = null) { if (formatter != null) { this.formatter = formatter; } try { ResolveValue(resolving, cache); } catch (Exception e) { throw new Exception($"Cannot resolve {resolving.Name}: {e.Message}"); } }
/// <summary> /// Resolve all mods and add to cache (ignore special cases) /// </summary> private void addResponseMods(Cache cache, Dictionary <string, string> mods) { foreach (var mod in mods) { ResolvedValue resolving; if (!mod.Key.Equals("*") && mod.Value != null) { if (mod.Value[0] == '~') { // Try to retrieve modded value from cache string findName = mod.Value.Substring(1); var refEntity = this.resolver.FindEntity(findName); resolving = new ResolvedValue(findName, refEntity.Type, refEntity.Value); this.resolver.Resolve(resolving, cache, this.formatter); // Also add to cache using mod key name cache.AddResolved(mod.Key, resolving.Value); } else { // Mod value is a literal string value; if (mod.Value[0] == '"' && mod.Value[mod.Value.Length - 1] == '"') { value = mod.Value.Substring(1, mod.Value.Length - 2); } else { value = mod.Value; // Still needs resolving if it contains funcs //if (value.Contains("func.")) //{ // resolving = new ResolvedValue(mod.Key, Entity.Types.STR, value); // this.resolver.Resolve(resolving, cache, this.formatter); // return; //} } cache.AddResolved(mod.Key, value); } } } }
public void TestResolveCombinedFuncs() { /// Note: Individual funcs are tested by FuncsTests string yaml = @" entity: test: ""STR, func.pick(me)-func.pick(me2) : func.pick(me3)"" "; var apiService = this.yamlParser.Load(yaml); this.resolver.Init(apiService); var entity = this.resolver.FindEntity("test"); Assert.NotNull(entity); var resolving = new ResolvedValue("test", entity.Type, entity.Value); this.resolver.Resolve(resolving, new Cache()); Assert.Equal("me-me2 : me3", resolving.Value); }
/// <summary> /// Outputs the entity in JSON format after resolving all values /// </summary> private string ToJson(string parentName, Entity entity, Cache cache, Dictionary <string, string> mods, int nestedLevel, string knownName = "") { if (knownName.Length == 0) { knownName = entity.Name; } string fullName; if (parentName != null) { int parentLast = parentName.LastIndexOf('.'); if (parentLast != -1 && parentName[parentLast + 1] == '~' && entity.Type == Entity.Types.PARENT) { fullName = parentName; } else { fullName = parentName + "." + entity.Name; } } else { fullName = entity.Name; } // Get mod string mod = null; bool modAll = false; if (mods != null) { if (mods.TryGetValue(fullName, out mod)) { if (mod == null) { // Attribute should be ignored return(""); } } else if (mods.TryGetValue("*", out mod)) { modAll = true; } } if (nestedLevel > 500 && parentName != null) { throw new Exception($"Cannot resolve entity '{parentName}' as it contains a circular reference"); } StringBuilder sb = new StringBuilder(); bool isArrayChild = knownName.StartsWith("~") || knownName.StartsWith('#'); if (entity.Type == Entity.Types.PARENT | entity.Type == Entity.Types.ARRAY) { if (parentName != null && !isArrayChild) { sb.Append($"{entity.Name}: "); } if (entity.Type == Entity.Types.PARENT) { sb.Append("{"); } else { sb.Append("["); } bool isFirst = true; foreach (var entityName in entity.ChildOrder) { var toJsonStr = ToJson(fullName, entity.ChildEntities[entityName], cache, mods, nestedLevel + 1); if (toJsonStr.Length > 0) { if (!isFirst) { sb.Append(", "); } else { isFirst = false; } sb.Append(toJsonStr); } } if (entity.Type == Entity.Types.PARENT) { sb.Append("}"); } else { sb.Append("]"); } } else if (entity.Type == Entity.Types.REF) { Entity refEntity; try { refEntity = this.resolver.FindEntity(entity.Value); } catch (Exception e) { throw new Exception($"Attribute '{knownName}' has bad reference '{entity.Value}': {e.Message}"); } string refParent; if (modAll) { refParent = fullName; } else { refParent = null; } if (refEntity.Type == Entity.Types.REPEAT) { sb.Append($"{ToJson(refParent, refEntity, cache, mods, nestedLevel + 1, "#" + knownName)}"); } else { sb.Append($"{ToJson(refParent, refEntity, cache, mods, nestedLevel + 1, knownName)}"); } } else if (entity.Type == Entity.Types.REPEAT) { if (knownName.StartsWith('#')) { sb.Append($"{knownName.Substring(1)}: "); } Entity refEntity; try { refEntity = this.resolver.FindEntity(entity.Value); } catch (Exception) { throw new Exception($"Attribute '{knownName}' references unknown entity '{entity.Value}'"); } string modParent; int repeatCount; if (modAll && mod[0] == '~') { modParent = parentName + ".~"; string childGroup = mod.Substring(1) + "." + modParent; repeatCount = 0; while (true) { if (!cache.HasValue(childGroup + (repeatCount + 1))) { break; } repeatCount++; } } else { // Choose a random repeat count in the range specified modParent = null; repeatCount = random.Next(entity.Min, entity.Max + 1); } sb.Append("["); for (int i = 0; i < repeatCount; i++) { if (i > 0) { sb.Append(", "); } if (modParent == null) { // Use a clean cache for each instance (so they are not related) sb.Append($"{ToJson(null, refEntity, new Cache(), mods, nestedLevel + 1, knownName)}"); } else { // Getting data from request which is in cache sb.Append($"{ToJson(modParent + (i + 1), refEntity, cache, mods, nestedLevel + 1, knownName)}"); } } sb.Append("]"); } else { if (!isArrayChild) { sb.Append($"{knownName}: "); } ResolvedValue resolving = null; if (modAll) { // Apply mod prefix to attribute name. // e.g. if *=~request, employee.id becomes ~request.employee.id mod += "." + fullName; if (mod[0] == '~') { mod = mod.Substring(1); } // Retrieve from cache try { resolving = cache.GetValue(mod); } catch (Exception e) { // Not an error if request or query value not found if (!mod.StartsWith("request.") && !mod.StartsWith("query.")) { throw e; } resolving = null; } } if (resolving == null) { // No mod applied resolving = new ResolvedValue(fullName, entity.Type, entity.Value); this.resolver.Resolve(resolving, cache, this); } sb.Append(resolving.GetValue(false)); } return(sb.ToString()); }
/// <summary> /// Evaluates a single function. Function name must be a known /// function or an exception will be thrown. /// /// The arg list is resolved (as a whole) before being split into /// separate arguments so it may contain embedded func.xxx() calls. /// </summary> private string eval(string funcName, string argList, Cache cache, string parentName, out Sample.Genders gender) { string called = $"func.{funcName}({argList})"; // Validate function name Funcs.FuncNames func; try { func = (Funcs.FuncNames)Enum.Parse(typeof(Funcs.FuncNames), funcName.Trim(), true); } catch (Exception) { throw new Exception($"Unknown function 'func.{funcName}(...)'. Must be one of: {String.Join(", ", Enum.GetNames(typeof(Funcs.FuncNames)))}"); } // Resolve arg list (in case funcs are being passed as args to other funcs) but don't add // to cache as we want them to evaluate differently each time (if they're random). string nocacheParent; int sep = parentName.IndexOf('.'); if (sep == -1) { nocacheParent = parentName + ".NOCACHE"; } else { nocacheParent = parentName.Substring(0, sep) + ".NOCACHE"; } var resolvingArgs = new ResolvedValue(nocacheParent, Entity.Types.STR, argList); ResolveValue(resolvingArgs, cache); gender = resolvingArgs.Gender; // Replace escaped brackets and split up arguments var args = SplitArgs(resolvingArgs.Value.Replace("\\(", "(").Replace("\\)", ")").Trim()); // Call the relevant function string result; Sample.Genders newGender; switch (func) { case Funcs.FuncNames.DATE: return(this.funcs.FuncDate(called, args)); case Funcs.FuncNames.GEN: return(this.funcs.FuncGen(called, args)); case Funcs.FuncNames.IF: return(this.funcs.FuncIf(called, args, cache, parentName, this, this.formatter)); case Funcs.FuncNames.MATH: return(this.funcs.FuncMath(called, args, cache, parentName, this)); case Funcs.FuncNames.NUM: return(this.funcs.FuncNum(called, args, cache, parentName, this)); case Funcs.FuncNames.PICK: return(this.funcs.FuncPick(called, args)); case Funcs.FuncNames.RAND: return(this.funcs.FuncRand(called, args)); case Funcs.FuncNames.REF: result = this.funcs.FuncRef(called, args, cache, parentName, this, out newGender); if (newGender != Sample.Genders.NEUTRAL) { gender = newGender; } return(result); case Funcs.FuncNames.SAMPLE: result = this.funcs.FuncSample(called, args, cache, parentName, this, out newGender); if (newGender != Sample.Genders.NEUTRAL) { gender = newGender; } return(result); case Funcs.FuncNames.SPLIT: return(this.funcs.FuncSplit(called, args, cache, parentName, this)); case Funcs.FuncNames.STR: return(this.funcs.FuncStr(called, args)); case Funcs.FuncNames.TIME: return(this.funcs.FuncTime(called, args)); default: throw new Exception($"Missing function name: {func}"); } }
/// <summary> /// Takes a literal string and looks for any embedded /// func.xxx(...) calls. Each embedded function is resolved /// and the result is included in the returned string. /// /// Throws an exception if the value cannot be resolved /// </summary> private void ResolveValue(ResolvedValue resolving, Cache cache) { // See if it's already been resolved var resolved = cache.GetResolved(resolving.Name); if (resolved != null) { resolving.SetValue(resolved.Value, resolved.Gender); return; } if (resolving.UnresolvedValue == null) { throw new Exception("Unresolved value cannot be null"); } // If value is already concrete it's resolved string value = resolving.UnresolvedValue; if (!value.Contains("func.")) { resolving.SetValue(value); cache.SetResolved(resolving); return; } StringBuilder resolvedStr = new StringBuilder(); Sample.Genders gender = Sample.Genders.NEUTRAL; int idx = 0; while (idx < value.Length) { int funcPos = value.IndexOf("func.", idx, StringComparison.CurrentCultureIgnoreCase); if (funcPos == -1) { resolvedStr.Append(value.Substring(idx)); break; } resolvedStr.Append(value.Substring(idx, funcPos - idx)); // Find function name idx = funcPos + 5; int argStart = value.IndexOf('(', idx); if (argStart == -1) { throw new Exception($"Function 'func.{value.Substring(idx)}' has missing opening bracket"); } string funcName = value.Substring(idx, argStart - idx).Trim(); // Find matching close bracket argStart++; int argEnd = argStart; int openBrackets = 0; while (true) { if (argEnd == value.Length) { throw new Exception($"Function 'func.{value.Substring(idx)}' has missing closing bracket"); } // Ignore escaped brackets if (value[argEnd] == '\\' && argEnd < value.Length - 1 && (value[argEnd + 1] == '(' || value[argEnd + 1] == ')')) { argEnd += 2; continue; } if (value[argEnd] == '(') { openBrackets++; } else if (value[argEnd] == ')') { if (openBrackets == 0) { break; } else { openBrackets--; } } argEnd++; } string argList = value.Substring(argStart, argEnd - argStart); string parentName; int pos = resolving.Name == null? -1 : resolving.Name.LastIndexOf('.'); if (pos == -1) { parentName = resolving.Name; } else { parentName = resolving.Name.Substring(0, pos); } resolvedStr.Append(eval(funcName, argList, cache, parentName, out var newGender)); if (newGender != Sample.Genders.NEUTRAL) { gender = newGender; } idx = argEnd + 1; } resolving.SetValue(resolvedStr.ToString(), gender); cache.SetResolved(resolving); }
/// <summary> /// Sample function /// /// The sample function has a circular dependency on the resolver (so we can /// have samples based on the gender of other fields which may not be resolved /// yet) so we have to pass the resolver in as a parameter. /// </summary> public string FuncSample(string called, string[] args, Cache cache, string parent, IResolver resolver, out Sample.Genders gender) { string help = "Use func.sample(--help) for help."; if (args.Length == 1 && args[0].Equals("--help", StringComparison.CurrentCultureIgnoreCase)) { // Show usage string usage = "Usage: func.sample(arg1, [arg2]) where arg1 is the name of the samples file to use and arg2 is " + "either another attribute to take the gender from, or M or F to use a fixed gender."; throw new Exception(usage); } if (args.Length < 1 || args.Length > 2) { throw new Exception($"{called} has bad number of arguments. {help}"); } if (args[0].Length == 0) { throw new Exception($"{called} has empty argument. {help}"); } gender = Sample.Genders.NEUTRAL; if (args.Length == 2) { if (args[1].Equals("M", StringComparison.CurrentCultureIgnoreCase)) { gender = Sample.Genders.MALE; } else if (args[1].Equals("F", StringComparison.CurrentCultureIgnoreCase)) { gender = Sample.Genders.FEMALE; } else { // Inherit gender from referenced attribute. string refName; if (args[1].Length > 1 && args[1][0] == '~') { refName = parent + "." + args[1].Substring(1); } else { refName = parent + "." + args[1]; } Entity refAttrib; try { refAttrib = resolver.FindEntity(refName); } catch (Exception) { throw new Exception($"{called} references unknown attribute '{refName}'"); } var refValue = new ResolvedValue(refName, refAttrib.Type, refAttrib.Value); try { resolver.Resolve(refValue, cache); } catch (Exception e) { throw new Exception($"{called} references unresolvable attribute '{refName}': {e.Message}"); } gender = refValue.Gender; } } var samples = resolver.GetSamples(args[0]); var sample = samples.Pick(gender); gender = sample.Gender; return(sample.Value); }
/// <see cref="JsonPlusValue.ToString(int, int)"/> public string ToString(int indent, int indentSize) { return(ResolvedValue.ToString(indent, indentSize)); }
/// <summary> /// Returns a string representation of this <see cref="JsonPlusSubstitution"/>. /// </summary> /// <returns>A string representation of this <see cref="JsonPlusSubstitution"/>.</returns> public override string ToString() { return(ResolvedValue.ToString(0, 2)); }
/// <see cref="IJsonPlusNode.GetValue()"/> public JsonPlusValue GetValue() { return(ResolvedValue?.GetValue() ?? new JsonPlusValue(Parent)); }
/// <see cref="IJsonPlusNode.GetObject()"/> public JsonPlusObject GetObject() { return(ResolvedValue?.GetObject() ?? new JsonPlusObject(Parent)); }
/// <see cref="IJsonPlusNode.GetArray()"/> public List <IJsonPlusNode> GetArray() { return(ResolvedValue?.GetArray() ?? new List <IJsonPlusNode>()); }
/// <see cref="IJsonPlusNode.GetString()"/> public string GetString() { return(ResolvedValue?.GetString()); }