launchdarkly_server_sdk_evaluation/contexts/
context_builder.rs

1use super::{attribute_reference::Reference, context::Context, context::Kind};
2use crate::AttributeValue;
3use log::warn;
4
5use std::collections::HashMap;
6
7const DEFAULT_MULTI_BUILDER_CAPACITY: usize = 3; // arbitrary value based on presumed likely use cases
8
9/// Contains methods for building a [Context] with a specified key.
10///
11/// To define a multi-context (containing more than one kind) see [MultiContextBuilder].
12///
13/// You may use these methods to set additional attributes and/or change the kind before calling
14/// [ContextBuilder::build]. If you do not change any values, the defaults for the [Context] are:
15/// - its kind is "user"
16/// - its key is set to whatever value you passed to [ContextBuilder::new]
17/// - its [anonymous attribute](ContextBuilder::anonymous) is `false`
18/// - it has no values for any other attributes.
19pub struct ContextBuilder {
20    kind: String,
21    name: Option<String>,
22    anonymous: bool,
23    secondary: Option<String>,
24    private_attributes: Vec<Reference>,
25
26    key: String,
27    attributes: HashMap<String, AttributeValue>,
28
29    // Contexts that were deserialized from implicit user format
30    // are allowed to have empty string keys. Otherwise,
31    // key is never allowed to be empty.
32    allow_empty_key: bool,
33}
34
35impl ContextBuilder {
36    /// Create a new context builder with the provided "key" attribute.
37    pub fn new(key: impl Into<String>) -> Self {
38        Self {
39            kind: "user".to_owned(),
40            name: None,
41            anonymous: false,
42            secondary: None,
43            private_attributes: Vec::new(),
44            key: key.into(),
45            attributes: HashMap::new(),
46            allow_empty_key: false,
47        }
48    }
49
50    /// Sets the context's "kind" attribute, which is "user" by default.
51    ///
52    /// Validation rules are as follows:
53    /// - It may not be an empty string
54    /// - It may only contain letters, numbers, and the characters `.`, `_`, and `-`
55    /// - It cannot be "kind"
56    /// - It cannot be "multi"
57    ///
58    /// If the value is invalid, you will receive an error when [ContextBuilder::build] is called.
59    ///
60    /// To ensure that a given kind will be valid, you may use [Kind::try_from] and pass that here.
61    pub fn kind(&mut self, kind: impl Into<String>) -> &mut Self {
62        self.kind = kind.into();
63        self
64    }
65
66    /// Sets the Context's key attribute. The provided key cannot be an empty string.
67    ///
68    /// The key attribute can be referenced by flag rules, flag target
69    /// lists, and segments.
70    pub fn key(&mut self, key: impl Into<String>) -> &mut Self {
71        self.key = key.into();
72        self
73    }
74
75    /// Sets the context's "name" attribute.
76    ///
77    /// This attribute is optional. It has the following special rules:
78    ///
79    /// - Unlike most other attributes, it is always a string if it is specified.
80    /// - The LaunchDarkly dashboard treats this attribute as the preferred display name for users.
81    pub fn name(&mut self, name: impl Into<String>) -> &mut Self {
82        self.name = Some(name.into());
83        self
84    }
85
86    /// Sets an attribute to a boolean value.
87    ///
88    /// For rules regarding attribute names and values, see [ContextBuilder::set_value]. This method is
89    /// exactly equivalent to calling `self.set_value(attribute_name,
90    /// AttributeValue::Bool(value))`.
91    pub fn set_bool(&mut self, attribute_name: &str, value: bool) -> &mut Self {
92        self.set_value(attribute_name, AttributeValue::Bool(value));
93        self
94    }
95
96    /// Sets an attribute to a f64 numeric value.
97    ///
98    /// For rules regarding attribute names and values, see [ContextBuilder::set_value]. This method is
99    /// exactly equivalent to calling `self.set_value(attribute_name,
100    /// AttributeValue::Number(value))`.
101    ///
102    /// Note: the LaunchDarkly model for feature flags and context attributes is based on JSON types,
103    /// and does not distinguish between integer and floating-point types.
104    pub fn set_float(&mut self, attribute_name: &str, value: f64) -> &mut Self {
105        self.set_value(attribute_name, AttributeValue::Number(value));
106        self
107    }
108
109    /// Sets an attribute to a string value.
110    ///
111    /// For rules regarding attribute names and values, see [ContextBuilder::set_value]. This method is
112    /// exactly equivalent to calling `self.set_value(attribute_name,
113    /// AttributeValue::String(value.to_string()))`.
114    pub fn set_string(&mut self, attribute_name: &str, value: impl Into<String>) -> &mut Self {
115        self.set_value(attribute_name, AttributeValue::String(value.into()));
116        self
117    }
118
119    /// Sets the value of any attribute for the context.
120    ///
121    /// This includes only attributes that are addressable in evaluations -- not metadata such as
122    /// private attributes. For example, if `attribute_name` is "privateAttributes", you will be
123    /// setting an attribute with that name which you can use in evaluations or to record data for
124    /// your own purposes, but it will be unrelated to [ContextBuilder::add_private_attribute].
125    ///
126    /// If `attribute_name` is "privateAttributeNames", it is ignored and no
127    /// attribute is set.
128    ///
129    /// This method uses the [AttributeValue] type to represent a value of any JSON type: null,
130    /// boolean, number, string, array, or object. For all attribute names that do not have special
131    /// meaning to LaunchDarkly, you may use any of those types. Values of different JSON types are
132    /// always treated as different values: for instance, null, false, and the empty string "" are
133    /// not the same, and the number 1 is not the same as the string "1".
134    ///
135    /// The following attribute names have special restrictions on their value types, and any value
136    /// of an unsupported type will be ignored (leaving the attribute unchanged):
137    ///
138    /// - "kind", "key": Must be a string. See [ContextBuilder::kind] and [ContextBuilder::key].
139    ///
140    /// - "name": Must be a string. See [ContextBuilder::name].
141    ///
142    /// - "anonymous": Must be a boolean. See [ContextBuilder::anonymous].
143    ///
144    /// The attribute name "_meta" is not allowed, because it has special meaning in the JSON
145    /// schema for contexts; any attempt to set an attribute with this name has no effect.
146    ///
147    /// Values that are JSON arrays or objects have special behavior when referenced in
148    /// flag/segment rules.
149    ///
150    /// For attributes that aren't subject to the special restrictions mentioned above,
151    /// a value of [AttributeValue::Null] is equivalent to removing any current non-default value
152    /// of the attribute. Null is not a valid attribute value in the LaunchDarkly model; any
153    /// expressions in feature flags that reference an attribute with a null value will behave as
154    /// if the attribute did not exist.
155    pub fn set_value(&mut self, attribute_name: &str, value: AttributeValue) -> &mut Self {
156        let _ = self.try_set_value(attribute_name, value);
157        self
158    }
159
160    /// Sets the value of any attribute for the context.
161    ///
162    /// This is the same as [ContextBuilder::set_value], except that it returns true for success, or false if
163    /// the parameters violated one of the restrictions described for [ContextBuilder::set_value] (for
164    /// instance, attempting to set "key" to a value that was not a string).
165    pub fn try_set_value(&mut self, attribute_name: &str, value: AttributeValue) -> bool {
166        match (attribute_name, value.clone()) {
167            ("", _) => {
168                warn!("Provided attribute name is empty. Ignoring.");
169                false
170            }
171            ("kind", AttributeValue::String(s)) => {
172                self.kind(s);
173                true
174            }
175            ("kind", _) => false,
176            ("key", AttributeValue::String(s)) => {
177                self.key(s);
178                true
179            }
180            ("key", _) => false,
181            ("name", AttributeValue::String(s)) => {
182                self.name(s);
183                true
184            }
185            ("name", AttributeValue::Null) => {
186                self.name = None;
187                true
188            }
189            ("name", _) => false,
190            ("anonymous", AttributeValue::Bool(b)) => {
191                self.anonymous(b);
192                true
193            }
194            ("anonymous", _) => false,
195            ("_meta", _) => false,
196            (_, AttributeValue::Null) => {
197                self.attributes.remove(attribute_name);
198                true
199            }
200            (_, _) => {
201                self.attributes.insert(attribute_name.to_string(), value);
202                true
203            }
204        }
205    }
206
207    /// Sets a secondary key for the context.
208    ///
209    /// This corresponds to the "secondary" attribute in the older LaunchDarkly user schema. Since
210    /// LaunchDarkly still supports evaluating feature flags for old-style users, this attribute is
211    /// still available to the evaluation logic if it was present in user JSON and the
212    /// 'secondary_key_bucketing' Cargo feature flag is enabled, but it cannot be intentionally set
213    /// by external users via the builder API.
214    ///
215    /// Setting this value to an empty string is not the same as leaving it unset.
216    pub(in crate::contexts) fn secondary(&mut self, value: impl Into<String>) -> &mut Self {
217        self.secondary = Some(value.into());
218        self
219    }
220
221    /// Designates any number of context attributes as private: that is, their values will not
222    /// be sent to LaunchDarkly.
223    ///
224    /// See [Reference] for details on how to construct a valid reference.
225    ///
226    /// This action only affects analytics events that involve this particular context. To mark some (or all)
227    /// context attributes as private for all uses, use the overall event configuration for the SDK.
228    ///
229    /// The attributes "kind" and "key", and the metadata property set by [ContextBuilder::anonymous],
230    /// cannot be made private.
231    pub fn add_private_attribute<R: Into<Reference>>(&mut self, reference: R) -> &mut Self {
232        self.private_attributes.push(reference.into());
233        self
234    }
235
236    /// Remove any reference provided through [ContextBuilder::add_private_attribute]. If the reference was
237    /// added more than once, this method will remove all instances of it.
238    pub fn remove_private_attribute<R: Into<Reference>>(&mut self, reference: R) -> &mut Self {
239        let reference = reference.into();
240        self.private_attributes
241            .retain(|private| *private != reference);
242        self
243    }
244
245    /// Sets whether the context is only intended for flag evaluations and should not be indexed by
246    /// LaunchDarkly.
247    ///
248    /// The default value is `false`, which means that this context represents an entity such as a
249    /// user that you want to see on the LaunchDarkly dashboard.
250    ///
251    /// Setting anonymous to `true` excludes this context from the database that is used by the
252    /// dashboard. It does not exclude it from analytics event data, so it is not the same as
253    /// making attributes private; all non-private attributes will still be included in events and
254    /// data export.
255    ///
256    /// This value is also addressable in evaluations as the attribute name "anonymous".
257    pub fn anonymous(&mut self, value: bool) -> &mut Self {
258        self.anonymous = value;
259        self
260    }
261
262    /// Allows the context to have an empty string key. This is for backwards compatability purposes
263    /// when deserializing implicit user contexts, and is only used internally.
264    pub(super) fn allow_empty_key(&mut self) -> &mut Self {
265        self.allow_empty_key = true;
266        self
267    }
268
269    /// Creates a context from the current builder's properties.
270    ///
271    /// The context is immutable and will not be affected by any subsequent actions on the
272    /// builder.
273    ///
274    /// It is possible to specify invalid attributes for a builder, such as an empty key.
275    /// In those situations, an `Err` type will be returned.
276    pub fn build(&self) -> Result<Context, String> {
277        let kind = Kind::try_from(self.kind.clone())?;
278
279        if kind.is_multi() {
280            return Err(String::from(
281                "context of kind \"multi\" must be built with MultiContextBuilder",
282            ));
283        }
284
285        if !self.allow_empty_key && self.key.is_empty() {
286            return Err(String::from("key cannot be empty"));
287        }
288
289        let canonical_key = canonical_key_for_kind(&kind, &self.key, true);
290
291        Ok(Context {
292            kind,
293            contexts: None,
294            name: self.name.clone(),
295            anonymous: self.anonymous,
296            secondary: self.secondary.clone(),
297            private_attributes: if self.private_attributes.is_empty() {
298                None
299            } else {
300                Some(self.private_attributes.clone())
301            },
302            key: self.key.clone(),
303            canonical_key,
304            attributes: self.attributes.clone(),
305        })
306    }
307}
308
309fn canonical_key_for_kind(kind: &Kind, key: &str, omit_user_kind: bool) -> String {
310    if omit_user_kind && kind.is_user() {
311        return key.to_owned();
312    }
313    format!("{}:{}", kind, key.replace('%', "%25").replace(':', "%3A"))
314}
315
316/// Contains methods for building a multi-context.
317///
318/// Use this builder if you need to construct a context that has multiple kinds, each representing their
319/// own [Context]. Otherwise, use [ContextBuilder].
320///
321/// Obtain an instance of the builder by calling [MultiContextBuilder::new]; then, call
322/// [MultiContextBuilder::add_context] to add a kind.
323/// [MultiContextBuilder] setters return a reference the same builder, so they can be chained
324/// together.
325pub struct MultiContextBuilder {
326    contexts: Vec<Context>,
327}
328
329impl MultiContextBuilder {
330    /// Create a new multi-context builder. An empty builder cannot create a valid [Context]; you must
331    /// add one or more kinds via [MultiContextBuilder::add_context].
332    ///
333    /// If you already have a list of contexts, you can instead use [MultiContextBuilder::of].
334    pub fn new() -> Self {
335        Self {
336            contexts: Vec::with_capacity(DEFAULT_MULTI_BUILDER_CAPACITY),
337        }
338    }
339
340    /// Create a new multi-context builder from the given list of contexts.
341    pub fn of(contexts: Vec<Context>) -> Self {
342        let mut this = MultiContextBuilder::new();
343        for c in contexts {
344            this.add_context(c);
345        }
346        this
347    }
348
349    /// Adds a context to the builder.
350    ///
351    /// It is invalid to add more than one context of the same [Kind]. This error is detected when
352    /// you call [MultiContextBuilder::build].
353    ///
354    /// If `context` is a multi-context, this is equivalent to adding each individual
355    /// context.
356    pub fn add_context(&mut self, context: Context) -> &mut Self {
357        let mut contexts = match context.contexts {
358            Some(multi) => multi,
359            None => vec![context],
360        };
361        self.contexts.append(&mut contexts);
362        self
363    }
364
365    /// Creates a context from the builder's current properties.
366    ///
367    /// The context is immutable and will not be affected by any subsequent actions on the
368    /// [MultiContextBuilder].
369    ///
370    /// It is possible for a [MultiContextBuilder] to represent an invalid state. In those
371    /// situations, an `Err` type will be returned.
372    ///
373    /// If only one context was added to the builder, this method returns that context.
374    pub fn build(&self) -> Result<Context, String> {
375        if self.contexts.is_empty() {
376            return Err("multi-kind context must contain at least one nested context".into());
377        }
378
379        if self.contexts.len() == 1 {
380            return Ok(self.contexts[0].clone());
381        }
382
383        let mut contexts = self.contexts.clone();
384        contexts.sort_by(|a, b| a.kind.cmp(&b.kind));
385        for (index, context) in contexts.iter().enumerate() {
386            if index > 0 && contexts[index - 1].kind == context.kind {
387                return Err("multi-kind context cannot have same kind more than once".into());
388            }
389        }
390
391        let canonicalized_key = contexts
392            .iter()
393            .map(|context| canonical_key_for_kind(context.kind(), context.key(), false))
394            .collect::<Vec<_>>()
395            .join(":");
396
397        Ok(Context {
398            kind: Kind::multi(),
399            contexts: Some(contexts),
400            name: None,
401            anonymous: false,
402            secondary: None,
403            private_attributes: None,
404            key: "".to_owned(),
405            canonical_key: canonicalized_key,
406            attributes: HashMap::new(),
407        })
408    }
409}
410
411impl Default for MultiContextBuilder {
412    /// Creates an empty multi-context builder. In this state, a context must
413    /// be [added](MultiContextBuilder::add_context) in order to build successfully.
414    fn default() -> Self {
415        Self::new()
416    }
417}
418
419#[cfg(test)]
420mod tests {
421    use super::{ContextBuilder, MultiContextBuilder};
422    use crate::{AttributeValue, Reference};
423
424    use crate::contexts::context::Kind;
425    use test_case::test_case;
426
427    #[test]
428    fn builder_can_create_correct_context() {
429        let mut builder = ContextBuilder::new("key");
430        builder
431            .kind(Kind::user())
432            .name("Name")
433            .secondary("Secondary")
434            .anonymous(true);
435
436        let context = builder.build().expect("Failed to build context");
437
438        assert!(!context.is_multi());
439        assert!(context.kind.is_user());
440        assert_eq!(Some("Name".to_string()), context.name);
441        assert_eq!(Some("Secondary".to_string()), context.secondary);
442        assert!(context.anonymous);
443    }
444
445    #[test_case("key", Kind::user(), "key")]
446    #[test_case("key", Kind::from("org"), "org:key")]
447    #[test_case("hi:there", Kind::user(), "hi:there")]
448    #[test_case("hi:there", Kind::from("org"), "org:hi%3Athere")]
449    fn builder_sets_canonical_key_correctly_for_single_context(
450        key: &str,
451        kind: Kind,
452        expected_key: &str,
453    ) {
454        let context = ContextBuilder::new(key)
455            .kind(kind)
456            .build()
457            .expect("Failed to create context");
458
459        assert_eq!(expected_key, context.canonical_key());
460    }
461
462    // no 'user:' prefix because a multi-kind context with a single kind is built
463    // as a single-kind context (special case.)
464    #[test_case(vec![("key", Kind::user())], "key")]
465    #[test_case(vec![("userKey", Kind::user()), ("orgKey", Kind::from("org"))], "org:orgKey:user:userKey")]
466    #[test_case(vec![("some user", Kind::user()), ("org:key", Kind::from("org"))], "org:org%3Akey:user:some user")]
467    #[test_case(vec![("my:key%x/y", Kind::from("org"))], "org:my%3Akey%25x/y")]
468    fn builder_sets_canonical_key_correctly_for_multiple_contexts(
469        tuples: Vec<(&str, Kind)>,
470        expected_key: &str,
471    ) {
472        let mut multi_builder = MultiContextBuilder::new();
473
474        for (key, kind) in tuples {
475            multi_builder.add_context(
476                ContextBuilder::new(key)
477                    .kind(kind)
478                    .build()
479                    .expect("Failed to create context"),
480            );
481        }
482
483        let multi_context = multi_builder.build().expect("Failed to create context");
484        assert_eq!(expected_key, multi_context.canonical_key());
485    }
486
487    #[test_case("multi"; "Cannot set kind as multi")]
488    #[test_case("kind"; "Cannot set kind as kind")]
489    #[test_case("🦀"; "Cannot set kind as invalid character")]
490    #[test_case(" "; "Cannot set kind as only whitespace")]
491    fn build_fails_on_invalid_kinds(kind: &str) {
492        let mut builder = ContextBuilder::new("key");
493        builder.kind(kind);
494
495        let result = builder.build();
496        assert!(result.is_err());
497    }
498
499    #[test]
500    fn builder_can_set_custom_properties_by_type() {
501        let mut builder = ContextBuilder::new("key");
502        builder
503            .kind(Kind::user())
504            .set_bool("loves-rust", true)
505            .set_float("pi", 3.1459)
506            .set_string("company", "LaunchDarkly");
507
508        let context = builder.build().expect("Failed to build context");
509
510        assert_eq!(
511            &AttributeValue::Bool(true),
512            context.attributes.get("loves-rust").unwrap()
513        );
514        assert_eq!(
515            &AttributeValue::Number(3.1459),
516            context.attributes.get("pi").unwrap()
517        );
518        assert_eq!(
519            &AttributeValue::String("LaunchDarkly".to_string()),
520            context.attributes.get("company").unwrap()
521        );
522    }
523
524    #[test_case("", AttributeValue::Bool(true), false)]
525    #[test_case("kind", AttributeValue::Bool(true), false)]
526    #[test_case("kind", AttributeValue::String("user".to_string()), true)]
527    #[test_case("key", AttributeValue::Bool(true), false)]
528    #[test_case("key", AttributeValue::String("key".to_string()), true)]
529    #[test_case("name", AttributeValue::Bool(true), false)]
530    #[test_case("name", AttributeValue::String("name".to_string()), true)]
531    #[test_case("anonymous", AttributeValue::String("anonymous".to_string()), false)]
532    #[test_case("anonymous", AttributeValue::Bool(true), true)]
533    #[test_case("secondary", AttributeValue::String("secondary".to_string()), true)]
534    #[test_case("secondary", AttributeValue::Bool(true), true)]
535    #[test_case("my-custom-attribute", AttributeValue::Bool(true), true)]
536    #[test_case("my-custom-attribute", AttributeValue::String("string name".to_string()), true)]
537    fn builder_try_set_value_handles_invalid_values_correctly(
538        attribute_name: &str,
539        value: AttributeValue,
540        expected: bool,
541    ) {
542        let mut builder = ContextBuilder::new("key");
543        assert_eq!(builder.try_set_value(attribute_name, value), expected);
544    }
545
546    #[test_case("secondary", AttributeValue::String("value".to_string()))]
547    #[test_case("privateAttributes", AttributeValue::Array(vec![AttributeValue::String("value".to_string())]))]
548    fn builder_set_value_cannot_set_meta_properties(attribute_name: &str, value: AttributeValue) {
549        let builder = ContextBuilder::new("key")
550            .set_value(attribute_name, value.clone())
551            .build()
552            .unwrap();
553
554        assert_eq!(&value, builder.attributes.get(attribute_name).unwrap());
555        assert!(builder.secondary.is_none());
556    }
557
558    #[test]
559    fn builder_try_set_value_cannot_set_meta() {
560        let mut builder = ContextBuilder::new("key");
561
562        assert!(!builder.try_set_value("_meta", AttributeValue::String("value".to_string())));
563        assert_eq!(builder.build().unwrap().attributes.len(), 0);
564    }
565
566    #[test]
567    fn builder_deals_with_missing_kind_correctly() {
568        let mut builder = ContextBuilder::new("key");
569        assert!(builder.build().unwrap().kind.is_user());
570
571        builder.kind("");
572        assert!(builder.build().is_err());
573    }
574
575    #[test]
576    fn builder_deals_with_empty_key_correctly() {
577        assert!(ContextBuilder::new("").build().is_err());
578    }
579
580    #[test]
581    fn builder_handles_private_attributes() {
582        let mut builder = ContextBuilder::new("key");
583        let context = builder.build().expect("Failed to build context");
584
585        assert!(context.private_attributes.is_none());
586
587        builder.add_private_attribute("name");
588        let context = builder.build().expect("Failed to build context");
589
590        let private = context
591            .private_attributes
592            .expect("Private attributes should be set");
593
594        assert_eq!(1, private.len());
595    }
596
597    #[test]
598    fn builder_handles_removing_private_attributes() {
599        let mut builder = ContextBuilder::new("key");
600        builder
601            .add_private_attribute("name")
602            .add_private_attribute("name")
603            .add_private_attribute("/user/email");
604
605        assert_eq!(
606            3,
607            builder.build().unwrap().private_attributes.unwrap().len()
608        );
609
610        // Removing an attribute should remove all of them.
611        builder.remove_private_attribute("name");
612        assert_eq!(
613            1,
614            builder.build().unwrap().private_attributes.unwrap().len()
615        );
616
617        builder.remove_private_attribute("/user/email");
618        assert!(builder.build().unwrap().private_attributes.is_none());
619    }
620
621    #[test]
622    fn build_can_add_and_remove_with_different_formats() {
623        let mut builder = ContextBuilder::new("key");
624        builder
625            .add_private_attribute("name")
626            .add_private_attribute(Reference::new("name"));
627
628        assert_eq!(
629            2,
630            builder.build().unwrap().private_attributes.unwrap().len()
631        );
632
633        // Removing an attribute should remove all of them.
634        builder.remove_private_attribute("name");
635        assert!(builder.build().unwrap().private_attributes.is_none());
636
637        // Removing using an attribute should also remove them all.
638        builder
639            .add_private_attribute("name")
640            .add_private_attribute(Reference::new("name"));
641        builder.remove_private_attribute(Reference::new("name"));
642        assert!(builder.build().unwrap().private_attributes.is_none());
643    }
644
645    #[test]
646    fn multi_builder_can_build_multi_context() {
647        let mut single_builder = ContextBuilder::new("key");
648        single_builder.kind(Kind::user());
649        let mut multi_builder = MultiContextBuilder::new();
650
651        multi_builder.add_context(single_builder.build().expect("Failed to build context"));
652
653        single_builder.key("second-key").kind("org".to_string());
654        multi_builder.add_context(single_builder.build().expect("Failed to build context"));
655
656        let multi_context = multi_builder
657            .build()
658            .expect("Failed to create multi context");
659
660        assert!(multi_context.is_multi());
661        assert_eq!(2, multi_context.contexts.unwrap().len());
662    }
663
664    #[test]
665    fn multi_builder_cannot_handle_more_than_one_of_same_kind() {
666        let mut single_builder = ContextBuilder::new("key");
667        single_builder.kind(Kind::user());
668        let mut multi_builder = MultiContextBuilder::new();
669
670        multi_builder.add_context(single_builder.build().expect("Failed to build context"));
671
672        single_builder.key("second-key");
673        multi_builder.add_context(single_builder.build().expect("Failed to build context"));
674
675        let result = multi_builder.build();
676        assert!(result.is_err());
677    }
678
679    #[test]
680    fn multi_builder_must_contain_another_context() {
681        assert!(MultiContextBuilder::new().build().is_err());
682    }
683
684    #[test]
685    fn multi_builder_should_flatten_multi_contexts() {
686        let cat = ContextBuilder::new("c")
687            .kind("cat")
688            .build()
689            .expect("should build cat");
690
691        let dog = ContextBuilder::new("d")
692            .kind("dog")
693            .build()
694            .expect("should build dog");
695
696        let rabbit = ContextBuilder::new("r")
697            .kind("rabbit")
698            .build()
699            .expect("should build rabbit");
700
701        let ferret = ContextBuilder::new("f")
702            .kind("ferret")
703            .build()
704            .expect("should build ferret");
705
706        let catdog = MultiContextBuilder::of(vec![cat, dog])
707            .build()
708            .expect("should build cat/dog multi-context");
709
710        let rabbitferret = MultiContextBuilder::of(vec![rabbit, ferret])
711            .build()
712            .expect("should build rabbit/ferret multi-context");
713
714        let chimera = MultiContextBuilder::of(vec![catdog, rabbitferret])
715            .build()
716            .expect("should build cat/dog/rabbit/ferret multi-context");
717
718        assert_eq!(chimera.kinds().len(), 4);
719        for k in &["cat", "dog", "rabbit", "ferret"] {
720            assert!(chimera.as_kind(&Kind::from(k)).is_some());
721        }
722        assert_eq!(chimera.canonical_key(), "cat:c:dog:d:ferret:f:rabbit:r");
723    }
724}