From b2bd4f674660d16029f3d83c5d92afc75e492fca Mon Sep 17 00:00:00 2001 From: jwaliszko Date: Sat, 12 Aug 2017 17:02:27 +0200 Subject: [PATCH] Naming collisions mechanism extended. Registration of methods within the model context narrowed down to the essential ones (+ redundant warning removed). --- .../ValidatorsTest.cs | 118 ++++++++++++++++-- .../Caching/ProcessStorage.cs | 5 +- .../Helper.cs | 2 +- .../Validators/ExpressiveValidator.cs | 47 ++++++- src/ExpressiveAnnotations.Tests/ParserTest.cs | 20 ++- src/ExpressiveAnnotations/Analysis/Parser.cs | 15 +++ src/expressive.annotations.validate.js | 33 ++--- src/expressive.annotations.validate.min.js | 2 +- src/expressive.annotations.validate.test.js | 79 ++++++------ src/form.tests.harness.html | 1 + src/form.tests.harness.latestdeps.html | 1 + src/form.tests.html | 1 + 12 files changed, 244 insertions(+), 80 deletions(-) diff --git a/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/ValidatorsTest.cs b/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/ValidatorsTest.cs index e0f8ab1..09e13f4 100644 --- a/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/ValidatorsTest.cs +++ b/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/ValidatorsTest.cs @@ -5,6 +5,7 @@ using System.Web; using ExpressiveAnnotations.Analysis; using ExpressiveAnnotations.Attributes; +using ExpressiveAnnotations.Functions; using ExpressiveAnnotations.MvcUnobtrusive.Validators; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; @@ -381,7 +382,7 @@ public void verify_that_culture_change_affects_message_sent_to_client() } [Fact] - public void possible_naming_colission_at_client_side_are_detected() + public void segments_collisions_are_detected() { // A.B.C = 0 {"A":{"B":{"C":0}}} // A.D = true {"A":{"D":true}} @@ -393,29 +394,33 @@ public void possible_naming_colission_at_client_side_are_detected() string name; int level; - Assert.False(Helper.SegmentsCollide(new string[0], new string[0], out name, out level)); - Assert.False(Helper.SegmentsCollide(new[] {"A"}, new string[0], out name, out level)); - Assert.False(Helper.SegmentsCollide(new string[0], new[] {"A"}, out name, out level)); - Assert.False(Helper.SegmentsCollide(new[] {"A"}, new[] {"B"}, out name, out level)); - Assert.False(Helper.SegmentsCollide(new[] {"A.A"}, new[] {"A.B"}, out name, out level)); - Assert.False(Helper.SegmentsCollide(new[] {"A.B.C"}, new[] {"A.B.D"}, out name, out level)); - Assert.False(Helper.SegmentsCollide(new[] {"A.B.C", "A.B.E"}, new[] {"B.B", "B.C", "B.E"}, out name, out level)); + Assert.False(new string[0].SegmentsCollide(new string[0], out name, out level)); + Assert.False(new[] {"A"}.SegmentsCollide(new string[0], out name, out level)); + Assert.False(new string[0].SegmentsCollide(new[] {"A"}, out name, out level)); + Assert.False(new[] {"A"}.SegmentsCollide(new[] {"B"}, out name, out level)); + Assert.False(new[] {"A.A"}.SegmentsCollide(new[] {"A.B"}, out name, out level)); + Assert.False(new[] {"A.B.C"}.SegmentsCollide(new[] {"A.B.D"}, out name, out level)); + Assert.False(new[] {"A.B.C", "A.B.E"}.SegmentsCollide(new[] {"B.B", "B.C", "B.E"}, out name, out level)); Assert.Equal(null, name); Assert.Equal(level, -1); - Assert.True(Helper.SegmentsCollide(new[] {"A"}, new[] {"A"}, out name, out level)); + Assert.True(new[] {"A"}.SegmentsCollide(new[] {"A"}, out name, out level)); Assert.Equal("A", name); Assert.Equal(level, 0); - Assert.True(Helper.SegmentsCollide(new[] {"A.B"}, new[] {"A.B"}, out name, out level)); + Assert.True(new[] {"A.B"}.SegmentsCollide(new[] {"A.B"}, out name, out level)); Assert.Equal("B", name); Assert.Equal(level, 1); - Assert.True(Helper.SegmentsCollide(new[] {"A.B.C"}, new[] {"A.B"}, out name, out level)); + Assert.True(new[] {"A.B.C"}.SegmentsCollide(new[] {"A.B"}, out name, out level)); Assert.Equal("B", name); Assert.Equal(level, 1); + } + [Fact] + public void naming_colissions_at_client_side_are_detected() + { var model = new Model(); var metadata = GetModelMetadata(model, m => m.Value); var controllerContext = GetControllerContext(); @@ -437,6 +442,71 @@ public void possible_naming_colission_at_client_side_are_detected() Assert.Equal( "Naming collisions cannot be accepted by client-side - Value part at level 0 is ambiguous.", e.InnerException.Message); + + Toolchain.Instance.AddFunction("Number", (int n) => n); + + var cfm = new CollisionFieldModel(); + metadata = GetModelMetadata(cfm, m => m.Number); + + e = Assert.Throws(() => new AssertThatValidator(metadata, controllerContext, new AssertThatAttribute("Number(Number.One) == 0"))); + Assert.Equal( + "AssertThatValidator: validation applied to Number field failed.", + e.Message); + Assert.IsType(e.InnerException); + Assert.Equal( + "Naming collisions cannot be accepted by client-side - method Number(...) is colliding with Number.One field identifier.", + e.InnerException.Message); + + e = Assert.Throws(() => new RequiredIfValidator(metadata, controllerContext, new RequiredIfAttribute("Number(Number.One) == 0"))); + Assert.Equal( + "RequiredIfValidator: validation applied to Number field failed.", + e.Message); + Assert.IsType(e.InnerException); + Assert.Equal( + "Naming collisions cannot be accepted by client-side - method Number(...) is colliding with Number.One field identifier.", + e.InnerException.Message); + + var ccm = new CollisionConstModel(); + metadata = GetModelMetadata(ccm, m => m.Value); + + e = Assert.Throws(() => new AssertThatValidator(metadata, controllerContext, new AssertThatAttribute("Number(Number) == 0"))); + Assert.Equal( + "AssertThatValidator: validation applied to Value field failed.", + e.Message); + Assert.IsType(e.InnerException); + Assert.Equal( + "Naming collisions cannot be accepted by client-side - method Number(...) is colliding with Number const identifier.", + e.InnerException.Message); + + e = Assert.Throws(() => new RequiredIfValidator(metadata, controllerContext, new RequiredIfAttribute("Number(Number) == 0"))); + Assert.Equal( + "RequiredIfValidator: validation applied to Value field failed.", + e.Message); + Assert.IsType(e.InnerException); + Assert.Equal( + "Naming collisions cannot be accepted by client-side - method Number(...) is colliding with Number const identifier.", + e.InnerException.Message); + + var cem = new CollisionEnumModel(); + metadata = GetModelMetadata(cem, m => m.Value); + + e = Assert.Throws(() => new AssertThatValidator(metadata, controllerContext, new AssertThatAttribute("Number(Number.One) == 0"))); + Assert.Equal( + "AssertThatValidator: validation applied to Value field failed.", + e.Message); + Assert.IsType(e.InnerException); + Assert.Equal( + "Naming collisions cannot be accepted by client-side - method Number(...) is colliding with Number.One enum identifier.", + e.InnerException.Message); + + e = Assert.Throws(() => new RequiredIfValidator(metadata, controllerContext, new RequiredIfAttribute("Number(Number.One) == 0"))); + Assert.Equal( + "RequiredIfValidator: validation applied to Value field failed.", + e.Message); + Assert.IsType(e.InnerException); + Assert.Equal( + "Naming collisions cannot be accepted by client-side - method Number(...) is colliding with Number.One enum identifier.", + e.InnerException.Message); } [Fact] @@ -505,6 +575,32 @@ public class MathModel } } + public class CollisionFieldModel + { + public N Number { get; set; } + + public class N + { + public int One { get; set; } + } + } + + public class CollisionConstModel + { + public int Value { get; set; } + public const int Number = 0; + } + + public class CollisionEnumModel + { + public int Value { get; set; } + + public enum Number + { + One + }; + } + public class MsgModel { [Display(Name = "_{Value}_")] diff --git a/src/ExpressiveAnnotations.MvcUnobtrusive/Caching/ProcessStorage.cs b/src/ExpressiveAnnotations.MvcUnobtrusive/Caching/ProcessStorage.cs index 84033df..e2dd284 100644 --- a/src/ExpressiveAnnotations.MvcUnobtrusive/Caching/ProcessStorage.cs +++ b/src/ExpressiveAnnotations.MvcUnobtrusive/Caching/ProcessStorage.cs @@ -26,8 +26,8 @@ public static TValue GetOrAdd(TKey key, Func valueFactory) // dele k => new Lazy( () => valueFactory(k), LazyThreadSafetyMode.ExecutionAndPublication)); - return lazyResult.Value; /* From http://bit.ly/2b8E1AS: If multiple concurrent threads try to call GetOrAdd with the same key at once, multiple Lazy objects may be - * created but these are cheap, and all but one will be thrown away. The return Lazy object will be the same across all threads, and the + return lazyResult.Value; /* From http://bit.ly/2b8E1AS: If multiple concurrent threads try to call GetOrAdd with the same key at once, multiple Lazy objects may be + * created but these are cheap, and all but one will be thrown away. The return Lazy object will be the same across all threads, and the * first one to call the Value property will run the expensive delegate method, whilst the other threads are locked, waiting for the result. */ } @@ -42,6 +42,7 @@ internal class CacheItem public IDictionary FieldsMap { get; set; } public IDictionary ConstsMap { get; set; } public IDictionary EnumsMap { get; set; } + public IEnumerable MethodsList { get; set; } public IDictionary ParsersMap { get; set; } } } diff --git a/src/ExpressiveAnnotations.MvcUnobtrusive/Helper.cs b/src/ExpressiveAnnotations.MvcUnobtrusive/Helper.cs index 5d9fe18..e36dace 100644 --- a/src/ExpressiveAnnotations.MvcUnobtrusive/Helper.cs +++ b/src/ExpressiveAnnotations.MvcUnobtrusive/Helper.cs @@ -53,7 +53,7 @@ public static string ToJson(this object data) } } - public static bool SegmentsCollide(IEnumerable listA, IEnumerablelistB, out string name, out int level) + public static bool SegmentsCollide(this IEnumerable listA, IEnumerablelistB, out string name, out int level) { Debug.Assert(listA != null); Debug.Assert(listB != null); diff --git a/src/ExpressiveAnnotations.MvcUnobtrusive/Validators/ExpressiveValidator.cs b/src/ExpressiveAnnotations.MvcUnobtrusive/Validators/ExpressiveValidator.cs index 83d4322..f625f33 100644 --- a/src/ExpressiveAnnotations.MvcUnobtrusive/Validators/ExpressiveValidator.cs +++ b/src/ExpressiveAnnotations.MvcUnobtrusive/Validators/ExpressiveValidator.cs @@ -56,6 +56,7 @@ protected ExpressiveValidator(ModelMetadata metadata, ControllerContext context, FieldsMap = fields.ToDictionary(x => x.Key, x => Helper.GetCoarseType(x.Value.Type)); ConstsMap = parser.GetConsts(); EnumsMap = parser.GetEnums(); + MethodsList = parser.GetMethods(); ParsersMap = fields .Select(kvp => new { @@ -73,7 +74,7 @@ protected ExpressiveValidator(ModelMetadata metadata, ControllerContext context, ParsersMap.Add(new KeyValuePair(metadata.PropertyName, valueParser.ParserName)); } - AssertNoNamingCollisionsAtCorrespondingSegments(); + AssertClientSideCompatibility(); attribute.Compile(metadata.ContainerType); // compile expressions in attributes (to be cached for subsequent invocations) return new CacheItem @@ -81,6 +82,7 @@ protected ExpressiveValidator(ModelMetadata metadata, ControllerContext context, FieldsMap = FieldsMap, ConstsMap = ConstsMap, EnumsMap = EnumsMap, + MethodsList = MethodsList, ParsersMap = ParsersMap }; }); @@ -88,6 +90,7 @@ protected ExpressiveValidator(ModelMetadata metadata, ControllerContext context, FieldsMap = item.FieldsMap; ConstsMap = item.ConstsMap; EnumsMap = item.EnumsMap; + MethodsList = item.MethodsList; ParsersMap = item.ParsersMap; Expression = attribute.Expression; @@ -138,6 +141,11 @@ protected ExpressiveValidator(ModelMetadata metadata, ControllerContext context, /// protected IDictionary EnumsMap { get; private set; } + /// + /// Gets names of methods extracted from specified expression within given context. + /// + protected IEnumerable MethodsList { get; private set; } + /// /// Gets attribute strong identifier - attribute type identifier concatenated with annotated field identifier. /// @@ -182,6 +190,9 @@ protected ModelClientValidationRule GetBasicRule(string type) Debug.Assert(EnumsMap != null); if (EnumsMap.Any()) rule.ValidationParameters.Add("enumsmap", EnumsMap.ToJson()); + Debug.Assert(MethodsList != null); + if (MethodsList.Any()) + rule.ValidationParameters.Add("methodslist", MethodsList.ToJson()); Debug.Assert(ParsersMap != null); if (ParsersMap.Any()) rule.ValidationParameters.Add("parsersmap", ParsersMap.ToJson()); @@ -224,16 +235,42 @@ private void ResetSuffixAllocation() RequestStorage.Remove(AttributeWeakId); } + private void AssertClientSideCompatibility() // verify client-side compatibility of current expression + { + AssertNoNamingCollisionsAtCorrespondingSegments(); + } + private void AssertNoNamingCollisionsAtCorrespondingSegments() { string name; int level; - var collision = Helper.SegmentsCollide(FieldsMap.Keys, ConstsMap.Keys, out name, out level) - || Helper.SegmentsCollide(FieldsMap.Keys, EnumsMap.Keys, out name, out level) - || Helper.SegmentsCollide(ConstsMap.Keys, EnumsMap.Keys, out name, out level); + var prefix = "Naming collisions cannot be accepted by client-side"; + var collision = FieldsMap.Keys.SegmentsCollide(ConstsMap.Keys, out name, out level) + || FieldsMap.Keys.SegmentsCollide(EnumsMap.Keys, out name, out level) + || ConstsMap.Keys.SegmentsCollide(EnumsMap.Keys, out name, out level); // combination (3 2) => 3!/(2!1!) = 3 if (collision) throw new InvalidOperationException( - $"Naming collisions cannot be accepted by client-side - {name} part at level {level} is ambiguous."); + $"{prefix} - {name} part at level {level} is ambiguous."); + + // instead of extending the checks above to combination (4 2), check for collisions with methods is done separately to provide more accurate messages: + + var fields = FieldsMap.Keys.Select(x => x.Split('.').First()); + name = MethodsList.Intersect(fields).FirstOrDefault(); + if (name != null) + throw new InvalidOperationException( + $"{prefix} - method {name}(...) is colliding with {FieldsMap.Keys.First(x => x.StartsWith(name))} field identifier."); + + var consts = ConstsMap.Keys.Select(x => x.Split('.').First()); + name = MethodsList.Intersect(consts).FirstOrDefault(); + if (name != null) + throw new InvalidOperationException( + $"{prefix} - method {name}(...) is colliding with {ConstsMap.Keys.First(x => x.StartsWith(name))} const identifier."); + + var enums = EnumsMap.Keys.Select(x => x.Split('.').First()); + name = MethodsList.Intersect(enums).FirstOrDefault(); + if (name != null) + throw new InvalidOperationException( + $"{prefix} - method {name}(...) is colliding with {EnumsMap.Keys.First(x => x.StartsWith(name))} enum identifier."); } private void AssertAttribsQuantityAllowed(int count) diff --git a/src/ExpressiveAnnotations.Tests/ParserTest.cs b/src/ExpressiveAnnotations.Tests/ParserTest.cs index cf0ec9e..3935ce5 100644 --- a/src/ExpressiveAnnotations.Tests/ParserTest.cs +++ b/src/ExpressiveAnnotations.Tests/ParserTest.cs @@ -329,7 +329,7 @@ public void verify_logic_without_context() Assert.True(parser.Parse("- - -1+'a'+'b'+null+''+'c'+1+2=='-1abc12'").Invoke()); Helper.CulturalExecution(() => Assert.True(parser.Parse("1.2 + 'a' + .12=='1.2a0.12'").Invoke()), "en"); Helper.CulturalExecution(() => Assert.True(parser.Parse("1.2 + 'a' + .12=='1,2a0,12'").Invoke()), "pl"); // regional specific decimal separator - + Assert.True(parser.Parse("1 - 2 -(6 / ((2*1.5 - 1) + 1)) * -2 + 1/2/1 == 3.50").Invoke()); Assert.True(parser.Parse("-.11e-10+.11e-10==.0-.0").Invoke()); @@ -429,12 +429,14 @@ public void verify_logic_with_context() var parser = new Parser(); Toolchain.Instance.AddFunction("GetModel", () => model); Toolchain.Instance.AddFunction("GetModels", () => new[] {model}); + Toolchain.Instance.AddFunction("Number", (int n) => n); parser.RegisterToolchain(); Assert.True(parser.Parse("Chaaar == 'a'").Invoke(model)); Assert.True(parser.Parse(model.GetType(), "Number < 1").Invoke(model)); Assert.True(parser.Parse(model.GetType(), "Number == 0").Invoke(model)); + Assert.True(parser.Parse(model.GetType(), "Number(Number) == 0").Invoke(model)); Assert.True(parser.Parse(model.GetType(), "Number != null").Invoke(model)); Assert.True(parser.Parse(model.GetType(), "SubModel.Number / 2 == 0.5").Invoke(model)); @@ -521,7 +523,7 @@ public void verify_logic_with_context() (Text != 'hello world' && Date < SubModel.Date) || ( (Number >= 0 && Number < 1) - && Collection[true ? 0 : 1].Number < 0 + && Number(Collection[true ? 0 : 1].Number) < Number(IncNumber(DecNumber(0))) && PoliticalStability == Utility.Stability.High ) ) @@ -529,7 +531,7 @@ public void verify_logic_with_context() var func = parser.Parse(model.GetType(), expression); Assert.True(func(model)); - parser.GetFields()["Flag"] = null; // try to mess up with internal fields - original data should not be affected + parser.GetFields()["Flag"] = null; // try to mess up with internal items - original data should not be affected var parsedFields = parser.GetFields(); var expectedFields = new Dictionary { @@ -547,7 +549,7 @@ public void verify_logic_with_context() key => parsedFields.ContainsKey(key) && EqualityComparer.Default.Equals(expectedFields[key], parsedFields[key].Type))); - parser.GetConsts()["Const"] = null; // try to mess up with internal fields - original data should not be affected + parser.GetConsts()["Const"] = null; // try to mess up with internal items - original data should not be affected var parsedConsts = parser.GetConsts(); var expectedConsts = new Dictionary { @@ -560,7 +562,7 @@ public void verify_logic_with_context() key => parsedConsts.ContainsKey(key) && EqualityComparer.Default.Equals(expectedConsts[key], parsedConsts[key]))); - parser.GetEnums()["Enum"] = null; // try to mess up with internal fields - original data should not be affected + parser.GetEnums()["Enum"] = null; // try to mess up with internal items - original data should not be affected var parsedEnums = parser.GetEnums(); var expectedEnums = new Dictionary { @@ -572,6 +574,12 @@ public void verify_logic_with_context() key => parsedEnums.ContainsKey(key) && EqualityComparer.Default.Equals(expectedEnums[key], parsedEnums[key]))); + parser.GetMethods().ToList()[0] = null; // try to mess up with internal items - original data should not be affected + var parsedMethods = parser.GetMethods().ToList(); + var expectedMethods = new[] {"Number", "IncNumber", "DecNumber"}; + Assert.Equal(expectedMethods.Length, parsedMethods.Count); + Assert.True(!expectedMethods.Except(parsedMethods).Any()); + Assert.True(parser.Parse("Array[0] != null && Array[1] != null").Invoke(model)); Assert.True(parser.Parse("Array[0].Number + Array[0].Array[0].Number + Array[1].Number + Array[1].Array[0].Number == 0").Invoke(model)); @@ -840,7 +848,7 @@ public void verify_invalid_identifier() } [Fact] - public void verify_naming_collisions() + public void parser_properly_handles_situations_when_names_are_collided() { var parser = new Parser(); diff --git a/src/ExpressiveAnnotations/Analysis/Parser.cs b/src/ExpressiveAnnotations/Analysis/Parser.cs index 03ca597..35c2d63 100644 --- a/src/ExpressiveAnnotations/Analysis/Parser.cs +++ b/src/ExpressiveAnnotations/Analysis/Parser.cs @@ -65,6 +65,7 @@ public Parser() Fields = new Dictionary(); Consts = new Dictionary(); Enums = new Dictionary(); + Methods = new HashSet(); } private Stack TokensToProcess { get; set; } @@ -77,6 +78,7 @@ public Parser() private IDictionary Fields { get; set; } private IDictionary Consts { get; set; } private IDictionary Enums { get; set; } + private ISet Methods { get; set; } private IFunctionsProvider FuncProvider { get; set; } private IDictionary> Functions => FuncProvider == null ? new Dictionary>() : FuncProvider.GetFunctions(); @@ -255,6 +257,17 @@ public IDictionary GetEnums() return Enums.ToDictionary(x => x.Key, x => x.Value); } + /// + /// Gets names of methods extracted from specified expression within given context. + /// + /// + /// Collection containing names. + /// + public IEnumerable GetMethods() + { + return Methods.ToArray(); + } + /// /// Gets the abstract syntax tree built during parsing. /// @@ -271,6 +284,7 @@ private void Clear() Fields.Clear(); Consts.Clear(); Enums.Clear(); + Methods.Clear(); SyntaxTree = null; } @@ -749,6 +763,7 @@ private Expression ParseFuncCall(Token func) } ReadToken(); // read ")" + Methods.Add(name); return ExtractMethodExpression(name, args, func.Location); // get method call } diff --git a/src/expressive.annotations.validate.js b/src/expressive.annotations.validate.js index 837e096..50b1c5f 100644 --- a/src/expressive.annotations.validate.js +++ b/src/expressive.annotations.validate.js @@ -101,6 +101,7 @@ var } }, prep: function(message, date) { + message = typeHelper.string.tryParse(message); var lines = message.split('\n'); var stamp = date !== undefined && date !== null ? '(' + typeHelper.datetime.stamp(date) + '): ' : ''; var fline = stamp + lines.shift(); @@ -124,15 +125,12 @@ var return func.apply(this, arguments); // no exact signature match, most likely variable number of arguments is accepted }; }, - registerMethods: function(model) { - var name, body; + registerMethods: function(model, essentialMethods) { + var i, name, body; this.initialize(); - for (name in this.methods) { - if (this.methods.hasOwnProperty(name)) { - if (model.hasOwnProperty(name)) { - logger.warn(typeHelper.string.format('Skipping {0} function registration due to naming conflict (property of the same name already defined within the context).', name)); - continue; - } + for (i = 0; i < essentialMethods.length; i++) { + name = essentialMethods[i]; + if (this.methods.hasOwnProperty(name)) { // if not available, exception will be thrown later, during expression evaluation (not thrown here on purpose - too early, let the log to become more complete) body = this.methods[name]; model[name] = body; } @@ -350,7 +348,10 @@ var string: { format: function(text, params) { function makeParam(value) { - value = typeHelper.isObject(value) ? JSON.stringify(value, null, 4): value; + var replacer = function (key, value) { + return typeof value === 'function' ? 'function(...) {...}' : value; + } + value = typeHelper.isObject(value) ? JSON.stringify(value, replacer, 4): value; value = typeHelper.isString(value) ? value.replace(/\$/g, '$$$$'): value; // escape $ sign for string.replace() return value; } @@ -729,8 +730,8 @@ var // value parser, e.g. for an array which values are distracted among multiple fields) if (value !== undefined && value !== null && value !== '') { // check if the field value is set (continue if so, otherwise skip condition verification) var model = modelHelper.deserializeObject(params.form, params.fieldsMap, params.constsMap, params.enumsMap, params.parsersMap, params.prefix); - toolchain.registerMethods(model); - var message = 'Field {0} - {1} expression:\n[{2}]\nto be executed within the following context (methods not shown):\n{3}'; + toolchain.registerMethods(model, params.methodsList); + var message = 'Field {0} - {1} expression:\n[{2}]\nto be executed within the following context:\n{3}'; logger.info(typeHelper.string.format(message, element.name, method, params.expression, model)); var exprVal = modelHelper.ctxEval(params.expression, model); // verify assertion, if not satisfied => notify (return false) return { @@ -748,10 +749,10 @@ var value = modelHelper.adjustGivenValue(value, element, params); var exprVal = undefined, model; - var message = 'Field {0} - {1} expression:\n[{2}]\nto be executed within the following context (methods not shown):\n{3}'; + var message = 'Field {0} - {1} expression:\n[{2}]\nto be executed within the following context:\n{3}'; if (!api.settings.optimize) { // no optimization - compute requirement condition (which now may have changed) despite the fact field value may be provided model = modelHelper.deserializeObject(params.form, params.fieldsMap, params.constsMap, params.enumsMap, params.parsersMap, params.prefix); - toolchain.registerMethods(model); + toolchain.registerMethods(model, params.methodsList); logger.info(typeHelper.string.format(message, element.name, method, params.expression, model)); exprVal = modelHelper.ctxEval(params.expression, model); } @@ -767,7 +768,7 @@ var } model = modelHelper.deserializeObject(params.form, params.fieldsMap, params.constsMap, params.enumsMap, params.parsersMap, params.prefix); - toolchain.registerMethods(model); + toolchain.registerMethods(model, params.methodsList); logger.info(typeHelper.string.format(message, element.name, method, params.expression, model)); exprVal = modelHelper.ctxEval(params.expression, model); // verify requirement, if satisfied => notify (return false) return { @@ -786,7 +787,7 @@ var // bind requirements first... $.each(annotations.split(''), function() { // it would be ideal to have exactly as many handlers as there are unique annotations, but the number of annotations isn't known untill DOM is ready var adapter = typeHelper.string.format('requiredif{0}', $.trim(this)); - $.validator.unobtrusive.adapters.add(adapter, ['expression', 'fieldsMap', 'constsMap', 'enumsMap', 'parsersMap', 'errFieldsMap', 'allowEmpty'], function(options) { + $.validator.unobtrusive.adapters.add(adapter, ['expression', 'fieldsMap', 'constsMap', 'enumsMap', 'methodsList', 'parsersMap', 'errFieldsMap', 'allowEmpty'], function(options) { buildAdapter(adapter, options); }); }); @@ -794,7 +795,7 @@ var // ...then move to asserts $.each(annotations.split(''), function() { var adapter = typeHelper.string.format('assertthat{0}', $.trim(this)); - $.validator.unobtrusive.adapters.add(adapter, ['expression', 'fieldsMap', 'constsMap', 'enumsMap', 'parsersMap', 'errFieldsMap'], function(options) { + $.validator.unobtrusive.adapters.add(adapter, ['expression', 'fieldsMap', 'constsMap', 'enumsMap', 'methodsList', 'parsersMap', 'errFieldsMap'], function(options) { buildAdapter(adapter, options); }); }); diff --git a/src/expressive.annotations.validate.min.js b/src/expressive.annotations.validate.min.js index 3910e82..243d0b6 100644 --- a/src/expressive.annotations.validate.min.js +++ b/src/expressive.annotations.validate.min.js @@ -4,4 +4,4 @@ * * Copyright (c) 2014 Jarosław Waliszko * Licensed MIT: http://opensource.org/licenses/MIT */ -!function(e,t){"use strict";var n=t.ea,r=t.console,i={settings:{debug:!1,optimize:!0,enumsAsNumbers:!0,dependencyTriggers:"change keyup",apply:function(t){!function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])}(i.settings,t),function(){if(!o.isBool(i.settings.debug))throw"EA settings error: debug value must be a boolean (true or false)";if(!o.isBool(i.settings.optimize))throw"EA settings error: optimize value must be a boolean (true or false)";if(!o.isBool(i.settings.enumsAsNumbers))throw"EA settings error: enumsAsNumbers value must be a boolean (true or false)";if(!o.isString(i.settings.dependencyTriggers)&&null!==i.settings.dependencyTriggers&&void 0!==i.settings.dependencyTriggers)throw"EA settings error: dependencyTriggers value must be a string (multiple event types can be bound at once by including each one separated by a space), null or undefined"}(),e("form").each(function(){e(this).find("input, select, textarea").off(".expressive.annotations"),d.bindFields(this,!0)}),a.info(o.string.format("EA settings applied:\n{0}",t))}},addMethod:function(e,t){s.addMethod(e,t)},addValueParser:function(e,t){o.addValueParser(e,t)},noConflict:function(){return t.ea===this&&(t.ea=n),this}},a={info:function(e){i.settings.debug&&r&&"function"==typeof r.log&&r.log("[info] "+a.prep(e,new Date))},warn:function(e){r&&"function"==typeof r.warn&&r.warn("[warn] "+a.prep(e,new Date))},fail:function(e){r&&"function"==typeof r.error&&r.error("[fail] "+a.prep(e,new Date))},prep:function(e,t){var n=e.split("\n"),r=(void 0!==t&&null!==t?"("+o.datetime.stamp(t)+"): ":"")+n.shift();return n.length>0?r+"\n"+o.string.indent(n.join("\n"),19):r}},s={methods:{},addMethod:function(e,t){var n=this.methods[e];this.methods[e]=function(){return t.length===arguments.length?t.apply(this,arguments):"function"==typeof n?n.apply(this,arguments):t.apply(this,arguments)}},registerMethods:function(e){var t,n;this.initialize();for(t in this.methods)if(this.methods.hasOwnProperty(t)){if(e.hasOwnProperty(t)){a.warn(o.string.format("Skipping {0} function registration due to naming conflict (property of the same name already defined within the context).",t));continue}n=this.methods[t],e[t]=n}},initialize:function(){this.addMethod("Now",function(){return Date.now()}),this.addMethod("Today",function(){return new Date((new Date).setHours(0,0,0,0)).getTime()}),this.addMethod("ToDate",function(e){return Date.parse(e)}),this.addMethod("Date",function(e,t,n){return new Date(new Date(e,t-1,n).setFullYear(e)).getTime()}),this.addMethod("Date",function(e,t,n,r,i,a){return new Date(new Date(e,t-1,n,r,i,a).setFullYear(e)).getTime()}),this.addMethod("TimeSpan",function(e,t,n,r){return 1e3*r+6e4*n+36e5*t+864e5*e}),this.addMethod("Length",function(e){return null!==e&&void 0!==e?e.length:0}),this.addMethod("Trim",function(t){return null!==t&&void 0!==t?e.trim(t):null}),this.addMethod("Concat",function(e,t){return[e,t].join("")}),this.addMethod("Concat",function(e,t,n){return[e,t,n].join("")}),this.addMethod("CompareOrdinal",function(e,t){return e===t?0:null!==e&&null===t?1:null===e&&null!==t?-1:e>t?1:-1}),this.addMethod("CompareOrdinalIgnoreCase",function(e,t){return e=null!==e&&void 0!==e?e.toLowerCase():null,t=null!==t&&void 0!==t?t.toLowerCase():null,this.CompareOrdinal(e,t)}),this.addMethod("StartsWith",function(e,t){return null!==e&&void 0!==e&&null!==t&&void 0!==t&&e.slice(0,t.length)===t}),this.addMethod("StartsWithIgnoreCase",function(e,t){return e=null!==e&&void 0!==e?e.toLowerCase():null,t=null!==t&&void 0!==t?t.toLowerCase():null,this.StartsWith(e,t)}),this.addMethod("EndsWith",function(e,t){return null!==e&&void 0!==e&&null!==t&&void 0!==t&&e.slice(-t.length)===t}),this.addMethod("EndsWithIgnoreCase",function(e,t){return e=null!==e&&void 0!==e?e.toLowerCase():null,t=null!==t&&void 0!==t?t.toLowerCase():null,this.EndsWith(e,t)}),this.addMethod("Contains",function(e,t){return null!==e&&void 0!==e&&null!==t&&void 0!==t&&e.indexOf(t)>-1}),this.addMethod("ContainsIgnoreCase",function(e,t){return e=null!==e&&void 0!==e?e.toLowerCase():null,t=null!==t&&void 0!==t?t.toLowerCase():null,this.Contains(e,t)}),this.addMethod("IsNullOrWhiteSpace",function(e){return null===e||!/\S/.test(e)}),this.addMethod("IsDigitChain",function(e){return/^[0-9]+$/.test(e)}),this.addMethod("IsNumber",function(e){return/^[+-]?(?:(?:[0-9]+)|(?:[0-9]+[eE][+-]?[0-9]+)|(?:[0-9]*\.[0-9]+(?:[eE][+-]?[0-9]+)?))$/.test(e)}),this.addMethod("IsEmail",function(e){return/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(e)}),this.addMethod("IsPhone",function(e){return/^(\+\s?)?((?!\+.*)\(\+?\d+([\s\-\.]?\d+)?\)|\d+)([\s\-\.]?(\(\d+([\s\-\.]?\d+)?\)|\d+))*(\s?(x|ext\.?)\s?\d+)?$/.test(e)}),this.addMethod("IsUrl",function(e){return/^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/\S*)?$/i.test(e)}),this.addMethod("IsRegexMatch",function(e,t){return null!==e&&void 0!==e&&null!==t&&void 0!==t&&new RegExp(t).test(e)}),this.addMethod("Guid",function(e){var t=o.guid.tryParse(e);if(t.error)throw t.msg;return t}),this.addMethod("Min",function(e){if(0===arguments.length)throw"no arguments";if(1===arguments.length&&o.isArray(e)){if(0===e.length)throw"empty sequence";return Math.min.apply(null,e)}return Math.min.apply(null,arguments)}),this.addMethod("Max",function(e){if(0===arguments.length)throw"no arguments";if(1===arguments.length&&o.isArray(e)){if(0===e.length)throw"empty sequence";return Math.max.apply(null,e)}return Math.max.apply(null,arguments)}),this.addMethod("Sum",function(e){if(0===arguments.length)throw"no arguments";var t,n,r=0;if(1===arguments.length&&o.isArray(e)){if(0===e.length)throw"empty sequence";for(t=0,n=e.length;t2&&a.warn(o.string.format("DOM field {0} is ambiguous (unless custom value parser is provided).",e.attr("name"))),e.is(":checked");case"radio":return e.filter(":checked").val();default:return e.length>1&&a.warn(o.string.format("DOM field {0} is ambiguous (unless custom value parser is provided).",e.attr("name"))),e.val()}}(u))||void 0===l||""===l)return null;if(null!==(f=o.tryParse(l,i,d,s))&&void 0!==f&&f.error)throw o.string.format("DOM field {0} value conversion to {1} failed. {2}",d,i,f.msg);return f},deserializeObject:function(e,t,n,r,a,s){function o(e,t,n){var r,i,a,s,o,u,d;for(d=/^([a-z_0-9]+)\[([0-9]+)\]$/i,r=e.split("."),i=n,a=0;a0?r+"\n"+o.string.indent(n.join("\n"),19):r}},s={methods:{},addMethod:function(e,t){var n=this.methods[e];this.methods[e]=function(){return t.length===arguments.length?t.apply(this,arguments):"function"==typeof n?n.apply(this,arguments):t.apply(this,arguments)}},registerMethods:function(e,t){var n,r,i;for(this.initialize(),n=0;nt?1:-1}),this.addMethod("CompareOrdinalIgnoreCase",function(e,t){return e=null!==e&&void 0!==e?e.toLowerCase():null,t=null!==t&&void 0!==t?t.toLowerCase():null,this.CompareOrdinal(e,t)}),this.addMethod("StartsWith",function(e,t){return null!==e&&void 0!==e&&null!==t&&void 0!==t&&e.slice(0,t.length)===t}),this.addMethod("StartsWithIgnoreCase",function(e,t){return e=null!==e&&void 0!==e?e.toLowerCase():null,t=null!==t&&void 0!==t?t.toLowerCase():null,this.StartsWith(e,t)}),this.addMethod("EndsWith",function(e,t){return null!==e&&void 0!==e&&null!==t&&void 0!==t&&e.slice(-t.length)===t}),this.addMethod("EndsWithIgnoreCase",function(e,t){return e=null!==e&&void 0!==e?e.toLowerCase():null,t=null!==t&&void 0!==t?t.toLowerCase():null,this.EndsWith(e,t)}),this.addMethod("Contains",function(e,t){return null!==e&&void 0!==e&&null!==t&&void 0!==t&&e.indexOf(t)>-1}),this.addMethod("ContainsIgnoreCase",function(e,t){return e=null!==e&&void 0!==e?e.toLowerCase():null,t=null!==t&&void 0!==t?t.toLowerCase():null,this.Contains(e,t)}),this.addMethod("IsNullOrWhiteSpace",function(e){return null===e||!/\S/.test(e)}),this.addMethod("IsDigitChain",function(e){return/^[0-9]+$/.test(e)}),this.addMethod("IsNumber",function(e){return/^[+-]?(?:(?:[0-9]+)|(?:[0-9]+[eE][+-]?[0-9]+)|(?:[0-9]*\.[0-9]+(?:[eE][+-]?[0-9]+)?))$/.test(e)}),this.addMethod("IsEmail",function(e){return/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(e)}),this.addMethod("IsPhone",function(e){return/^(\+\s?)?((?!\+.*)\(\+?\d+([\s\-\.]?\d+)?\)|\d+)([\s\-\.]?(\(\d+([\s\-\.]?\d+)?\)|\d+))*(\s?(x|ext\.?)\s?\d+)?$/.test(e)}),this.addMethod("IsUrl",function(e){return/^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/\S*)?$/i.test(e)}),this.addMethod("IsRegexMatch",function(e,t){return null!==e&&void 0!==e&&null!==t&&void 0!==t&&new RegExp(t).test(e)}),this.addMethod("Guid",function(e){var t=o.guid.tryParse(e);if(t.error)throw t.msg;return t}),this.addMethod("Min",function(e){if(0===arguments.length)throw"no arguments";if(1===arguments.length&&o.isArray(e)){if(0===e.length)throw"empty sequence";return Math.min.apply(null,e)}return Math.min.apply(null,arguments)}),this.addMethod("Max",function(e){if(0===arguments.length)throw"no arguments";if(1===arguments.length&&o.isArray(e)){if(0===e.length)throw"empty sequence";return Math.max.apply(null,e)}return Math.max.apply(null,arguments)}),this.addMethod("Sum",function(e){if(0===arguments.length)throw"no arguments";var t,n,r=0;if(1===arguments.length&&o.isArray(e)){if(0===e.length)throw"empty sequence";for(t=0,n=e.length;t2&&a.warn(o.string.format("DOM field {0} is ambiguous (unless custom value parser is provided).",e.attr("name"))),e.is(":checked");case"radio":return e.filter(":checked").val();default:return e.length>1&&a.warn(o.string.format("DOM field {0} is ambiguous (unless custom value parser is provided).",e.attr("name"))),e.val()}}(u))||void 0===l||""===l)return null;if(null!==(f=o.tryParse(l,i,d,s))&&void 0!==f&&f.error)throw o.string.format("DOM field {0} value conversion to {1} failed. {2}",d,i,f.msg);return f},deserializeObject:function(e,t,n,r,a,s){function o(e,t,n){var r,i,a,s,o,u,d;for(d=/^([a-z_0-9]+)\[([0-9]+)\]$/i,r=e.split("."),i=n,a=0;a