private static string Replace(string source, JsRegex regEx, string replaceWith) { return(Script.Write <string>("{0}.replace({1}, {2})", source, regEx, replaceWith)); }
private static PropertySetter ConstructSetter <T, TPropertyValue>(this T source, Func <T, TPropertyValue> propertyIdentifier) { if (source == null) { throw new ArgumentNullException("source"); } if (propertyIdentifier == null) { throw new ArgumentNullException("propertyIdentifier"); } if (GetFunctionArgumentCount(propertyIdentifier) != 1) { throw new ArgumentException("The specified propertyIdentifier function must have precisely one argument"); } // Ensure that the propertyIdentifier is of a form similar to // function (_) { return _.getName(); } // Note that in minified JavaScript, it may be // function(_){return _.getName()} // The way that this is verified is to get a "normalised" version of the string representation of the propertyIdentifier function - this removes any comments // and replaces any single whitespace characters (line returns, tabs, whatever) with a space and ensures that any runs of whitespace are reduced to a single // character. We compare this (with a reg ex) to an expected format which is a string that is built up to match the first form. This is then tweaked to make // the spaces and semi-colon optional (so that it can also match the minified form). // 2016-08-04 DWR: There are some additional forms that should be supported; for example, Firefox may report "use strict" as part of the function content - // function (_) { "use strict"; return _.getName(); } // .. and code coverage tools may inject other content before the getter is called - // function (_) { coverageFramework.track("MyClass.cs", 18); return _.getName(); } // .. as such, the function format matching has been relaxed to allow a section to be ignored before the getter call. I contempled removing this entirely since // there is an analyser to ensure that only valid properties are referenced in the C# code but this change seemed minor and could be useful if a project included // IAmImmutable implementations that disabled the analyser (or that were built in VS2013 or earlier). If there are any further problems then I may reconsider. var singleArgumentNameForPropertyIdentifier = GetFunctionSingleArgumentName(propertyIdentifier); // In case there are any new lines in the additional content that is supported before the getter call (see 2016-08-04 notes above), we need to look for "any // character" that includes line returns and so use "[.\s\S]*" instead of just ".*" (see http://trentrichardson.com/2012/07/13/5-must-know-javascript-regex-tips/). // It might be cleaner to use the C# RegEx which is fully supported by Bridge (but wasn't when this code was first written). var argumentName = GetFunctionSingleArgumentName(propertyIdentifier); // If an IAmImmutable type is also decorated with [ObjectLiteral] then instances won't actually have real getter and setter methods, they will just have raw // properties. There are some hoops to jump through to combine IAmImmutable and [ObjectLiteral] (the constructor won't be called and so CtorSet can't be used // to initialise the instance) but if this combination is required then the "With" method may still be used by identifying whether the current object is a // "plain object" and working directly on the property value if so. var isObjectLiteral = IsObjectLiteral(source); if (isObjectLiteral) { var objectLiteralRegExSegments = new[] { AsRegExSegment(string.Format( "function ({0}) {{", argumentName )), AsRegExSegment(string.Format( "return {0}.", argumentName )), AsRegExSegment("; }") }; var objectLiteralExpectedFunctionFormatMatcher = new JsRegex( string.Join("([.\\s\\S]*?)", objectLiteralRegExSegments) ); var objectLiteralPropertyIdentifierStringContent = GetNormalisedFunctionStringRepresentation(propertyIdentifier); var objectLiteralPropertyNameMatchResults = objectLiteralExpectedFunctionFormatMatcher.Exec(objectLiteralPropertyIdentifierStringContent); if (objectLiteralPropertyNameMatchResults == null) { throw new ArgumentException("The specified propertyIdentifier function did not match the expected format - must be a simple property access for an [ObjectLiteral], such as \"function(_) { return _.name; }\", rather than \"" + objectLiteralPropertyIdentifierStringContent + "\""); } // If the target is an [ObjectLiteral] then just set the property name on the target, don't try to call a setter (since it won't be defined) var objectLiteralPropertyName = objectLiteralPropertyNameMatchResults[objectLiteralPropertyNameMatchResults.Length - 1]; return((target, newValue, ignoreAnyExistingLock) => { Script.Write("target[{0}] = {1};", objectLiteralPropertyName, newValue); }); } // Note: A property specified directly on the target type will be specified using the simple format "function (_) { return _.Name; }" (before Bridge 16, there // were custom getter and setter functions - eg. "function (_) { return _.getName(); }" - but now it uses ES5 properties and so the simpler version appears in // the generated JavaScript. If the target property is specified via an interface cast then an alias property name will be used (in case there is a property on // the target type with the same name as the interface property and then the interface property is implemented explicitly) - it will look something like // "function (_) { return _.Example$IHaveName$Name; }" and we need to correctly handle that below too. var regExSegments = new[] { AsRegExSegment(string.Format( "function ({0}) {{", argumentName )), AsRegExSegment(string.Format( "return {0}.", argumentName )), AsRegExSegment("; }") }; var expectedFunctionFormatMatcher = new JsRegex( string.Join("([.\\s\\S]*?)", regExSegments) ); var propertyIdentifierStringContent = GetNormalisedFunctionStringRepresentation(propertyIdentifier); var propertyNameMatchResults = expectedFunctionFormatMatcher.Exec(propertyIdentifierStringContent); if (propertyNameMatchResults == null) { throw new ArgumentException("The specified propertyIdentifier function did not match the expected format - must be a simple property get, such as \"function(_) { return _.Name; }\", rather than \"" + propertyIdentifierStringContent + "\""); } var typeAliasPrefix = propertyNameMatchResults[propertyNameMatchResults.Length - 2];; var propertyName = propertyNameMatchResults[propertyNameMatchResults.Length - 1]; var propertyDescriptorIfDefined = TryToGetPropertyDescriptor(source, propertyName); if (propertyDescriptorIfDefined == null) { throw new ArgumentException("Failed to find expected property \"" + propertyName + "\" (could not retrieve PropertyDescriptor)"); } // 2018-01-18 DWR: Bridge 16.0 moved to auto properties being ES6 style properties with getters and setters at all times but 16.3 introduced an option for them to be "Plain" rather than "Managed" // and the bridge.json that is included with the Bridge NuGet package enables this option and so we can't presume that a setter will be present. If not, check whether the property is declared as // being "Writable" and generate a setter function that will work as if the "Managed" autoProperty rule is specified. var setter = propertyDescriptorIfDefined.OptionalSetter; if (Script.Write <bool>("!!setter")) { var hasExpectedSetter = Script.Write <bool>("(typeof(setter) === \"function\") && (setter.length === 1)"); if (!hasExpectedSetter) { throw new ArgumentException("Property setter does not match expected format (single argument function) for \"" + propertyName + "\""); } } else if (propertyDescriptorIfDefined.IsWritableIfPlainAutoProperty) { setter = Script.Write <Action <object> >("function (value) { this[{0}] = value; }", propertyName); } else { throw new ArgumentException("Failed to retrieve expected property setter for \"" + propertyName + "\""); } return((target, newValue, ignoreAnyExistingLock) => { var isLocked = false; var propertyLockName = "__" + propertyName + "_Lock"; /*@isLocked = !ignoreAnyExistingLock && (target[propertyLockName] === true); * if (!isLocked) { * setter.apply(target, [newValue]); * target[propertyLockName] = true; * }*/ if (isLocked) { throw new ArgumentException("This property has been locked - it should only be set within the constructor"); } }); }