/// <summary> /// The "copy" operation copies the value at a specified location to the /// target location. /// /// The operation object MUST contain a "from" member, which is a string /// containing a JSON Pointer value that references the location in the /// target document to copy the value from. /// /// The "from" location MUST exist for the operation to be successful. /// /// For example: /// /// { "op": "copy", "from": "/a/b/c", "path": "/a/b/e" } /// /// This operation is functionally identical to an "add" operation at the /// target location using the value specified in the "from" member. /// /// Note: even though it's the same functionally, we do not call add with /// the value specified in from for performance reasons (multiple checks of same requirements). /// </summary> /// <param name="operation">The copy operation</param> /// <param name="objectApplyTo">Object to apply the operation to</param> public void Copy(Operation <T> operation, T objectToApplyTo) { object valueAtFromLocation = null; // get path result var pathResult = PropertyHelpers.GetActualPropertyPath( operation.from, objectToApplyTo, operation, true); var positionAsInteger = pathResult.NumericEnd; var actualPathToFromProperty = pathResult.PathToProperty; PropertyInfo fromProperty = PropertyHelpers .FindProperty(objectToApplyTo, actualPathToFromProperty); // does property at from exist? if (fromProperty == null) { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operation, string.Format("Patch failed: property at location from: {0} does not exist", operation.from)) , 422); } // get the property path // is the path an array (but not a string (= char[]))? In this case, // the path must end with "/position" or "/-", which we already determined before. if (positionAsInteger > -1) { if (fromProperty.PropertyType.IsNonStringList()) { // now, get the generic type of the enumerable var genericTypeOfArray = PropertyHelpers.GetEnumerableType(fromProperty.PropertyType); if (!fromProperty.CanRead) { // cannot get the property throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operation, string.Format("Patch failed: cannot get property value at location from: {0}. Possible cause: the property doesn't have an accessible getter.", operation.from)), 422); } // get value (it can be cast, we just checked that) var array = PropertyHelpers.GetValue(fromProperty, objectToApplyTo, actualPathToFromProperty) as IList; if (array.Count <= positionAsInteger) { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operation, string.Format("Patch failed: provided from path is invalid for array property type at location from: {0}: invalid position", operation.from)) , 422); } valueAtFromLocation = array[positionAsInteger]; } else { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operation, string.Format("Patch failed: provided from path is invalid for array property type at location from: {0}: expected array", operation.from)) , 422); } } else { if (!fromProperty.CanRead) { // cannot get the property throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operation, string.Format("Patch failed: cannot get property value at location from: {0}. Possible cause: the property doesn't have an accessible getter.", operation.from)), 422); } // no list, just get the value valueAtFromLocation = PropertyHelpers.GetValue(fromProperty, objectToApplyTo, actualPathToFromProperty); } // add operation to target location with that value. Add(operation.path, valueAtFromLocation, objectToApplyTo, operation); }
/// <summary> /// Add is used by various operations (eg: add, copy, ...), yet through different operations; /// This method allows code reuse yet reporting the correct operation on error /// </summary> private void Add(string path, object value, T objectToApplyTo, Operation <T> operationToReport) { // add, in this implementation, does not just "add" properties - that's // technically impossible; It can however be used to add items to arrays, // or to replace values. // first up: if the path ends in a numeric value, we're inserting in a list and // that value represents the position; if the path ends in "-", we're appending // to the list. // get path result var pathResult = PropertyHelpers.GetActualPropertyPath( path, objectToApplyTo, operationToReport, true); var appendList = pathResult.ExecuteAtEnd; var positionAsInteger = pathResult.NumericEnd; var actualPathToProperty = pathResult.PathToProperty; var pathProperty = PropertyHelpers .FindProperty(objectToApplyTo, actualPathToProperty); // does property at path exist? if (pathProperty == null) { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: property at location path: {0} does not exist", path)) , 422); } // it exists. If it' an array, add to that array. If it's not, we replace. // is the path an array (but not a string (= char[]))? In this case, // the path must end with "/position" or "/-", which we already determined before. if (appendList || positionAsInteger > -1) { // what if it's an array but there's no position?? if (pathProperty.PropertyType.IsNonStringList()) { // now, get the generic type of the enumerable var genericTypeOfArray = PropertyHelpers.GetEnumerableType(pathProperty.PropertyType); var conversionResult = PropertyHelpers.ConvertToActualType(genericTypeOfArray, value); if (!conversionResult.CanBeConverted) { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided value is invalid for array property type at location path: {0}", path)) , 422); } if (!pathProperty.CanRead) { // cannot get the property throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: cannot get property value at location from: {0}. Possible cause: the property doesn't have an accessible getter.", path)), 422); } // get value (it can be cast, we just checked that) var array = PropertyHelpers.GetValue(pathProperty, objectToApplyTo, actualPathToProperty) as IList; if (appendList) { array.Add(conversionResult.ConvertedInstance); } else { // specified index must not be greater than the amount of items in the // array if (positionAsInteger <= array.Count) { array.Insert(positionAsInteger, conversionResult.ConvertedInstance); } else { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: position larger than array size", path)) , 422); } } } else { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: expected array", path)) , 422); } } else { var conversionResultTuple = PropertyHelpers.ConvertToActualType(pathProperty.PropertyType, value); // conversion successful if (conversionResultTuple.CanBeConverted) { if (!pathProperty.CanWrite) { // cannot set the property throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: cannot set property value at path {0}. Possible cause: the property doesn't have an accessible setter.", path)), 422); } PropertyHelpers.SetValue(pathProperty, objectToApplyTo, actualPathToProperty, conversionResultTuple.ConvertedInstance); } else { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided value is invalid for property type at location path: {0}", path)) , 422); } } }
/// <summary> /// Remove is used by various operations (eg: remove, move, ...), yet through different operations; /// This method allows code reuse yet reporting the correct operation on error /// </summary> private void Remove(string path, T objectToApplyTo, Operation <T> operationToReport) { // get path result var pathResult = PropertyHelpers.GetActualPropertyPath( path, objectToApplyTo, operationToReport, false); var removeFromList = pathResult.ExecuteAtEnd; var positionAsInteger = pathResult.NumericEnd; var actualPathToProperty = pathResult.PathToProperty; var pathProperty = PropertyHelpers .FindProperty(objectToApplyTo, actualPathToProperty); // does the target location exist? if (pathProperty == null) { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: property at location path: {0} does not exist", path)) , 422); } // get the property, and remove it - in this case, for DTO's, that means setting // it to null or its default value; in case of an array, remove at provided index // or at the end. if (removeFromList || positionAsInteger > -1) { // what if it's an array but there's no position?? if (pathProperty.PropertyType.IsNonStringList()) { // now, get the generic type of the enumerable var genericTypeOfArray = PropertyHelpers.GetEnumerableType(pathProperty.PropertyType); if (!pathProperty.CanRead) { // cannot get the property throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: cannot get property value at path: {0}. Possible cause: the property doesn't have an accessible getter.", path)), 422); } // get value (it can be cast, we just checked that) var array = PropertyHelpers.GetValue(pathProperty, objectToApplyTo, actualPathToProperty) as IList; if (removeFromList) { if (array.Count == 0) { // if the array is empty, we should throw an error throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: position larger than array size", path)) , 422); } array.RemoveAt(array.Count - 1); } else { if (positionAsInteger < array.Count) { array.RemoveAt(positionAsInteger); } else { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: position larger than array size", path)) , 422); } } } else { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: expected array", path)) , 422); } } else { if (!pathProperty.CanWrite) { // cannot set the property throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: cannot set property value at path {0}. Possible cause: the property doesn't have an accessible setter.", path)), 422); } // setting the value to "null" will use the default value in case of value types, and // null in case of reference types PropertyHelpers.SetValue(pathProperty, objectToApplyTo, actualPathToProperty, null); } }