Skip to content

Commit

Permalink
implemented attribute_exists condition on replace operations
Browse files Browse the repository at this point in the history
  • Loading branch information
cm-alexp committed Aug 19, 2016
1 parent c081ca3 commit e013f85
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 41 deletions.
10 changes: 10 additions & 0 deletions src/main/java/com/github/fge/jsonpatch/ReplaceOperation.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
Expand Down
123 changes: 123 additions & 0 deletions src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecAdd.java
Original file line number Diff line number Diff line change
@@ -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.<String, Object> 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.<String, Object> 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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,15 @@
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 JsonPatchToExpressionSpecBuilderRemoveIT {
public class JsonPatchToXSpecRemove {
private static final String TABLE_NAME = "json_patch_test";

private static final String KEY_ATTRIBUTE_NAME = "key";
Expand All @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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";

Expand All @@ -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))
Expand All @@ -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.<String, Object> 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);
Expand All @@ -86,15 +97,40 @@ 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.<String, Object> 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");
Assert.assertTrue(item.hasAttribute("a"));
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.<String, Object> builder()
.put(KEY_ATTRIBUTE_NAME, VALUE)
Expand All @@ -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
Expand Down Expand Up @@ -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.<String, Object> 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);
Expand Down Expand Up @@ -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.<String, Object> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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());
Expand All @@ -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());
Expand Down

0 comments on commit e013f85

Please sign in to comment.