Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement applyPatch to fix #147, with all needed specs #167

Merged
merged 23 commits into from
May 29, 2017

Conversation

alshakero
Copy link
Collaborator

@alshakero alshakero commented May 10, 2017

Fixes: #147

Copy link
Contributor

@felixfbecker felixfbecker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for taking this on!

* Apply a single json-patch on an object tree
* Returns the result object.
*/
export function applyPatch(tree: any, patch: any, validate = false): any {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we default validate to false if typeof validate !== 'boolean'? Otherwise the function doesn't work as a reducer because the index is passed as a third parameter and would be coerced to boolean.

if (patch.path === "") {
if (patch.op === 'add' || patch.op === 'replace') {// same for add or replace
// a primitive value, just return it
if (typeof patch.value !== 'object') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

|| patch.value === null

// a primitive value, just return it
if (typeof patch.value !== 'object') {
return patch.value;
} else if (typeof patch.value === 'object') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this condition never evaluates to false as it is checked above already

*/
export function applyPatch(tree: any, patch: any, validate = false): any {
if (validate && typeof tree !== 'object') {
throw new TypeError('Tree has to be an object');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does the document have to be an object?

} else { // test operation (even if not on root) needs to return a boolean
if (patch.op === 'test') {
var results = apply(tree, [patch], validate);
return results[0];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This breaks the contract of the function to always return the updated document.
If a test operation fails, it should abort the patch by throwing an error.
If you have an array of patches, it should be possible to use array.reduce() to apply all patches. That doesn't work if test returns a boolean, but would correctly abort the patch if it throws an error.

} else { // it's a remove or test on root
if (patch.op === 'test') {
return _equals(tree, patch.value);
} else { // a remove on root
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would check patch.op === 'remove' here and and either error or ignore the patch (return the input) if the operation is anything else. Otherwise any invalid patch would result in a null result and it wouldn't be obvious why.

README.md Outdated

Applying patches on root is a little tricky, for that you can't replace your root object with an object of a different type in place (eg. can't replace `{item: "item 1"}` with `[{item: "item 1"}]`) on the root level, because the original document is an object `{}` and the new value is an array `[]`, nor can you do the opposite (`[] => {}`), obviously. See https://github.com/Starcounter-Jack/JSON-Patch/issues/147.

However, you can use `applyPatch` method to over come this issue, because it returns the result object, and this allows it to change types.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be great to document the usage with array.reduce() to apply multiple patches (that may modify the root).

}
} else if (patch.op === 'move' || patch.op === 'copy') { // it's a move or copy to root
// get the value by json-pointer in `from` field
var temp: any = { op: "_get", path: patch.from };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these variables are never reassigned so can use const

* Apply a single json-patch on an object tree
* Returns the result object.
*/
export function applyPatch(tree: any, patch: any, validate = false): any {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Signature should be

export function applyPatch<T>(tree: T, patch: Patch<any>): T;
export function applyPatch<T>(tree: T, patch: Patch<any>, validate: boolean): T;
export function applyPatch<T>(tree: T, patch: Patch<any>, validate = false): T {
  if (typeof validate !== 'boolean') {
    validate = false;
  }
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll make it return any for the true that is returned by test op. Agree?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

- Enhance typings
- Verify if validate is a boolean or default to false
- Throw an error when `test` operation fails
- Change tests accordingly
- Add documentation for using applyPatch with reduce
README.md Outdated
Applies a single `patch` on `obj`.

Returns the modified object.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rename it to applyOperationObject/applyOperation to be more consistent with naming in spec. Then I'd remove argument and variable inside to operation

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, I'd also adjust the names in apply, to something like:

Applies JSON patch (array of operations) on obj.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not 100% satisfied with the name either, but note that the type for a single operation is also already called Patch, while apply() takes an array of that: Patch[]. Renaming those would be a breaking change. It also matches how the other libraries name these types.

Operation could be a bit confusing because there is the op attribute, which is short for operation, that is a string.

In any case I would like to have a short name, applyOperationObject is very long.

Copy link
Collaborator Author

@alshakero alshakero May 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@felixfbecker if I understand correctly, he just means the params/args names, it isn't a breaking change.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tomalec I guess you mean 'rename' instead of 'remove' right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know, but imo the name of types and the param names / function name should be consistent. A parameter of type Patch should be named patch, not operation. A function applyOperation should take a parameter of type Operation, not Patch. So I would stay with the "patch" naming meaning a single patch vs calling a single patch "operation".

README.md Outdated

Available in *json-patch.js* and *json-patch-duplex.js*

Applies a single `patch` on `obj`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Applies single operation object patch on obj"

@tomalec
Copy link
Collaborator

tomalec commented May 10, 2017

I'd get deeper into it after our HO meeting, or later this week..

// conflicting types [] vs {} just return the new value
if (_isArray(tree) !== _isArray(patch.value)) {
return patch.value;
//same type, use original apply
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a big issue, but why are we making the effort here to mutate the original object at all if the function returns a new reference in some cases anyway? The new object is already in the heap. It is way faster to just switch the reference out than mutating the original object, basically diffing it, removing all properties, then copying over all from the new object. Given the performance focus of this package I would vote for always returning the new value if the root is replaced (it also makes more sense semantically).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree I was building a demo and not modifying the original object seems like a semantic must. Let alone the perf boost.

}
}
}
} else { // test operation (even if not on root) needs to return a boolean
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment is outdated

@alshakero
Copy link
Collaborator Author

@felixfbecker I guess we were both off with our definition of patch, and this means even the TS types we merged recently are incorrect. Because the RFC says:

JSON Patch document is a JSON [RFC4627] document that represents an array of objects. Each object represents a single operation to be applied to the target JSON document.

So this:

export interface AddPatch<T> extends PatchBase {
  op: 'add';
  value: T;
}

Isn't a correct interface for a patch 😢 It is correct for operation, though.

@felixfbecker
Copy link
Contributor

I am aware and not happy with it either, but again it would be a breaking change to change. What we could do is export the old types as type aliases and deprecate them though.

@felixfbecker
Copy link
Contributor

@alshakero @tomalec Wdyt about

  • naming the function applyOperation
  • documenting that it applies a single operation and returns the updated document
  • renaming the types to Operation, AddOperation etc.
  • exporting the types under an alias as the old name with a deprecation note

@alshakero
Copy link
Collaborator Author

@felixfbecker Sounds great. Let's see @tomalec's opinion.

In the meantime, I'll push now some changes I did to applyPatch so you might like to review them. I think it's much faster now.

- Turn to opt-in pure function
- Better documentation
- Needed tests
*/
export function applyPatch<T>(tree: T, patch: Patch<any>): T;
export function applyPatch<T>(tree: T, patch: Patch<any>, validate: boolean, touchOriginalTree: boolean): T;
export function applyPatch<T>(tree: T, patch: Patch<any>, validate = false, touchOriginalTree = true): T {
Copy link
Contributor

@felixfbecker felixfbecker May 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not convinced we should have the third parameter, it bloats the signature and adds more branches to the function (you would have to test this for all operations). JSON Patch's sole purpose is to mutate objects. If you really want to clone the tree on every operation, it can be easily achieved through functional composition:

patches.reduce((obj, op) => applyOperation(deepClone(obj), op), obj)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this makes the function inconsistently impure. And we have a use case for the pure version already. I don't mind writing tests for more cases.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to keep it, the signature needs to be updated to make touchOriginalTree optional

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@felixfbecker doesn't this signature alone suffice?

export function applyPatch<T>(tree: T, patch: Patch<any>, validate: boolean = false, touchOriginalTree: boolean = true): T {
  if (typeof validate !== 'boolean') { // if validate is not a boolean, it's being called from `reduce`
    validate = false;
    touchOriginalTree = true;
  }

According to TS docs, any param with a default value is optional.

Default-initialized parameters that come after all required parameters are treated as optional, and just like optional parameters, can be omitted when calling their respective function.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, because that is just the implementation. This signature is overloaded, so the two signatures above the implementation that just end with a semicolon define the signature. Overloading is needed here because without the first signature the function would not work as reducer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify, the second signature needs to be updated.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

export function applyPatch<T>(tree: T, patch: Patch<any>): T;
export function applyPatch<T>(tree: T, patch: Patch<any>, validate: boolean, touchOriginalTree?: boolean): T;
export function applyPatch<T>(tree: T, patch: Patch<any>, validate = false, touchOriginalTree = true): T {
  if (typeof validate !== 'boolean') { // if validate is not a boolean, it's being called from `reduce`
    validate = false;
    touchOriginalTree = true;
  }
}

This does it, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, LGTM

} else if (patch.op === 'move' || patch.op === 'copy') { // it's a move or copy to root
// get the value by json-pointer in `from` field
const temp: any = { op: "_get", path: patch.from };
apply(tree, [temp]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copy and move should also just return the new value

Copy link
Collaborator Author

@alshakero alshakero May 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They do return?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah sorry, just saw that the apply is only for getting the value 👍

@tomalec
Copy link
Collaborator

tomalec commented May 12, 2017

@felixfbecker 👍

  • naming the function applyOperation
  • documenting that it applies a single operation and returns the updated document
  • renaming the types to Operation, AddOperation etc.
  • exporting the types under an alias as the old name with a deprecation note

I'd just change the docs to match exact RFC naming so "that it applies a single operation object and returns the updated document"

@alshakero
Copy link
Collaborator Author

@felixfbecker is there a chance that you do the type aliasing thing? That would be a huge favour because you're the most fluent TypeScripter here.

@felixfbecker
Copy link
Contributor

Yeah no problem

felixfbecker and others added 6 commits May 15, 2017 10:53
- Make `applyOperation` standalone (#175)
- Deprecate `apply` and throw a warning when called
- Fix `_equals` (#166)
- Replace parseInt with bitwise opers (#171)
- Export `escapePath` and `unEscapePath`(#173)
- Add `applyReduer` to use with Array.Reduce
- Remove `rootOps`
- Add tests for all new functions
- Add backwards compability tests
- Update readme accordingly
@alshakero
Copy link
Collaborator Author

@tomalec Ready for review.

export interface PatchBase {
export interface OperationResult {
result: any,
newDocument: any
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be generic T

* @param path The escaped pointer
* @return The unescaped path
*/
export function unEscapePath(path: string): string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The native unescape function does not uppercase the E

* @param pointer an escaped JSON pointer
* @return The retrieved value
*/
export function getValueByPointer(document, pointer) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(document: any, pointer: string): any

* @param mutateDocument Whether to mutate the original document or clone it before applying
* @return `{newDocument, result}` after the operation
*/
export function applyOperation<T>(document: any, operation: Operation, validate: boolean, mutateDocument: boolean): OperationResult;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this overload is unneeded

README.md Outdated
* `test` - boolean result of the test
* `remove`, `replace` and `move` - original object that has been removed
* `add` (only when adding to an array) - index at which item has been inserted (useful when using `-` alias)

#### jsonpatch.observe (`obj` Object, `callback` Function (optional)) : `observer` Object
#### `jsonpatch.applyOperation<T>(document: T, operation: Operation, validate: boolean = false, mutateDocument: boolean = false): T`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

signature is outdated

}
} /* END ROOT OPERATIONS */
else {
!mutateDocument && (document = deepClone(document));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

statement syntax (if) is more readable for side effects than expression syntax imo

value = jsonpatch.getValueByPointer(document, (<any>patch[i]).from);
}
if (patch[i].op == "replace" || patch[i].op == "add") {
value = (<any>patch[i]).value;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeScript should infer the type as ReplaceOperation | AddOperation if you use === instead of ==, you shouldn't need to cast to any

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't work, still complaining. I'm using TS 2.0.0.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you try upgrading?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, upgrading breaks compilation if you remember. Fixing the issue needs some investigation and it's not a priority for now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I though it might have been fixed. I would cast to ReplaceOperation | AddOperation then


}
else {
results[i] = applyOperation.call(this, document, patch[i], validate, true).result;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why .call(this, ...? What is this even in this context? What does applyOperation need it for?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It needs it for this.validator.

* @param operation The operation to apply
* @return The updated document
*/
export function applyReducer<T>(document: T, operation: Operation): T;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redundant signature

*/
export function applyReducer<T>(document: T, operation: Operation): T;
export function applyReducer<T>(document: T, operation: Operation): T {
const operationResult: OperationResult = applyOperation.call(this, document, operation)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why .call(this, ...?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same reason, it needs this for this.validator.

export function applyOperation<T>(document: any, operation: Operation, validate: boolean, mutateDocument: boolean): OperationResult;
export function applyOperation<T>(document: any, operation: Operation, validate = false, mutateDocument = true): OperationResult {
if (validate) {
this.validator(operation, 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait, so if you just call applyValidation(document, op, true) the function will throw? Why does the user have to provide this as a this binding, instead of an optional parameter?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really because it is bound to jsonpatch object by default.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in

var jsonpatch;
(function (jsonpatch) {
    function sub() {
    	return this;
    }
    jsonpatch.sub = sub;
})(jsonpatch = {});
jsonpatch == jsonpatch.sub(); // ==> true

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if I import it like this

import { applyOperation } from 'fast-json-patch'

and transpile with Babel, or let Webpack do tree-shaking? It probably won't pick up the reference. This feels like a hack

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or in ES5:

const { applyOperation } = require('fast-json-patch')
const applyOperation = require('fast-json-patch').applyOperation

Copy link
Collaborator Author

@alshakero alshakero May 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer keeping the API as close as possible as I can to its current shape. Especially when it comes to often-used functions. Setting the validator for once sounds better.

A .setValidator function or a setter sound better, and I'll ask my teammates to maybe throw this whole thing. I don't see any use for it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tbh I would consider that a code smell. It's a global setting that could be set anywhere in an application (even by a library) and would yield unexpected results. How would a library customize the validator without affecting application code?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

100% legit point.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would consider that a code smell.

Totally agree.

I'm for the optional param: validate: boolean | Validator, where

  • false - no validation
  • true - default validator: jsonpatch.validator
  • function - custom validator

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect then 💃

README.md Outdated
@@ -92,29 +92,24 @@ var patch = [
{ op: "add", path: "/lastName", value: "Wester" },
{ op: "add", path: "/contactDetails/phoneNumbers/0", value: { number: "555-123" } }
];
jsonpatch.apply(document, patch);
document = jsonpatch.applyPatch(document, patch).document;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the old jsonpatch.applyPatch(document, patch) should work a before, do we need to obfuscate the example code?

README.md Outdated

If the `validate` parameter is set to `true`, the patch is extensively validated before applying.
An invalid patch results in throwing an error (see `jsonpatch.validate` for more information about the error object).

Returns an array of results - one item for each item in `patches`. The type of each item depends on type of operation applied
Returns an array of objects - one item for each item in `patches`, each item is an object `{newDocument, result}`. The type of `result` depends on type of operation applied.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe one item for each operation inpatches, each item is an object to reduce the number of "items"

README.md Outdated
TEST_OPERATION_FAILED | When operation is `test` and the test fails, applies to `applyReducer`.


#### `jsonpatch.escapePath(path: string): string`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it a duplicate of the same thing written above?

@tomalec
Copy link
Collaborator

tomalec commented May 18, 2017

I'll continue review, later, had to 'finish' it to make github allow me to respond to your comments.

@alshakero
Copy link
Collaborator Author

maybe now we could change enigmatic result: Boolean | Object, to removed: Object, test: Boolean.

Can you elaborate on removed: Object, test: Boolean. I don't really understand. A sample return value would do.

What was the rationale with renaming tree to document? Is it used that way in RFC? document is confusing to me as I think of browser's DOM document.

Yes:

JSON Patch defines a JSON document structure for expressing a sequence of operations to apply to a JavaScript Object Notation (JSON) document

Regarding deprecating old apply. What do you think about instead of keeping it backwards compatible but deprecated, break compatibility and make it a regular alias of applyPatch?
Patch in jsonpatch.applyPatch looks quite redundant jsonpatch.apply was quite intuitive. However I may just get used to it.

I don't mind that as a next step, but for now, I guess we should keep warning people to use applyPatch, and that's not to push the name, but to push using the return value. After a month or so we can do whatever we please with apply.

alshakero added 2 commits May 21, 2017 14:08
- Add `.newDocument` to applyPatch return value and readme
- A little cleanup
@felixfbecker
Copy link
Contributor

I like { removed: any, test: boolean }, the return type is less magic that way.

getPointerByValue didn't make use of this and went back to the tree root again.

Is there a reason getPointerValue cannot be changed to do this instead?

@alshakero
Copy link
Collaborator Author

@felixfbecker it is changed to a _get now.

for (var i = 0, l = a.length; i < l; i++)
if (!_equals(a[i], b[i])) return false;

if (!_equals(a[i], b[i]))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add {} after if?

var original = getOriginalDestination.value === undefined ?
undefined : JSON.parse(JSON.stringify(getOriginalDestination.value));
move: function (obj, key, document) {
const originalValue = getValueByPointer(document, this.path);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't you use result of const originalValue = applyOperation(document, { op: "remove", path: this.from } ).result; so you don't have to apply _get here and iterate over tree once again?

@tomalec
Copy link
Collaborator

tomalec commented May 22, 2017

Speaking of naming of applyPatch and not aliasing it as apply, - then to change apply from deprecated old behavior to just an alias of applyPatch we would have to release major version. I think it would be better not to bump major version too often, especially just to release an alias.

I would consider old behavior buggy and would like to drop support for it sooner than later.
So have jsonpatch.applyPatch <=> jsonpatch.apply (short and easy) @felixfbecker, @warpech WDYT?


Speaking of performance of _get and op: 'move', could you check if it's better now? I also suggested something in review what may help.


Speaking of results:

const removeOutcome = jsonpatch.applyOperation(document, { op: "remove", path: 'path/foo' } );
removeOutcome: {
    document: document,
    removed: 'bar'
};
// replace, move, copy are in fact remove + add
const testOutcome = jsonpatch.applyOperation(document, { op: "test", path: 'patt/foo', value: 'baz' } );
testOutcome: {
    document: document,
    test: true
};

@felixfbecker
Copy link
Contributor

I would say either

  • deprecate apply (but keep old behaviour), release a minor
  • or remove apply, release a major

@alshakero
Copy link
Collaborator Author

I would consider old behavior buggy and would like to drop support for it sooner than later.
So have jsonpatch.applyPatch <=> jsonpatch.apply (short and easy)

I'd keep buggy apply and release minor, not because I don't agree with you at all. But because I think we have a necessary update that must be major (changes files hierarchy). So I think we should save major release for that, the update is to go for proper modularization:

  • Using CommonJS in files, so duplex will use json-patch.ts, and by this:
  • we can use (read require) thoroughly tested libraries for _equal and deepClone instead of making it our problem like now.
  • This will mean less code, fewer bugs, and all applyX functions will be in one file. It was hell copying and pasting with every modification I make.
  • This will remove the hacky exporting we're doing right now that breaks browersify apps.
  • It will make moving to proxies easier for next-next version 3.0.0.

As big as this may sound. It's exactly two hours for me, I don't even mind doing it in this PR today. With benchmarking clone lib vs our deepClone. If you agree, I can go for it and we can remove apply just now.

Speaking of performance of _get and op: 'move', could you check if it's better now? I also suggested something in review what may help.

I did, it's back as it was 💃

Regarding results, I'm working on it.

@felixfbecker
Copy link
Contributor

I would vote for keeping a minor and not bloating this PR any further.

@alshakero
Copy link
Collaborator Author

alshakero commented May 22, 2017

@felixfbecker Me too. I'm just trying my best to point out that it's not going to take a lot of time to implement it. And that we need a major release for something else.

- Update tests to follow.
- Explain `OperationResult` clearly in README.
- Return overwritten value in `move` operation.
@tomalec
Copy link
Collaborator

tomalec commented May 22, 2017

Thanks, now I feel convinced :)

Copy link
Collaborator

@tomalec tomalec left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should also return result.removed for add operation, as it may replace:
https://tools.ietf.org/html/rfc6902#section-4.1

If the target location specifies an object member that does exist,
that member's value is replaced.

return originalValue;
var overwrittenValue = getValueByPointer(document, this.path);
if (overwrittenValue) {
overwrittenValue = deepClone(overwrittenValue);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to clone for move?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how it originally behaved. In case the move target was overwritten we need to keep a copy of it to return it as removed. I can remove this but there is a spec for it and I thought it's necessary.

Copy link
Collaborator

@tomalec tomalec May 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, fair point.
However, I'm not 100% sure if it's very useful, but it could be expensive for large object.

Could you add short code comment, jsdoc, or readme node why it's needed? We could discuss it later if we would like to improve performance.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! A couple minutes.

}
}
else {
debugger;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this debugger

}
else {
debugger;
returnValue.removed = arrOps[operation.op].call(operation, obj, key, document); // Apply patch
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be cleaner if each arrOps[*] would return applicable object {remove: smth} or {test: true} so you will not have to check it once again here.

}
}
else {
debugger;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please remove debugger

}
else {
debugger;
returnValue.removed = objOps[operation.op].call(operation, obj, key, document); // Apply patch
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above. Maybe we don't need this if at all?

@@ -593,7 +606,9 @@ var jsonpatch;
Object.keys(value_1).forEach(function (key) { return document[key] = value_1[key]; });
}
else {
results[i] = applyOperation(document, patch[i], validateOperation, true).result;
results[i] = applyOperation(document, patch[i], validateOperation);
debugger;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

debugger once again

results[i] = applyOperation(document, patch[i], validateOperation, true).result;
results[i] = applyOperation(document, patch[i], validateOperation);
debugger;
results[i] = results[i].removed || results[i].test;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the results array be an array of result objects?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really, because this is deprecated apply.

else {
debugger;
returnValue.removed = arrOps[operation.op].call(operation, obj, key, document); // Apply patch
returnValue = arrOps[operation.op].call(operation, obj, key, document); // Apply patch
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now, we don't need to create returnValue above, to replace it here .

@alshakero
Copy link
Collaborator Author

@tomalec shall I merge?

results[i] = applyOperation(document, patch[i], validateOperation);
document = results[i].newDocument; // in case root was replaced
}
(<any>results).newDocument = document;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be reflected in the return type, e.g.

export interface PatchResult<T> extends Array<OperationResult<T>> {
  newDocument: T;
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And initialize results array like this?

const results = (<PatchResult<T>>new Array(patch.length));

Copy link
Contributor

@felixfbecker felixfbecker May 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, the cast is needed since at the time of initialising newDocument is not set even though it's not optional. You can also write the slightly prettier

const results = new Array(patch.length) as PatchResult<T>;

Btw, is there a perf benefit in using the array constructor? I once read that array literals are faster

Copy link
Collaborator Author

@alshakero alshakero May 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to this, the constructor is ~200% faster.
https://jsperf.com/array-constructor-vs-literal-modalities

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants