Skip to content

Commit

Permalink
patch: serialization/deserialization of ConsequenceQuery (#684)
Browse files Browse the repository at this point in the history
[changelog]

Fixed: Custom serializer to handle polymorphism of "query" attribute
in ConsequenceQuery

Example:

```json
// query string
"query": "some query string"

// remove attribute (deprecated)
"query": {"remove": ["query1", "query2"]}

// edits attribute
"query": {
   "edits": [
   { "type": "remove", "delete": "old" },
   { "type": "replace", "delete": "new", "insert": "newer" }
   ]
}}
```json
  • Loading branch information
Ant-hem committed Nov 26, 2019
1 parent 78559ec commit 7e8f477
Show file tree
Hide file tree
Showing 4 changed files with 379 additions and 67 deletions.
276 changes: 273 additions & 3 deletions src/Algolia.Search.Test/Serializer/SerializerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -248,10 +248,144 @@ void AssertOredResult(List<List<string>> result)

[Test]
[Parallelizable]
public void TestConsequenceQueryConverter()
public void TestConsequenceQueryAsString()
{
string json = "{\"remove\": [\"term1\", \"term2\"], \"edits\": [{\"type\": \"remove\", \"delete\": \"term3\"}]}";
ConsequenceQuery deserialized = JsonConvert.DeserializeObject<ConsequenceQuery>(json, new ConsequenceQueryConverter());
var payload =
"{\n"
+ " \"objectID\": \"rule-2\",\n"
+ " \"condition\": {\n"
+ " \"pattern\": \"toto\",\n"
+ " \"anchoring\": \"is\"\n"
+ " },\n"
+ " \"consequence\": {\n"
+ " \"params\": {\n"
+ " \"query\": \"tata\",\n"
+ " \"facetFilters\": [[\"facet\"]]\n"
+ " }\n"
+ " }\n"
+ "}";

var rule = JsonConvert.DeserializeObject<Rule>(payload, JsonConfig.AlgoliaJsonSerializerSettings);
Assert.That(rule, Is.Not.Null);
Assert.That(rule.ObjectID, Is.EqualTo("rule-2"));

Assert.That(rule.Condition.Pattern, Is.EqualTo("toto"));
Assert.That(rule.Condition.Anchoring, Is.EqualTo("is"));

Assert.That(rule.Consequence.Params.SearchQuery, Is.EqualTo("tata"));
Assert.That(rule.Consequence.Params.FacetFilters, Has.Count.EqualTo(1));
}

[Test]
[Parallelizable]
public void TestConsequenceQueryAsRemove()
{
var payload =
"{\n"
+ " \"objectID\": \"rule-2\",\n"
+ " \"condition\": {\n"
+ " \"pattern\": \"toto\",\n"
+ " \"anchoring\": \"is\"\n"
+ " },\n"
+ " \"consequence\": {\n"
+ " \"params\": {\n"
+ " \"query\": {\"remove\":[\"lastname\",\"firstname\"]},\n"
+ " \"facetFilters\": [[\"facet\"]]\n"
+ " }\n"
+ " }\n"
+ "}";

var rule = JsonConvert.DeserializeObject<Rule>(payload, JsonConfig.AlgoliaJsonSerializerSettings);

Assert.That(rule, Is.Not.Null);
Assert.That(rule.ObjectID, Is.EqualTo("rule-2"));

Assert.That(rule.Condition.Pattern, Is.EqualTo("toto"));
Assert.That(rule.Condition.Anchoring, Is.EqualTo("is"));

Assert.That(rule.Consequence.Params.Query.Edits.ElementAt(0).Type, Is.EqualTo(EditType.Remove));
Assert.That(rule.Consequence.Params.Query.Edits.ElementAt(0).Delete, Is.EqualTo("lastname"));
Assert.That(rule.Consequence.Params.Query.Edits.ElementAt(0).Insert, Is.Null);

Assert.That(rule.Consequence.Params.Query.Edits.ElementAt(1).Type, Is.EqualTo(EditType.Remove));
Assert.That(rule.Consequence.Params.Query.Edits.ElementAt(1).Delete, Is.EqualTo("firstname"));
Assert.That(rule.Consequence.Params.Query.Edits.ElementAt(1).Insert, Is.Null);

Assert.That(rule.Consequence.Params.FacetFilters, Has.Count.EqualTo(1));
}

[Test]
[Parallelizable]
public void TestConsequenceQueryAsEdits()
{
var payload =
"{\n"
+ " \"objectID\": \"rule-2\",\n"
+ " \"condition\": {\n"
+ " \"pattern\": \"toto\",\n"
+ " \"anchoring\": \"is\"\n"
+ " },\n"
+ " \"consequence\": {\n"
+ " \"params\": {\n"
+ " \"query\":{\n"
+ " \"edits\": [\n"
+ " { \"type\": \"remove\", \"delete\": \"old\" },\n"
+ " { \"type\": \"replace\", \"delete\": \"new\", \"insert\": \"newer\" }\n"
+ " ]\n"
+ "},"
+ " \"facetFilters\": [[\"facet\"]]\n"
+ " }\n"
+ " }\n"
+ "}";


var rule = JsonConvert.DeserializeObject<Rule>(payload, JsonConfig.AlgoliaJsonSerializerSettings);

Assert.That(rule, Is.Not.Null);
Assert.That(rule.ObjectID, Is.EqualTo("rule-2"));

Assert.That(rule.Condition.Pattern, Is.EqualTo("toto"));
Assert.That(rule.Condition.Anchoring, Is.EqualTo("is"));

Assert.That(rule.Consequence.Params.Query.Edits.ElementAt(0).Type, Is.EqualTo(EditType.Remove));
Assert.That(rule.Consequence.Params.Query.Edits.ElementAt(0).Delete, Is.EqualTo("old"));
Assert.That(rule.Consequence.Params.Query.Edits.ElementAt(0).Insert, Is.Null);

Assert.That(rule.Consequence.Params.Query.Edits.ElementAt(1).Type, Is.EqualTo(EditType.Replace));
Assert.That(rule.Consequence.Params.Query.Edits.ElementAt(1).Delete, Is.EqualTo("new"));
Assert.That(rule.Consequence.Params.Query.Edits.ElementAt(1).Insert, Is.EqualTo("newer"));

Assert.That(rule.Consequence.Params.FacetFilters, Has.Count.EqualTo(1));
}

[Test]
[Parallelizable]
public void TestConsequenceQueryEditsAndRemove()
{
var payload =
"{\n"
+ " \"objectID\": \"rule-2\",\n"
+ " \"condition\": {\n"
+ " \"pattern\": \"toto\",\n"
+ " \"anchoring\": \"is\"\n"
+ " },\n"
+ " \"consequence\": {\n"
+ " \"params\": {\n"
+ " \"query\":{\"remove\": [\"term1\", \"term2\"], \"edits\": [{\"type\": \"remove\", \"delete\": \"term3\"}]},"
+ " \"facetFilters\": [[\"facet\"]]\n"
+ " }\n"
+ " }\n"
+ "}";

var rule = JsonConvert.DeserializeObject<Rule>(payload, JsonConfig.AlgoliaJsonSerializerSettings);

Assert.That(rule, Is.Not.Null);
Assert.That(rule.ObjectID, Is.EqualTo("rule-2"));

Assert.That(rule.Condition.Pattern, Is.EqualTo("toto"));
Assert.That(rule.Condition.Anchoring, Is.EqualTo("is"));

ConsequenceQuery deserialized = rule.Consequence.Params.Query;

Assert.AreEqual(3, deserialized.Edits.Count());

Expand All @@ -266,6 +400,142 @@ public void TestConsequenceQueryConverter()
Assert.True(deserialized.Edits.ElementAt(2).Type.Equals("remove"));
Assert.True(deserialized.Edits.ElementAt(2).Delete.Equals("term3"));
Assert.Null(deserialized.Edits.ElementAt(2).Insert);

Assert.That(rule.Consequence.Params.FacetFilters, Has.Count.EqualTo(1));
}

[Test]
[Parallelizable]
public void TestConsequenceQueryQueryStringOverride()
{
Rule rule = new Rule
{
ObjectID = "brand_automatic_faceting",
Enabled = false,
Condition = new Condition { Anchoring = "is", Pattern = "{facet:brand}" },
Consequence =
new Consequence
{
FilterPromotes = false,
Params = new ConsequenceParams
{
SearchQuery = "test",
Query = new ConsequenceQuery
{
Edits = new List<Edit>
{
new Edit { Type = EditType.Remove, Delete = "mobile" },
new Edit
{
Type = EditType.Replace, Delete = "phone", Insert = "ihpone"
},
}
},
Facets = new List<string> { "facet" },
AutomaticFacetFilters = new List<AutomaticFacetFilter>
{
new AutomaticFacetFilter { Facet = "brand", Disjunctive = true, Score = 42 }
}
}
},
Description = "Automatic apply the faceting on `brand` if a brand value is found in the query"
};

var payload = JsonConvert.SerializeObject(rule, JsonConfig.AlgoliaJsonSerializerSettings);
var deserializedRule =
JsonConvert.DeserializeObject<Rule>(payload, JsonConfig.AlgoliaJsonSerializerSettings);

Assert.That(deserializedRule.Consequence.Params.SearchQuery, Is.Null);

Assert.That(deserializedRule.Consequence.Params.Query.Edits.ElementAt(0).Type, Is.EqualTo(EditType.Remove));
Assert.That(deserializedRule.Consequence.Params.Query.Edits.ElementAt(0).Delete, Is.EqualTo("mobile"));
Assert.That(deserializedRule.Consequence.Params.Query.Edits.ElementAt(0).Insert, Is.Null);

Assert.That(deserializedRule.Consequence.Params.Query.Edits.ElementAt(1).Type,
Is.EqualTo(EditType.Replace));
Assert.That(deserializedRule.Consequence.Params.Query.Edits.ElementAt(1).Delete, Is.EqualTo("phone"));
Assert.That(deserializedRule.Consequence.Params.Query.Edits.ElementAt(1).Insert, Is.EqualTo("ihpone"));
}

[Test]
[Parallelizable]
public void RuleSerializationCycle()
{
{
Rule rule = new Rule
{
ObjectID = "brand_automatic_faceting",
Enabled = false,
Condition = new Condition { Anchoring = "is", Pattern = "{facet:brand}" },
Consequence =
new Consequence
{
FilterPromotes = false,
Params = new ConsequenceParams
{
SearchQuery = "test",
Facets = new List<string> { "facet" },
AutomaticFacetFilters = new List<AutomaticFacetFilter>
{
new AutomaticFacetFilter
{
Facet = "brand", Disjunctive = true, Score = 42
}
}
}
},
Description = "Automatic apply the faceting on `brand` if a brand value is found in the query"
};

var payload = JsonConvert.SerializeObject(rule, JsonConfig.AlgoliaJsonSerializerSettings);
var deserializedRule =
JsonConvert.DeserializeObject<Rule>(payload, JsonConfig.AlgoliaJsonSerializerSettings);
Assert.True(TestHelper.AreObjectsEqual(rule, deserializedRule));
}

{
Rule rule = new Rule
{
ObjectID = "brand_automatic_faceting",
Enabled = false,
Condition = new Condition { Anchoring = "is", Pattern = "{facet:brand}" },
Consequence =
new Consequence
{
FilterPromotes = false,
Params = new ConsequenceParams
{
Query = new ConsequenceQuery
{
Edits = new List<Edit>
{
new Edit { Type = EditType.Remove, Delete = "mobile" },
new Edit
{
Type = EditType.Replace,
Delete = "phone",
Insert = "ihpone"
},
}
},
Facets = new List<string> { "facet" },
AutomaticFacetFilters = new List<AutomaticFacetFilter>
{
new AutomaticFacetFilter
{
Facet = "brand", Disjunctive = true, Score = 42
}
}
}
},
Description = "Automatic apply the faceting on `brand` if a brand value is found in the query"
};

var payload = JsonConvert.SerializeObject(rule, JsonConfig.AlgoliaJsonSerializerSettings);
var deserializedRule =
JsonConvert.DeserializeObject<Rule>(payload, JsonConfig.AlgoliaJsonSerializerSettings);
Assert.True(TestHelper.AreObjectsEqual(rule, deserializedRule));
}
}

[Test]
Expand Down
23 changes: 20 additions & 3 deletions src/Algolia.Search/Models/Rules/ConsequenceParams.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,29 @@ namespace Algolia.Search.Models.Rules
public class ConsequenceParams : Query
{
/// <summary>
/// When providing a string, it replaces the entire query string.
/// When providing an object, it describes incremental edits to be made to the query string (but you can’t do both).
/// When providing an object, it describes incremental edits to be made to the query string.
/// </summary>
[JsonConverter(typeof(ConsequenceQueryConverter))]
/// <remarks>Setting a ConsequenceQuery will override SearchQuery if set. Both can't be set at the same time.</remarks>
public ConsequenceQuery Query { get; set; }

/// <summary>
/// Providing a SearchQuery in ConsequenceParams replaces the entire query string for the given pattern.
/// </summary>
/// <remarks>Setting a SearchQuery will override ConsequenceQuery if set. Both can't be set at the same time.</remarks>
[JsonIgnore]
public new string SearchQuery
{
get
{
return Query?.SearchQuery;
}

set
{
Query = new ConsequenceQuery { SearchQuery = value };
}
}

/// <summary>
/// Names of facets to which automatic filtering must be applied; they must match the facet name of a facet value placeholder in the query pattern.
/// </summary>
Expand Down
9 changes: 9 additions & 0 deletions src/Algolia.Search/Models/Rules/ConsequenceQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,24 @@
* THE SOFTWARE.
*/

using Algolia.Search.Serializer;
using Newtonsoft.Json;
using System.Collections.Generic;

namespace Algolia.Search.Models.Rules
{
/// <summary>
/// Consequence query
/// </summary>
[JsonConverter(typeof(ConsequenceQueryConverter))]
public class ConsequenceQuery
{
/// <summary>
/// Stores a SearchQuery if defined.
/// Value could be used during deserialization/serialization
/// </summary>
internal string SearchQuery { get; set; }

/// <summary>
/// List of edits
/// </summary>
Expand Down
Loading

0 comments on commit 7e8f477

Please sign in to comment.