Skip to content

Commit 8d82410

Browse files
committed
implemented attribute_exists condition on replace operations
1 parent c081ca3 commit 8d82410

File tree

5 files changed

+194
-41
lines changed

5 files changed

+194
-41
lines changed

src/main/java/com/github/fge/jsonpatch/ReplaceOperation.java

+10
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
package com.github.fge.jsonpatch;
2323

24+
import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder;
2425
import com.fasterxml.jackson.annotation.JsonCreator;
2526
import com.fasterxml.jackson.annotation.JsonProperty;
2627
import com.fasterxml.jackson.databind.JsonNode;
@@ -47,6 +48,15 @@ public ReplaceOperation(@JsonProperty("path") final JsonPointer path,
4748
{
4849
super("replace", path, value);
4950
}
51+
52+
@Override
53+
public void applyToBuilder(ExpressionSpecBuilder builder) {
54+
//add the set operation
55+
super.applyToBuilder(builder);
56+
//because it is an error to replace a path that does not exist
57+
//add an attribute_exists() condition
58+
builder.withCondition(ExpressionSpecBuilder.attribute_exists(pathGenerator.apply(path)));
59+
}
5060

5161
@Override
5262
public JsonNode apply(final JsonNode node)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package com.github.fge.jsonpatch;
2+
3+
import java.math.BigDecimal;
4+
5+
import org.testng.Assert;
6+
import org.testng.annotations.BeforeTest;
7+
import org.testng.annotations.Test;
8+
9+
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
10+
import com.amazonaws.services.dynamodbv2.document.Item;
11+
import com.amazonaws.services.dynamodbv2.document.PrimaryKey;
12+
import com.amazonaws.services.dynamodbv2.document.Table;
13+
import com.amazonaws.services.dynamodbv2.local.embedded.DynamoDBEmbedded;
14+
import com.amazonaws.services.dynamodbv2.model.AttributeDefinition;
15+
import com.amazonaws.services.dynamodbv2.model.CreateTableRequest;
16+
import com.amazonaws.services.dynamodbv2.model.KeySchemaElement;
17+
import com.amazonaws.services.dynamodbv2.model.KeyType;
18+
import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput;
19+
import com.amazonaws.services.dynamodbv2.model.ResourceNotFoundException;
20+
import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType;
21+
import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder;
22+
import com.amazonaws.services.dynamodbv2.xspec.UpdateItemExpressionSpec;
23+
import com.fasterxml.jackson.databind.JsonNode;
24+
import com.github.fge.jackson.JsonLoader;
25+
import com.google.common.collect.ImmutableMap;
26+
27+
public class JsonPatchToXSpecAdd {
28+
private static final String KEY_ATTRIBUTE_NAME = "key";
29+
30+
private static final String VALUE = "keyValue";
31+
32+
private static final PrimaryKey PK = new PrimaryKey(KEY_ATTRIBUTE_NAME, VALUE);
33+
34+
private static final String TABLE_NAME = "json_patch_test";
35+
36+
private Table table;
37+
38+
39+
@BeforeTest
40+
public void setUp() throws Exception {
41+
AmazonDynamoDB amazonDynamoDB = DynamoDBEmbedded.create().amazonDynamoDB();
42+
try {
43+
amazonDynamoDB.deleteTable(TABLE_NAME);
44+
} catch(ResourceNotFoundException e) {
45+
//do nothing because the first run will not have the table.
46+
}
47+
amazonDynamoDB.createTable(new CreateTableRequest()
48+
.withTableName(TABLE_NAME)
49+
.withProvisionedThroughput(new ProvisionedThroughput(1L, 1L))
50+
.withAttributeDefinitions(new AttributeDefinition()
51+
.withAttributeName(KEY_ATTRIBUTE_NAME)
52+
.withAttributeType(ScalarAttributeType.S))
53+
.withKeySchema(new KeySchemaElement()
54+
.withAttributeName(KEY_ATTRIBUTE_NAME)
55+
.withKeyType(KeyType.HASH)));
56+
table = new Table(amazonDynamoDB, TABLE_NAME);
57+
}
58+
59+
@Test
60+
public void testAddSinglePathNumber() throws Exception {
61+
// setup
62+
table.putItem(Item.fromMap(ImmutableMap.<String, Object> builder()
63+
.put(KEY_ATTRIBUTE_NAME, VALUE)
64+
.build()));
65+
String patchExpression = "[ { \"op\": \"add\", \"path\": \"/a\", \"value\": 1 } ]";
66+
JsonNode jsonNode = JsonLoader.fromString(patchExpression);
67+
JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode);
68+
// exercise
69+
ExpressionSpecBuilder builder = jsonPatch.get();
70+
UpdateItemExpressionSpec spec = builder.buildForUpdate();
71+
table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, spec);
72+
// verify
73+
Item item = table.getItem(PK);
74+
Assert.assertTrue(item.hasAttribute("key"));
75+
Assert.assertEquals(item.getString("key"), "keyValue");
76+
Assert.assertTrue(item.hasAttribute("a"));
77+
Assert.assertEquals(item.getNumber("a").longValue(), 1L);
78+
}
79+
80+
@Test
81+
public void testAddNestedPathString() throws Exception {
82+
// setup
83+
table.putItem(Item.fromMap(ImmutableMap.<String, Object> builder()
84+
.put(KEY_ATTRIBUTE_NAME, VALUE)
85+
.put("a", ImmutableMap.of("a", 1L))
86+
.build()));
87+
88+
String patchExpression = "[ { \"op\": \"add\", \"path\": \"/a/b\", \"value\": \"foo\" } ]";
89+
JsonNode jsonNode = JsonLoader.fromString(patchExpression);
90+
JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode);
91+
// exercise
92+
ExpressionSpecBuilder builder = jsonPatch.get();
93+
UpdateItemExpressionSpec spec = builder.buildForUpdate();
94+
table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, spec);
95+
// verify
96+
Item item = table.getItem(PK);
97+
Assert.assertTrue(item.hasAttribute("key"));
98+
Assert.assertEquals(item.getString("key"), "keyValue");
99+
Assert.assertTrue(item.hasAttribute("a"));
100+
Assert.assertTrue(item.getRawMap("a").containsKey("a"));
101+
Assert.assertEquals(((BigDecimal) item.getMap("a").get("a")).longValue(), 1L);
102+
Assert.assertTrue(item.getMap("a").containsKey("b"));
103+
Assert.assertEquals(item.getMap("a").get("b"), "foo");
104+
}
105+
106+
@Test
107+
public void createItemWithJsonPatch() throws Exception {
108+
// setup
109+
String patchExpression = "[ { \"op\": \"add\", \"path\": \"/a\", \"value\": \"b\" } ]";
110+
JsonNode jsonNode = JsonLoader.fromString(patchExpression);
111+
JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode);
112+
// exercise
113+
ExpressionSpecBuilder builder = jsonPatch.get();
114+
UpdateItemExpressionSpec spec = builder.buildForUpdate();
115+
table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, spec);//throw
116+
// verify
117+
Item item = table.getItem(PK);
118+
Assert.assertTrue(item.hasAttribute("key"));
119+
Assert.assertEquals(item.getString("key"), "keyValue");
120+
Assert.assertTrue(item.hasAttribute("a"));
121+
Assert.assertEquals(item.getString("a"), "b");
122+
}
123+
}

