Skip to content

Commit 3bc15f7

Browse files
committed
feat: add builders for collection schema and collection fields using bon; restructure library layout
1 parent 2cb188f commit 3bc15f7

File tree

18 files changed

+271
-345
lines changed

18 files changed

+271
-345
lines changed

typesense/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ reqwest-retry = "0.7.0"
2929
reqwest = { version = "0.12", features = ["json"] }
3030
reqwest-middleware = { version = "0.4.2", features = ["json"] }
3131
thiserror = "1.0"
32+
bon = "3.7.0"
33+
strum = { version = "0.26", features = ["derive"] }
34+
3235

3336
[dev-dependencies]
3437
dotenvy = "0.15"
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//! Module with the common definitions for the
2+
//! [fields](https://github.com/typesense/typesense/blob/v0.19.0/include/field.)
3+
//! available in Typesense.
4+
5+
use crate::traits::FieldType;
6+
use bon::builder;
7+
use typesense_codegen::models::{Field as TypesenseField, FieldEmbed};
8+
9+
/// Creates a new [`TypesenseField`] builder.
10+
#[builder(
11+
// expose a public builder type named `FieldBuilder` and a public finish_fn `build()`
12+
builder_type(name = FieldBuilder, vis = "pub"),
13+
finish_fn(name = build, vis = "pub"),
14+
// allow passing &str into String params
15+
on(String, into)
16+
)]
17+
pub fn new_collection_field(
18+
#[builder(start_fn)] name: String,
19+
20+
#[builder(start_fn)] typesense_type: FieldType,
21+
22+
optional: Option<bool>,
23+
facet: Option<bool>,
24+
index: Option<bool>,
25+
locale: Option<String>,
26+
sort: Option<bool>,
27+
infix: Option<bool>,
28+
num_dim: Option<i32>,
29+
drop: Option<bool>,
30+
embed: Option<Box<FieldEmbed>>,
31+
store: Option<bool>,
32+
stem: Option<bool>,
33+
range_index: Option<bool>,
34+
vec_dist: Option<String>,
35+
) -> TypesenseField {
36+
TypesenseField {
37+
name,
38+
r#type: typesense_type,
39+
optional,
40+
facet,
41+
index,
42+
locale,
43+
sort,
44+
infix,
45+
num_dim,
46+
drop,
47+
embed,
48+
store,
49+
stem,
50+
range_index,
51+
vec_dist,
52+
..Default::default()
53+
}
54+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
//! # Collection
2+
//!
3+
//! In Typesense, a group of related documents is called a collection. A collection
4+
//! is roughly equivalent to a table in a relational database.
5+
//!
6+
use bon::builder;
7+
use typesense_codegen::models::{CollectionSchema, Field, VoiceQueryModelCollectionConfig};
8+
9+
/// Creates a new [`CollectionSchema`] builder.
10+
///
11+
/// In Typesense, a collection is a group of related documents, similar to a table
12+
/// in a relational database. This builder enforces that `name` must be provided
13+
/// before [`build`](CollectionSchemaBuilder::build) can be called.
14+
///
15+
/// # Example
16+
///
17+
/// ```
18+
/// use typesense::builders::new_collection_schema;
19+
/// let fields = vec![];
20+
/// let schema = new_collection_schema("companies", fields)
21+
/// .default_sorting_field("num_employees")
22+
/// .build();
23+
/// ```
24+
#[builder(
25+
builder_type(name = CollectionSchemaBuilder, vis = "pub"),
26+
finish_fn(name = build, vis = "pub"),
27+
state_mod(name = collection_schema_builder, vis = "pub"),
28+
on(String, into)
29+
)]
30+
pub fn new_collection_schema(
31+
/// The name of the collection. Must be unique within the Typesense instance.
32+
#[builder(start_fn)]
33+
name: String,
34+
35+
/// The list of fields that describe the schema of documents in this collection.
36+
#[builder(start_fn)]
37+
fields: Vec<Field>,
38+
39+
/// The name of the default sorting field for the collection.
40+
default_sorting_field: Option<String>,
41+
42+
/// A list of token separators to use when indexing text fields.
43+
token_separators: Option<Vec<String>>,
44+
45+
/// Whether nested fields are enabled.
46+
enable_nested_fields: Option<bool>,
47+
48+
/// Symbols that should be indexed for this collection.
49+
symbols_to_index: Option<Vec<String>>,
50+
51+
/// Configuration for Typesense’s Voice Query Model.
52+
voice_query_model: Option<Box<VoiceQueryModelCollectionConfig>>,
53+
) -> CollectionSchema {
54+
CollectionSchema {
55+
name,
56+
fields,
57+
default_sorting_field,
58+
token_separators,
59+
enable_nested_fields,
60+
symbols_to_index,
61+
voice_query_model,
62+
}
63+
}
64+
65+
// custom convenience methods; the typestate module name matches `state_mod`
66+
impl<S: collection_schema_builder::State> CollectionSchemaBuilder<S> {
67+
/// Adds a single [`Field`] to the collection schema.
68+
///
69+
/// This is a convenience method for pushing one field at a time.
70+
pub fn field(mut self, field: Field) -> Self {
71+
self.fields.push(field);
72+
self
73+
}
74+
75+
/// Adds multiple [`Field`] values to the collection schema.
76+
///
77+
/// This is a convenience method for appending a slice of fields.
78+
pub fn fields(mut self, fields: &[Field]) -> Self
79+
where
80+
Field: Clone,
81+
{
82+
self.fields.extend_from_slice(fields);
83+
self
84+
}
85+
}
86+
87+
#[cfg(test)]
88+
mod test {
89+
use super::*;
90+
use crate::builders::new_collection_field;
91+
use serde_json::json;
92+
93+
#[test]
94+
fn collection_schema_serializes_as_expected() {
95+
let fields = [
96+
("company_name", "string".to_owned(), None),
97+
("num_employees", "int32".to_owned(), None),
98+
("country", "string".to_owned(), Some(true)),
99+
]
100+
.map(|(name, typesense_type, facet)| {
101+
if facet.is_some() {
102+
new_collection_field(name, typesense_type.into())
103+
.facet(facet.unwrap())
104+
.build()
105+
} else {
106+
new_collection_field(name, typesense_type.into()).build()
107+
}
108+
});
109+
110+
let collection: CollectionSchema =
111+
new_collection_schema("companies", fields.clone().to_vec())
112+
.fields(&fields)
113+
.field(new_collection_field("size", "string".into()).build())
114+
.default_sorting_field("num_employees")
115+
.build();
116+
117+
let expected = json!(
118+
{
119+
"name": "companies",
120+
"fields": [
121+
{ "name": "company_name", "type": "string" },
122+
{ "name": "num_employees", "type": "int32" },
123+
{ "name": "country", "type": "string", "facet": true },
124+
125+
{ "name": "company_name", "type": "string" },
126+
{ "name": "num_employees", "type": "int32" },
127+
{ "name": "country", "type": "string", "facet": true },
128+
129+
{ "name": "size", "type": "string" },
130+
],
131+
"default_sorting_field": "num_employees"
132+
}
133+
);
134+
135+
assert_eq!(serde_json::to_value(&collection).unwrap(), expected)
136+
}
137+
}

typesense/src/builders/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
//! Contain convenient builders for Typesense schemas.
2+
3+
mod collection_field;
4+
mod collection_schema;
5+
pub use collection_field::new_collection_field;
6+
pub use collection_schema::new_collection_schema;

typesense/src/client/multi_search.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//! A `MultiSearch` instance is created via the main `Client::multi_search()` method.
44
55
use crate::{
6-
models::SearchResult, Client, Error, MultiSearchParseError, MultiSearchResultExt,
6+
models::SearchResult, traits::MultiSearchResultExt, Client, Error, MultiSearchParseError,
77
MultiSearchSearchesParameter,
88
};
99
use serde::de::DeserializeOwned;

typesense/src/collection_schema.rs

Lines changed: 0 additions & 136 deletions
This file was deleted.

typesense/src/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//! Contains the error types for the Typesense client
2+
13
use thiserror::Error;
24
pub use typesense_codegen::apis::Error as ApiError;
35

0 commit comments

Comments
 (0)