use crate::contexts::attribute_reference::AttributeName;
use crate::contexts::context::Kind;
use crate::contexts::context_serde_helpers::*;
use crate::{AttributeValue, MultiContextBuilder};
use crate::{Context, ContextBuilder};
use serde::de;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_with::skip_serializing_none;
use std::collections::HashMap;
use std::convert::TryFrom;
pub(super) enum ContextVariant {
Multi(MultiKindContext),
Single(SingleKindStandaloneContext),
Implicit(UserFormat),
}
#[derive(Serialize, Deserialize)]
pub(super) struct MultiKindContext {
kind: String,
#[serde(flatten)]
contexts: HashMap<Kind, SingleKindContext>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct SingleKindStandaloneContext {
kind: Kind,
#[serde(flatten)]
context: SingleKindContext,
}
#[skip_serializing_none]
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct SingleKindContext {
key: String,
name: Option<String>,
#[serde(skip_serializing_if = "is_false_bool_option")]
anonymous: Option<bool>,
#[serde(flatten)]
attributes: HashMap<String, AttributeValue>,
#[serde(rename = "_meta", skip_serializing_if = "is_none_meta_option")]
meta: Option<Meta>,
}
#[skip_serializing_none]
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct UserFormat {
key: String,
name: Option<String>,
secondary: Option<String>,
anonymous: Option<bool>,
custom: Option<HashMap<String, AttributeValue>>,
private_attribute_names: Option<Vec<AttributeName>>,
first_name: Option<String>,
last_name: Option<String>,
avatar: Option<String>,
email: Option<String>,
country: Option<String>,
ip: Option<String>,
}
#[skip_serializing_none]
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct Meta {
pub secondary: Option<String>,
#[serde(skip_serializing_if = "is_empty_vec_option")]
pub private_attributes: Option<Vec<String>>,
}
impl<'de> Deserialize<'de> for ContextVariant {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "camelCase")]
enum Tag {
Multi,
Custom(String),
}
let v = serde_json::Value::deserialize(deserializer)?;
match Option::deserialize(&v["kind"]).map_err(de::Error::custom)? {
None if v.get("kind").is_some() => {
Err(de::Error::custom("context kind cannot be null"))
}
None => {
let user = UserFormat::deserialize(v).map_err(de::Error::custom)?;
Ok(ContextVariant::Implicit(user))
}
Some(Tag::Multi) => {
let multi = MultiKindContext::deserialize(v).map_err(de::Error::custom)?;
Ok(ContextVariant::Multi(multi))
}
Some(Tag::Custom(kind)) if kind.is_empty() => {
Err(de::Error::custom("context kind cannot be empty string"))
}
Some(Tag::Custom(_)) => {
let single =
SingleKindStandaloneContext::deserialize(v).map_err(de::Error::custom)?;
Ok(ContextVariant::Single(single))
}
}
}
}
impl Serialize for ContextVariant {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
ContextVariant::Multi(multi) => multi.serialize(serializer),
ContextVariant::Single(single) => single.serialize(serializer),
ContextVariant::Implicit(_) => {
unimplemented!("cannot serialize implicit user contexts")
}
}
}
}
impl From<Context> for ContextVariant {
fn from(c: Context) -> Self {
match c.kind {
kind if kind.is_multi() => ContextVariant::Multi(MultiKindContext {
kind: "multi".to_owned(),
contexts: c
.contexts
.expect("multi-kind context must contain at least one nested context")
.into_iter()
.map(single_kind_context_from)
.collect(),
}),
_ => {
let (kind, nested) = single_kind_context_from(c);
ContextVariant::Single(SingleKindStandaloneContext {
kind,
context: nested,
})
}
}
}
}
fn single_kind_context_from(c: Context) -> (Kind, SingleKindContext) {
(
c.kind,
SingleKindContext {
key: c.key,
name: c.name,
anonymous: Some(c.anonymous),
attributes: c.attributes,
meta: Some(Meta {
secondary: c.secondary,
private_attributes: c
.private_attributes
.map(|attrs| attrs.into_iter().map(String::from).collect()),
}),
},
)
}
impl TryFrom<ContextVariant> for Context {
type Error = String;
fn try_from(variant: ContextVariant) -> Result<Self, Self::Error> {
match variant {
ContextVariant::Multi(m) => {
let mut multi_builder = MultiContextBuilder::new();
for (kind, context) in m.contexts {
let mut builder = ContextBuilder::new(context.key.clone());
let context = build_context(&mut builder, context).kind(kind).build()?;
multi_builder.add_context(context);
}
multi_builder.build()
}
ContextVariant::Single(context) => {
let mut builder = ContextBuilder::new(context.context.key.clone());
build_context(&mut builder, context.context)
.kind(context.kind)
.build()
}
ContextVariant::Implicit(user) => {
let mut builder = ContextBuilder::new(user.key.clone());
builder.allow_empty_key();
build_context_from_implicit_user(&mut builder, user).build()
}
}
}
}
fn build_context(b: &mut ContextBuilder, context: SingleKindContext) -> &mut ContextBuilder {
for (key, attr) in context.attributes {
b.set_value(key.as_str(), attr);
}
if let Some(anonymous) = context.anonymous {
b.anonymous(anonymous);
}
if let Some(name) = context.name {
b.name(name);
}
if let Some(meta) = context.meta {
if let Some(secondary) = meta.secondary {
b.secondary(secondary);
}
if let Some(private_attributes) = meta.private_attributes {
for attribute in private_attributes {
b.add_private_attribute(attribute);
}
}
}
b
}
fn should_skip_custom_attribute(attr_name: &str) -> bool {
matches!(attr_name, "kind" | "key" | "name" | "anonymous" | "_meta")
}
fn build_context_from_implicit_user(
b: &mut ContextBuilder,
user: UserFormat,
) -> &mut ContextBuilder {
if let Some(anonymous) = user.anonymous {
b.anonymous(anonymous);
}
if let Some(secondary) = user.secondary {
b.secondary(secondary);
}
if let Some(name) = user.name {
b.name(name);
}
if let Some(x) = user.avatar {
b.set_string("avatar", x);
}
if let Some(x) = user.first_name {
b.set_string("firstName", x);
}
if let Some(x) = user.last_name {
b.set_string("lastName", x);
}
if let Some(x) = user.country {
b.set_string("country", x);
}
if let Some(x) = user.email {
b.set_string("email", x);
}
if let Some(x) = user.ip {
b.set_string("ip", x);
}
if let Some(custom) = user.custom {
for (key, attr) in custom {
if !should_skip_custom_attribute(&key) {
b.set_value(key.as_str(), attr);
}
}
}
if let Some(attributes) = user.private_attribute_names {
for attribute in attributes {
b.add_private_attribute(attribute);
}
}
b
}
#[cfg(test)]
mod tests {
use crate::contexts::context_serde::{ContextVariant, UserFormat};
use crate::{AttributeValue, Context, ContextBuilder, MultiContextBuilder};
use assert_json_diff::assert_json_eq;
use maplit::hashmap;
use serde_json::json;
use std::convert::TryFrom;
use test_case::test_case;
#[test_case(json!({"key" : "foo"}),
json!({"kind" : "user", "key" : "foo"}))]
#[test_case(json!({"key" : "foo", "name" : "bar"}),
json!({"kind" : "user", "key" : "foo", "name" : "bar"}))]
#[test_case(json!({"key" : "foo", "custom" : {"a" : "b"}}),
json!({"kind" : "user", "key" : "foo", "a" : "b"}))]
#[test_case(json!({"key" : "foo", "anonymous" : true}),
json!({"kind" : "user", "key" : "foo", "anonymous" : true}))]
#[test_case(json!({"key" : "foo", "secondary" : "bar"}),
json!({"kind" : "user", "key" : "foo", "_meta" : {"secondary" : "bar"}}))]
#[test_case(json!({"key" : "foo", "ip" : "1", "privateAttributeNames" : ["ip"]}),
json!({"kind" : "user", "key" : "foo", "ip" : "1", "_meta" : { "privateAttributes" : ["ip"]} }))]
#[test_case(
json!({
"key" : "foo",
"name" : "bar",
"anonymous" : true,
"custom" : {
"kind": ".",
"key": ".",
"name": ".",
"anonymous": true,
"_meta": true,
"a": 1.0
}
}),
json!({
"kind": "user",
"key": "foo",
"name": "bar",
"anonymous": true,
"a": 1.0
})
)]
fn implicit_context_conversion(from: serde_json::Value, to: serde_json::Value) {
let context: Result<ContextVariant, _> = serde_json::from_value(from);
match context {
Ok(variant) => {
assert!(matches!(variant, ContextVariant::Implicit(_)));
match Context::try_from(variant) {
Ok(context) => {
assert_json_eq!(to, context);
}
Err(e) => panic!("variant should convert to context without error: {:?}", e),
}
}
Err(e) => panic!("test JSON should parse without error: {:?}", e),
}
}
#[test_case(json!({"kind" : "org", "key" : "foo"}))]
#[test_case(json!({"kind" : "user", "key" : "foo"}))]
#[test_case(json!({"kind" : "foo", "key" : "bar", "anonymous" : true}))]
#[test_case(json!({"kind" : "foo", "name" : "Foo", "key" : "bar", "a" : "b", "_meta" : {"secondary" : "baz", "privateAttributes" : ["a"]}}))]
#[test_case(json!({"kind" : "foo", "key" : "bar", "object" : {"a" : "b"}}))]
fn single_kind_context_roundtrip_identical(from: serde_json::Value) {
match serde_json::from_value::<Context>(from.clone()) {
Ok(context) => {
assert_json_eq!(from, context);
}
Err(e) => panic!("test JSON should convert to context without error: {:?}", e),
}
}
#[test_case(json!({"kind" : true, "key" : "a"}))]
#[test_case(json!({"kind" : null, "key" : "b"}))]
#[test_case(json!({"kind" : {}, "key" : "c"}))]
#[test_case(json!({"kind" : 1, "key" : "d"}))]
#[test_case(json!({"kind" : [], "key" : "e"}))]
fn reject_null_or_non_string_kind(from: serde_json::Value) {
match serde_json::from_value::<ContextVariant>(from) {
Err(e) => println!("{:?}", e),
Ok(c) => panic!(
"expected conversion to fail, but got: {:?}",
serde_json::to_string(&c)
),
}
}
#[test_case(json!({"kind" : "kind", "key" : "a"}))]
#[test_case(json!({"kind" : "", "key" : "a"}))]
#[test_case(json!({"kind" : "multi"}))]
#[test_case(json!({"kind" : "multi", "key" : "a"}))]
#[test_case(json!({"kind" : "user", "key" : "a", "_meta" : "string"}))]
#[test_case(json!({"a" : "b"}))]
#[test_case(json!({"kind" : "user"}))]
#[test_case(json!({"kind" : "user", "key" : ""}))]
fn reject_invalid_contexts(from: serde_json::Value) {
match serde_json::from_value::<Context>(from) {
Err(e) => println!("got expected error: {:?}", e),
Ok(c) => panic!(
"expected conversion to fail, but got: {:?}",
serde_json::to_string(&c)
),
}
}
#[test]
fn empty_key_allowed_for_implicit_user() {
let json_in = json!({
"key" : "",
});
let json_out = json!({
"kind" : "user",
"key" : ""
});
let context: Context = serde_json::from_value(json_in).unwrap();
assert_json_eq!(json_out, json!(context));
}
#[test]
fn unrecognized_implicit_user_props_are_ignored_without_error() {
let json = json!({
"key" : "foo",
"ip": "b",
"unknown-1" : "ignored",
"unknown-2" : "ignored",
"unknown-3" : "ignored",
});
match serde_json::from_value::<ContextVariant>(json) {
Err(e) => panic!("expected user to deserialize without error: {:?}", e),
Ok(c) => match c {
ContextVariant::Implicit(user) => {
assert_eq!(user.ip.unwrap_or_else(|| "".to_string()), "b");
}
_ => panic!("expected user format"),
},
}
}
#[test]
fn multi_kind_context_roundtrip() {
let json = json!({
"kind" : "multi",
"foo" : {
"key" : "foo_key",
"name" : "foo_name",
"anonymous" : true
},
"bar" : {
"key" : "bar_key",
"some" : "attribute",
"_meta" : {
"secondary" : "bar_two",
"privateAttributes" : [
"some"
]
}
},
"baz" : {
"key" : "baz_key",
}
});
let multi: Context = serde_json::from_value(json.clone()).unwrap();
assert_json_eq!(multi, json);
}
#[test]
fn builder_generates_correct_single_kind_context() {
let json = json!({
"kind" : "org",
"key" : "foo",
"anonymous" : true,
"_meta" : {
"privateAttributes" : ["a", "b", "/c/d"],
"secondary" : "bar"
},
"a" : true,
"b" : true,
"c" : {
"d" : "e"
}
});
let mut builder = ContextBuilder::new("foo");
let result = builder
.anonymous(true)
.secondary("bar")
.kind("org")
.set_bool("a", true)
.add_private_attribute("a")
.set_bool("b", true)
.add_private_attribute("b")
.set_value(
"c",
AttributeValue::Object(hashmap! {
"d".into() => "e".into()
}),
)
.add_private_attribute("/c/d")
.build()
.unwrap();
assert_json_eq!(json, result);
}
#[test]
fn build_generates_correct_multi_kind_context() {
let json = json!({
"kind" : "multi",
"user" : {
"key" : "foo-key",
},
"bar" : {
"key" : "bar-key",
},
"baz" : {
"key" : "baz-key",
"anonymous" : true
}
});
let user = ContextBuilder::new("foo-key");
let mut bar = ContextBuilder::new("bar-key");
bar.kind("bar");
let mut baz = ContextBuilder::new("baz-key");
baz.kind("baz");
baz.anonymous(true);
let multi = MultiContextBuilder::new()
.add_context(user.build().expect("failed to build context"))
.add_context(bar.build().expect("failed to build context"))
.add_context(baz.build().expect("failed to build context"))
.build()
.unwrap();
assert_json_eq!(multi, json);
}
#[test]
#[should_panic]
fn cannot_serialize_implicit_user_context() {
let x = ContextVariant::Implicit(UserFormat {
key: "foo".to_string(),
name: None,
secondary: None,
anonymous: None,
custom: None,
private_attribute_names: None,
first_name: None,
last_name: None,
avatar: None,
email: None,
country: None,
ip: None,
});
let _ = serde_json::to_string(&x);
}
}