src/test/java/com/github/fge/jsonpatch/JsonPatchToExpressionSpecBuilderRemoveIT.java src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecRemove.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,15 @@
3434
import com.amazonaws.services.dynamodbv2.model.KeySchemaElement;
3535
import com.amazonaws.services.dynamodbv2.model.KeyType;
3636
import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput;
37+
import com.amazonaws.services.dynamodbv2.model.ResourceNotFoundException;
3738
import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType;
3839
import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder;
3940
import com.amazonaws.services.dynamodbv2.xspec.UpdateItemExpressionSpec;
4041
import com.fasterxml.jackson.databind.JsonNode;
4142
import com.github.fge.jackson.JsonLoader;
4243
import com.google.common.collect.ImmutableMap;
4344

44-
public class JsonPatchToExpressionSpecBuilderRemoveIT {
45+
public class JsonPatchToXSpecRemove {
4546
private static final String TABLE_NAME = "json_patch_test";
4647

4748
private static final String KEY_ATTRIBUTE_NAME = "key";
@@ -56,6 +57,11 @@ public class JsonPatchToExpressionSpecBuilderRemoveIT {
5657
@BeforeTest
5758
public void setUp() throws Exception {
5859
AmazonDynamoDB amazonDynamoDB = DynamoDBEmbedded.create().amazonDynamoDB();
60+
try {
61+
amazonDynamoDB.deleteTable(TABLE_NAME);
62+
} catch(ResourceNotFoundException e) {
63+
//do nothing because the first run will not have the table.
64+
}
5965
amazonDynamoDB.createTable(new CreateTableRequest()
6066
.withTableName(TABLE_NAME)
6167
.withProvisionedThroughput(new ProvisionedThroughput(1L, 1L))

src/test/java/com/github/fge/jsonpatch/JsonPatchToExpressionSpecBuilderReplaceIT.java src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecReplace.java

+44-37
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,18 @@
3030
import com.amazonaws.services.dynamodbv2.document.Item;
3131
import com.amazonaws.services.dynamodbv2.document.PrimaryKey;
3232
import com.amazonaws.services.dynamodbv2.document.Table;
33+
import com.amazonaws.services.dynamodbv2.document.UpdateItemOutcome;
34+
import com.amazonaws.services.dynamodbv2.document.internal.InternalUtils;
35+
import com.amazonaws.services.dynamodbv2.document.spec.UpdateItemSpec;
3336
import com.amazonaws.services.dynamodbv2.local.embedded.DynamoDBEmbedded;
3437
import com.amazonaws.services.dynamodbv2.model.AttributeDefinition;
38+
import com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException;
3539
import com.amazonaws.services.dynamodbv2.model.CreateTableRequest;
3640
import com.amazonaws.services.dynamodbv2.model.KeySchemaElement;
3741
import com.amazonaws.services.dynamodbv2.model.KeyType;
3842
import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput;
43+
import com.amazonaws.services.dynamodbv2.model.ResourceNotFoundException;
44+
import com.amazonaws.services.dynamodbv2.model.ReturnValue;
3945
import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType;
4046
import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder;
4147
import com.amazonaws.services.dynamodbv2.xspec.UpdateItemExpressionSpec;
@@ -44,7 +50,7 @@
4450
import com.google.common.collect.ImmutableMap;
4551
import com.google.common.collect.ImmutableSet;
4652

47-
public class JsonPatchToExpressionSpecBuilderReplaceIT {
53+
public class JsonPatchToXSpecReplace {
4854

4955
private static final String KEY_ATTRIBUTE_NAME = "key";
5056

@@ -60,6 +66,11 @@ public class JsonPatchToExpressionSpecBuilderReplaceIT {
6066
@BeforeTest
6167
public void setUp() throws Exception {
6268
AmazonDynamoDB amazonDynamoDB = DynamoDBEmbedded.create().amazonDynamoDB();
69+
try {
70+
amazonDynamoDB.deleteTable(TABLE_NAME);
71+
} catch(ResourceNotFoundException e) {
72+
//do nothing because the first run will not have the table.
73+
}
6374
amazonDynamoDB.createTable(new CreateTableRequest()
6475
.withTableName(TABLE_NAME)
6576
.withProvisionedThroughput(new ProvisionedThroughput(1L, 1L))
@@ -72,12 +83,12 @@ public void setUp() throws Exception {
7283
table = new Table(amazonDynamoDB, TABLE_NAME);
7384
}
7485

75-
/**
76-
* try to update an item that doesnt exist. will create new item
77-
*/
78-
@Test
79-
public void test_replace_singlePath_number() throws Exception {
86+
@Test(expectedExceptions = ConditionalCheckFailedException.class)
87+
public void testReplaceSinglePathNumberNonextant() throws Exception {
8088
// setup
89+
table.putItem(Item.fromMap(ImmutableMap.<String, Object> builder()
90+
.put(KEY_ATTRIBUTE_NAME, VALUE)
91+
.build()));
8192
String patchExpression = "[ { \"op\": \"replace\", \"path\": \"/a\", \"value\": 1 } ]";
8293
JsonNode jsonNode = JsonLoader.fromString(patchExpression);
8394
JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode);
@@ -86,15 +97,40 @@ public void test_replace_singlePath_number() throws Exception {
8697
UpdateItemExpressionSpec spec = builder.buildForUpdate();
8798
table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, spec);
8899
// verify
100+
table.getItem(PK); //throw
101+
}
102+
103+
@Test
104+
public void testReplaceSinglePathNumberExtant() throws Exception {
105+
// setup
106+
table.putItem(Item.fromMap(ImmutableMap.<String, Object> builder()
107+
.put(KEY_ATTRIBUTE_NAME, VALUE)
108+
.put("a", "peekaboo")
109+
.build()));
110+
String patchExpression = "[ { \"op\": \"replace\", \"path\": \"/a\", \"value\": 1 } ]";
111+
JsonNode jsonNode = JsonLoader.fromString(patchExpression);
112+
JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode);
113+
// exercise
114+
ExpressionSpecBuilder builder = jsonPatch.get();
115+
UpdateItemExpressionSpec spec = builder.buildForUpdate();
116+
UpdateItemOutcome out = table.updateItem(new UpdateItemSpec()
117+
.withPrimaryKey(KEY_ATTRIBUTE_NAME, VALUE)
118+
.withExpressionSpec(spec)
119+
.withReturnValues(ReturnValue.ALL_OLD));
120+
121+
Item oldItem = Item.fromMap(InternalUtils.toSimpleMapValue(out.getUpdateItemResult().getAttributes()));
122+
Assert.assertTrue(oldItem.hasAttribute("a"));
123+
Assert.assertEquals(oldItem.getString("a"), "peekaboo");
124+
// verify
89125
Item item = table.getItem(PK);
90126
Assert.assertTrue(item.hasAttribute("key"));
91127
Assert.assertEquals(item.getString("key"), "keyValue");
92128
Assert.assertTrue(item.hasAttribute("a"));
93129
Assert.assertEquals(item.getNumber("a").longValue(), 1L);
94130
}
95131

96-
@Test
97-
public void test_replace_nestedPath_string() throws Exception {
132+
@Test(expectedExceptions = ConditionalCheckFailedException.class)
133+
public void testReplaceNestedPathString() throws Exception {
98134
// setup
99135
table.putItem(Item.fromMap(ImmutableMap.<String, Object> builder()
100136
.put(KEY_ATTRIBUTE_NAME, VALUE)
@@ -108,15 +144,6 @@ public void test_replace_nestedPath_string() throws Exception {
108144
ExpressionSpecBuilder builder = jsonPatch.get();
109145
UpdateItemExpressionSpec spec = builder.buildForUpdate();
110146
table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, spec);
111-
// verify
112-
Item item = table.getItem(PK);
113-
Assert.assertTrue(item.hasAttribute("key"));
114-
Assert.assertEquals(item.getString("key"), "keyValue");
115-
Assert.assertTrue(item.hasAttribute("a"));
116-
Assert.assertTrue(item.getRawMap("a").containsKey("a"));
117-
Assert.assertEquals(((BigDecimal) item.getMap("a").get("a")).longValue(), 1L);
118-
Assert.assertTrue(item.getMap("a").containsKey("b"));
119-
Assert.assertEquals(item.getMap("a").get("b"), "foo");
120147
}
121148

122149
@Test
@@ -162,26 +189,6 @@ public void test_replace_property_toScalar_string() throws Exception {
162189
table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, spec);
163190
}
164191

165-
@Test
166-
public void test_replace_singlePath_numberSet() throws Exception {
167-
// setup
168-
String patchExpression = "[ { \"op\": \"replace\", \"path\": \"/a\", \"value\": [1,2] } ]";
169-
JsonNode jsonNode = JsonLoader.fromString(patchExpression);
170-
JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode);
171-
// exercise
172-
ExpressionSpecBuilder builder = jsonPatch.get();
173-
UpdateItemExpressionSpec spec = builder.buildForUpdate();
174-
table.updateItem(KEY_ATTRIBUTE_NAME, VALUE, spec);
175-
// verify
176-
Item item = table.getItem(PK);
177-
Assert.assertTrue(item.hasAttribute("key"));
178-
Assert.assertEquals(item.getString("key"), "keyValue");
179-
Assert.assertTrue(item.hasAttribute("a"));
180-
//number comparisons are failing so comment this out for now
181-
Assert.assertTrue(item.getList("a").contains(BigDecimal.valueOf(1L)));
182-
Assert.assertTrue(item.getList("a").contains(BigDecimal.valueOf(2L)));
183-
}
184-
185192
@Test
186193
public void test_replace_singlePath_stringSet() throws Exception {
187194
// setup

src/test/java/com/github/fge/jsonpatch/JsonPatchToExpressionSpecBuilderTest.java src/test/java/com/github/fge/jsonpatch/JsonPatchToXSpecTest.java

+10-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import com.fasterxml.jackson.databind.JsonNode;
99
import com.github.fge.jackson.JsonLoader;
1010

11-
public class JsonPatchToExpressionSpecBuilderTest {
11+
public class JsonPatchToXSpecTest {
1212
@Test
1313
public void testEmpty() throws Exception {
1414
// setup
@@ -34,13 +34,19 @@ public void test_replace_singlePath_number() throws Exception {
3434
JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode);
3535
UpdateItemExpressionSpec expectedSpec = new ExpressionSpecBuilder()
3636
.addUpdate(ExpressionSpecBuilder.N("a").set(1))
37+
.withCondition(ExpressionSpecBuilder.attribute_exists("a"))
3738
.buildForUpdate();
3839
// exercise
3940
ExpressionSpecBuilder actual = jsonPatch.get();
4041
// verify
4142
Assert.assertNotNull(actual);
4243
UpdateItemExpressionSpec actualSpec = actual.buildForUpdate();
43-
Assert.assertNull(actualSpec.getConditionExpression());
44+
//the spec builder agressively replaces path components with expression attribute
45+
//with sequentially increasing number strings (#0, #1 etc)
46+
//names in order to avoid name clashes with reserved words/symbols in documents
47+
//"a" was the only path element in the update expression and the only path element
48+
//in the conditions, so it gets the number zero in this example ("attribute_exists(#0)")
49+
Assert.assertEquals(actualSpec.getConditionExpression(), expectedSpec.getConditionExpression());
4450
Assert.assertEquals(actualSpec.getUpdateExpression(), expectedSpec.getUpdateExpression());
4551
Assert.assertEquals(actualSpec.getNameMap(), expectedSpec.getNameMap());
4652
Assert.assertEquals(actualSpec.getValueMap(), expectedSpec.getValueMap());
@@ -54,13 +60,14 @@ public void test_replace_nestedPath_string() throws Exception {
5460
JsonPatch jsonPatch = JsonPatch.fromJson(jsonNode);
5561
UpdateItemExpressionSpec expectedSpec = new ExpressionSpecBuilder()
5662
.addUpdate(ExpressionSpecBuilder.S("a.b").set("foo"))
63+
.withCondition(ExpressionSpecBuilder.attribute_exists("a.b"))
5764
.buildForUpdate();
5865
// exercise
5966
ExpressionSpecBuilder actual = jsonPatch.get();
6067
// verify
6168
Assert.assertNotNull(actual);
6269
UpdateItemExpressionSpec actualSpec = actual.buildForUpdate();
63-
Assert.assertNull(actualSpec.getConditionExpression());
70+
Assert.assertEquals(actualSpec.getConditionExpression(), expectedSpec.getConditionExpression());
6471
Assert.assertEquals(actualSpec.getUpdateExpression(), expectedSpec.getUpdateExpression());
6572
Assert.assertEquals(actualSpec.getNameMap(), expectedSpec.getNameMap());
6673
Assert.assertEquals(actualSpec.getValueMap(), expectedSpec.getValueMap());

0 commit comments

Comments
 (0)