From e013f8514c598ba23fc469b3a3b723455c1cb642 Mon Sep 17 00:00:00 2001 From: Alexander Patrikalakis Date: Fri, 19 Aug 2016 23:21:07 +0900 Subject: [PATCH] implemented attribute_exists condition on replace operations --- .../fge/jsonpatch/ReplaceOperation.java | 10 ++ .../fge/jsonpatch/JsonPatchToXSpecAdd.java | 123 ++++++++++++++++++ ...oveIT.java => JsonPatchToXSpecRemove.java} | 8 +- ...ceIT.java => JsonPatchToXSpecReplace.java} | 89 +++++++------ ...derTest.java => JsonPatchToXSpecTest.java} | 13 +- 5 files changed, 202 insertions(+), 41 deletions(-) create mode 100644 src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecAdd.java rename src/test/java/com/github/fge/jsonpatch/{JsonPatchToExpressionSpecBuilderRemoveIT.java => JsonPatchToXSpecRemove.java} (95%) rename src/test/java/com/github/fge/jsonpatch/{JsonPatchToExpressionSpecBuilderReplaceIT.java => JsonPatchToXSpecReplace.java} (82%) rename src/test/java/com/github/fge/jsonpatch/{JsonPatchToExpressionSpecBuilderTest.java => JsonPatchToXSpecTest.java} (82%) diff --git a/src/main/java/com/github/fge/jsonpatch/ReplaceOperation.java b/src/main/java/com/github/fge/jsonpatch/ReplaceOperation.java index 95a17ea9..8a1f4b78 100644 --- a/src/main/java/com/github/fge/jsonpatch/ReplaceOperation.java +++ b/src/main/java/com/github/fge/jsonpatch/ReplaceOperation.java @@ -21,6 +21,7 @@ package com.github.fge.jsonpatch; +import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; @@ -47,6 +48,15 @@ public ReplaceOperation(@JsonProperty("path") final JsonPointer path, { super("replace", path, value); } + + @Override + public void applyToBuilder(ExpressionSpecBuilder builder) { + //add the set operation + super.applyToBuilder(builder); + //because it is an error to replace a path that does not exist + //add an attribute_exists() condition + builder.withCondition(ExpressionSpecBuilder.attribute_exists(pathGenerator.apply(path))); + } @Override public JsonNode apply(final JsonNode node) diff --git a/src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecAdd.java b/src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecAdd.java new file mode 100644 index 00000000..ae14e095 --- /dev/null +++ b/src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecAdd.java @@ -0,0 +1,123 @@ +package com.github.fge.jsonpatch; + +import java.math.BigDecimal; + +import org.testng.Assert; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; +import com.amazonaws.services.dynamodbv2.document.Item; +import com.amazonaws.services.dynamodbv2.document.PrimaryKey; +import com.amazonaws.services.dynamodbv2.document.Table; +import com.amazonaws.services.dynamodbv2.local.embedded.DynamoDBEmbedded; +import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; +import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; +import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; +import com.amazonaws.services.dynamodbv2.model.KeyType; +import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; +import com.amazonaws.services.dynamodbv2.model.ResourceNotFoundException; +import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType; +import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder; +import com.amazonaws.services.dynamodbv2.xspec.UpdateItemExpressionSpec; +import com.fasterxml.jackson.databind.JsonNode; +import com.github.fge.jackson.JsonLoader; +import com.google.common.collect.ImmutableMap; + +public class JsonPatchToXSpecAdd { + private static final String KEY_ATTRIBUTE_NAME = "key"; + + private static final String VALUE = "keyValue"; + + private static final PrimaryKey PK = new PrimaryKey(KEY_ATTRIBUTE_NAME, VALUE); + + private static final String TABLE_NAME = "json_patch_test"; + + private Table table; + + + @BeforeTest + public void setUp() throws Exception { + AmazonDynamoDB amazonDynamoDB = DynamoDBEmbedded.create().amazonDynamoDB(); + try { + amazonDynamoDB.deleteTable(TABLE_NAME); + } catch(ResourceNotFoundException e) { + //do nothing because the first run will not have the table. + } + amazonDynamoDB.createTable(new CreateTableRequest() + .withTableName(TABLE_NAME) + .withProvisionedThroughput(new ProvisionedThroughput(1L, 1L)) + .withAttributeDefinitions(new AttributeDefinition() + .withAttributeName(KEY_ATTRIBUTE_NAME) + .withAttributeType(ScalarAttributeType.S)) + .withKeySchema(new KeySchemaElement() + .withAttributeName(KEY_ATTRIBUTE_NAME) + .withKeyType(KeyType.HASH))); + table = new Table(amazonDynamoDB, TABLE_NAME); + } + + @Test + public void testAddSinglePathNumber() throws Exception { + // setup + table.putItem(Item.fromMap(ImmutableMap. builder() + .put(KEY_ATTRIBUTE_NAME, VALUE) + .build())); + String patchExpression = "[ { \"op\": \"add\", \"path\": \"/a\", \"value\": 1 } ]"; + JsonNode jsonNode = JsonLoader.fromString(patchExpression); + JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); + // exercise + ExpressionSpecBuilder builder = jsonPatch.get(); + UpdateItemExpressionSpec spec = builder.buildForUpdate(); + table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, spec); + // verify + Item item = table.getItem(PK); + Assert.assertTrue(item.hasAttribute("key")); + Assert.assertEquals(item.getString("key"), "keyValue"); + Assert.assertTrue(item.hasAttribute("a")); + Assert.assertEquals(item.getNumber("a").longValue(), 1L); + } + + @Test + public void testAddNestedPathString() throws Exception { + // setup + table.putItem(Item.fromMap(ImmutableMap. builder() + .put(KEY_ATTRIBUTE_NAME, VALUE) + .put("a", ImmutableMap.of("a", 1L)) + .build())); + + String patchExpression = "[ { \"op\": \"add\", \"path\": \"/a/b\", \"value\": \"foo\" } ]"; + JsonNode jsonNode = JsonLoader.fromString(patchExpression); + JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); + // exercise + ExpressionSpecBuilder builder = jsonPatch.get(); + UpdateItemExpressionSpec spec = builder.buildForUpdate(); + table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, spec); + // verify + Item item = table.getItem(PK); + Assert.assertTrue(item.hasAttribute("key")); + Assert.assertEquals(item.getString("key"), "keyValue"); + Assert.assertTrue(item.hasAttribute("a")); + Assert.assertTrue(item.getRawMap("a").containsKey("a")); + Assert.assertEquals(((BigDecimal) item.getMap("a").get("a")).longValue(), 1L); + Assert.assertTrue(item.getMap("a").containsKey("b")); + Assert.assertEquals(item.getMap("a").get("b"), "foo"); + } + + @Test + public void createItemWithJsonPatch() throws Exception { + // setup + String patchExpression = "[ { \"op\": \"add\", \"path\": \"/a\", \"value\": \"b\" } ]"; + JsonNode jsonNode = JsonLoader.fromString(patchExpression); + JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); + // exercise + ExpressionSpecBuilder builder = jsonPatch.get(); + UpdateItemExpressionSpec spec = builder.buildForUpdate(); + table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, spec);//throw + // verify + Item item = table.getItem(PK); + Assert.assertTrue(item.hasAttribute("key")); + Assert.assertEquals(item.getString("key"), "keyValue"); + Assert.assertTrue(item.hasAttribute("a")); + Assert.assertEquals(item.getString("a"), "b"); + } +} diff --git a/src/test/java/com/github/fge/jsonpatch/JsonPatchToExpressionSpecBuilderRemoveIT.java b/src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecRemove.java similarity index 95% rename from src/test/java/com/github/fge/jsonpatch/JsonPatchToExpressionSpecBuilderRemoveIT.java rename to src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecRemove.java index 4291d8ee..618bf551 100644 --- a/src/test/java/com/github/fge/jsonpatch/JsonPatchToExpressionSpecBuilderRemoveIT.java +++ b/src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecRemove.java @@ -34,6 +34,7 @@ import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; import com.amazonaws.services.dynamodbv2.model.KeyType; import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; +import com.amazonaws.services.dynamodbv2.model.ResourceNotFoundException; import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType; import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder; import com.amazonaws.services.dynamodbv2.xspec.UpdateItemExpressionSpec; @@ -41,7 +42,7 @@ import com.github.fge.jackson.JsonLoader; import com.google.common.collect.ImmutableMap; -public class JsonPatchToExpressionSpecBuilderRemoveIT { +public class JsonPatchToXSpecRemove { private static final String TABLE_NAME = "json_patch_test"; private static final String KEY_ATTRIBUTE_NAME = "key"; @@ -56,6 +57,11 @@ public class JsonPatchToExpressionSpecBuilderRemoveIT { @BeforeTest public void setUp() throws Exception { AmazonDynamoDB amazonDynamoDB = DynamoDBEmbedded.create().amazonDynamoDB(); + try { + amazonDynamoDB.deleteTable(TABLE_NAME); + } catch(ResourceNotFoundException e) { + //do nothing because the first run will not have the table. + } amazonDynamoDB.createTable(new CreateTableRequest() .withTableName(TABLE_NAME) .withProvisionedThroughput(new ProvisionedThroughput(1L, 1L)) diff --git a/src/test/java/com/github/fge/jsonpatch/JsonPatchToExpressionSpecBuilderReplaceIT.java b/src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecReplace.java similarity index 82% rename from src/test/java/com/github/fge/jsonpatch/JsonPatchToExpressionSpecBuilderReplaceIT.java rename to src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecReplace.java index 73c5245c..95013de0 100644 --- a/src/test/java/com/github/fge/jsonpatch/JsonPatchToExpressionSpecBuilderReplaceIT.java +++ b/src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecReplace.java @@ -30,12 +30,18 @@ import com.amazonaws.services.dynamodbv2.document.Item; import com.amazonaws.services.dynamodbv2.document.PrimaryKey; import com.amazonaws.services.dynamodbv2.document.Table; +import com.amazonaws.services.dynamodbv2.document.UpdateItemOutcome; +import com.amazonaws.services.dynamodbv2.document.internal.InternalUtils; +import com.amazonaws.services.dynamodbv2.document.spec.UpdateItemSpec; import com.amazonaws.services.dynamodbv2.local.embedded.DynamoDBEmbedded; import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; +import com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException; import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; import com.amazonaws.services.dynamodbv2.model.KeyType; import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; +import com.amazonaws.services.dynamodbv2.model.ResourceNotFoundException; +import com.amazonaws.services.dynamodbv2.model.ReturnValue; import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType; import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder; import com.amazonaws.services.dynamodbv2.xspec.UpdateItemExpressionSpec; @@ -44,7 +50,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -public class JsonPatchToExpressionSpecBuilderReplaceIT { +public class JsonPatchToXSpecReplace { private static final String KEY_ATTRIBUTE_NAME = "key"; @@ -60,6 +66,11 @@ public class JsonPatchToExpressionSpecBuilderReplaceIT { @BeforeTest public void setUp() throws Exception { AmazonDynamoDB amazonDynamoDB = DynamoDBEmbedded.create().amazonDynamoDB(); + try { + amazonDynamoDB.deleteTable(TABLE_NAME); + } catch(ResourceNotFoundException e) { + //do nothing because the first run will not have the table. + } amazonDynamoDB.createTable(new CreateTableRequest() .withTableName(TABLE_NAME) .withProvisionedThroughput(new ProvisionedThroughput(1L, 1L)) @@ -72,12 +83,12 @@ public void setUp() throws Exception { table = new Table(amazonDynamoDB, TABLE_NAME); } - /** - * try to update an item that doesnt exist. will create new item - */ - @Test - public void test_replace_singlePath_number() throws Exception { + @Test(expectedExceptions = ConditionalCheckFailedException.class) + public void testReplaceSinglePathNumberNonextant() throws Exception { // setup + table.putItem(Item.fromMap(ImmutableMap. builder() + .put(KEY_ATTRIBUTE_NAME, VALUE) + .build())); String patchExpression = "[ { \"op\": \"replace\", \"path\": \"/a\", \"value\": 1 } ]"; JsonNode jsonNode = JsonLoader.fromString(patchExpression); JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); @@ -86,6 +97,31 @@ public void test_replace_singlePath_number() throws Exception { UpdateItemExpressionSpec spec = builder.buildForUpdate(); table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, spec); // verify + table.getItem(PK); //throw + } + + @Test + public void testReplaceSinglePathNumberExtant() throws Exception { + // setup + table.putItem(Item.fromMap(ImmutableMap. builder() + .put(KEY_ATTRIBUTE_NAME, VALUE) + .put("a", "peekaboo") + .build())); + String patchExpression = "[ { \"op\": \"replace\", \"path\": \"/a\", \"value\": 1 } ]"; + JsonNode jsonNode = JsonLoader.fromString(patchExpression); + JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); + // exercise + ExpressionSpecBuilder builder = jsonPatch.get(); + UpdateItemExpressionSpec spec = builder.buildForUpdate(); + UpdateItemOutcome out = table.updateItem(new UpdateItemSpec() + .withPrimaryKey(KEY_ATTRIBUTE_NAME, VALUE) + .withExpressionSpec(spec) + .withReturnValues(ReturnValue.ALL_OLD)); + + Item oldItem = Item.fromMap(InternalUtils.toSimpleMapValue(out.getUpdateItemResult().getAttributes())); + Assert.assertTrue(oldItem.hasAttribute("a")); + Assert.assertEquals(oldItem.getString("a"), "peekaboo"); + // verify Item item = table.getItem(PK); Assert.assertTrue(item.hasAttribute("key")); Assert.assertEquals(item.getString("key"), "keyValue"); @@ -93,8 +129,8 @@ public void test_replace_singlePath_number() throws Exception { Assert.assertEquals(item.getNumber("a").longValue(), 1L); } - @Test - public void test_replace_nestedPath_string() throws Exception { + @Test(expectedExceptions = ConditionalCheckFailedException.class) + public void testReplaceNestedPathString() throws Exception { // setup table.putItem(Item.fromMap(ImmutableMap. builder() .put(KEY_ATTRIBUTE_NAME, VALUE) @@ -108,15 +144,6 @@ public void test_replace_nestedPath_string() throws Exception { ExpressionSpecBuilder builder = jsonPatch.get(); UpdateItemExpressionSpec spec = builder.buildForUpdate(); table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, spec); - // verify - Item item = table.getItem(PK); - Assert.assertTrue(item.hasAttribute("key")); - Assert.assertEquals(item.getString("key"), "keyValue"); - Assert.assertTrue(item.hasAttribute("a")); - Assert.assertTrue(item.getRawMap("a").containsKey("a")); - Assert.assertEquals(((BigDecimal) item.getMap("a").get("a")).longValue(), 1L); - Assert.assertTrue(item.getMap("a").containsKey("b")); - Assert.assertEquals(item.getMap("a").get("b"), "foo"); } @Test @@ -162,29 +189,13 @@ public void test_replace_property_toScalar_string() throws Exception { table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, spec); } - @Test - public void test_replace_singlePath_numberSet() throws Exception { - // setup - String patchExpression = "[ { \"op\": \"replace\", \"path\": \"/a\", \"value\": [1,2] } ]"; - JsonNode jsonNode = JsonLoader.fromString(patchExpression); - JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); - // exercise - ExpressionSpecBuilder builder = jsonPatch.get(); - UpdateItemExpressionSpec spec = builder.buildForUpdate(); - table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, spec); - // verify - Item item = table.getItem(PK); - Assert.assertTrue(item.hasAttribute("key")); - Assert.assertEquals(item.getString("key"), "keyValue"); - Assert.assertTrue(item.hasAttribute("a")); - //number comparisons are failing so comment this out for now - Assert.assertTrue(item.getList("a").contains(BigDecimal.valueOf(1L))); - Assert.assertTrue(item.getList("a").contains(BigDecimal.valueOf(2L))); - } - @Test public void test_replace_singlePath_stringSet() throws Exception { // setup + table.putItem(Item.fromMap(ImmutableMap. builder() + .put(KEY_ATTRIBUTE_NAME, VALUE) + .put("a", 1L) + .build())); String patchExpression = "[ { \"op\": \"replace\", \"path\": \"/a\", \"value\": [\"foo\",\"bar\"] } ]"; JsonNode jsonNode = JsonLoader.fromString(patchExpression); JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); @@ -228,6 +239,10 @@ public void test_replace_replaceExisting_singlePath_stringSet() throws Exception @Test public void test_replace_singlePath_object() throws Exception { + table.putItem(Item.fromMap(ImmutableMap. builder() + .put(KEY_ATTRIBUTE_NAME, VALUE) + .put("a", 1L) + .build())); // setup String patchExpression = "[ { \"op\": \"replace\", \"path\": \"/a\", \"value\": {\"b\": \"c\", \"d\": 1} } ]"; JsonNode jsonNode = JsonLoader.fromString(patchExpression); diff --git a/src/test/java/com/github/fge/jsonpatch/JsonPatchToExpressionSpecBuilderTest.java b/src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecTest.java similarity index 82% rename from src/test/java/com/github/fge/jsonpatch/JsonPatchToExpressionSpecBuilderTest.java rename to src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecTest.java index 553c8cb6..6d2bc2f9 100644 --- a/src/test/java/com/github/fge/jsonpatch/JsonPatchToExpressionSpecBuilderTest.java +++ b/src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecTest.java @@ -8,7 +8,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.github.fge.jackson.JsonLoader; -public class JsonPatchToExpressionSpecBuilderTest { +public class JsonPatchToXSpecTest { @Test public void testEmpty() throws Exception { // setup @@ -34,13 +34,19 @@ public void test_replace_singlePath_number() throws Exception { JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); UpdateItemExpressionSpec expectedSpec = new ExpressionSpecBuilder() .addUpdate(ExpressionSpecBuilder.N("a").set(1)) + .withCondition(ExpressionSpecBuilder.attribute_exists("a")) .buildForUpdate(); // exercise ExpressionSpecBuilder actual = jsonPatch.get(); // verify Assert.assertNotNull(actual); UpdateItemExpressionSpec actualSpec = actual.buildForUpdate(); - Assert.assertNull(actualSpec.getConditionExpression()); + //the spec builder agressively replaces path components with expression attribute + //with sequentially increasing number strings (#0, #1 etc) + //names in order to avoid name clashes with reserved words/symbols in documents + //"a" was the only path element in the update expression and the only path element + //in the conditions, so it gets the number zero in this example ("attribute_exists(#0)") + Assert.assertEquals(actualSpec.getConditionExpression(), expectedSpec.getConditionExpression()); Assert.assertEquals(actualSpec.getUpdateExpression(), expectedSpec.getUpdateExpression()); Assert.assertEquals(actualSpec.getNameMap(), expectedSpec.getNameMap()); Assert.assertEquals(actualSpec.getValueMap(), expectedSpec.getValueMap()); @@ -54,13 +60,14 @@ public void test_replace_nestedPath_string() throws Exception { JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode); UpdateItemExpressionSpec expectedSpec = new ExpressionSpecBuilder() .addUpdate(ExpressionSpecBuilder.S("a.b").set("foo")) + .withCondition(ExpressionSpecBuilder.attribute_exists("a.b")) .buildForUpdate(); // exercise ExpressionSpecBuilder actual = jsonPatch.get(); // verify Assert.assertNotNull(actual); UpdateItemExpressionSpec actualSpec = actual.buildForUpdate(); - Assert.assertNull(actualSpec.getConditionExpression()); + Assert.assertEquals(actualSpec.getConditionExpression(), expectedSpec.getConditionExpression()); Assert.assertEquals(actualSpec.getUpdateExpression(), expectedSpec.getUpdateExpression()); Assert.assertEquals(actualSpec.getNameMap(), expectedSpec.getNameMap()); Assert.assertEquals(actualSpec.getValueMap(), expectedSpec.getValueMap());