diff --git a/crates/meilisearch/src/routes/indexes/settings.rs b/crates/meilisearch/src/routes/indexes/settings.rs index e1794535b..b2922e5ff 100644 --- a/crates/meilisearch/src/routes/indexes/settings.rs +++ b/crates/meilisearch/src/routes/indexes/settings.rs @@ -17,6 +17,32 @@ use crate::extractors::authentication::GuardedData; use crate::routes::{get_task_id, is_dry_run, SummarizedTaskView}; use crate::Opt; +/// This macro generates the routes for the settings. +/// +/// It takes a list of settings and generates a module for each setting. +/// Each module contains the `get`, `update` and `delete` routes for the setting. +/// +/// It also generates a `configure` function that configures the routes for the settings. +macro_rules! make_setting_routes { + ($({route: $route:literal, update_verb: $update_verb:ident, value_type: $type:ty, err_type: $err_ty:ty, attr: $attr:ident, camelcase_attr: $camelcase_attr:literal, analytics: $analytics:ident},)*) => { + $( + make_setting_route!($route, $update_verb, $type, $err_ty, $attr, $camelcase_attr, $analytics); + )* + + pub fn configure(cfg: &mut web::ServiceConfig) { + use crate::extractors::sequential_extractor::SeqHandler; + cfg.service( + web::resource("") + .route(web::patch().to(SeqHandler(update_all))) + .route(web::get().to(SeqHandler(get_all))) + .route(web::delete().to(SeqHandler(delete_all)))) + $(.service($attr::resources()))*; + } + + pub const ALL_SETTINGS_NAMES: &[&str] = &[$(stringify!($attr)),*]; + }; +} + #[macro_export] macro_rules! make_setting_route { ($route:literal, $update_verb:ident, $type:ty, $err_ty:ty, $attr:ident, $camelcase_attr:literal, $analytics:ident) => { @@ -153,279 +179,227 @@ macro_rules! make_setting_route { }; } -make_setting_route!( - "/filterable-attributes", - put, - std::collections::BTreeSet, - meilisearch_types::deserr::DeserrJsonError< - meilisearch_types::error::deserr_codes::InvalidSettingsFilterableAttributes, - >, - filterable_attributes, - "filterableAttributes", - FilterableAttributesAnalytics -); - -make_setting_route!( - "/sortable-attributes", - put, - std::collections::BTreeSet, - meilisearch_types::deserr::DeserrJsonError< - meilisearch_types::error::deserr_codes::InvalidSettingsSortableAttributes, - >, - sortable_attributes, - "sortableAttributes", - SortableAttributesAnalytics -); - -make_setting_route!( - "/displayed-attributes", - put, - Vec, - meilisearch_types::deserr::DeserrJsonError< - meilisearch_types::error::deserr_codes::InvalidSettingsDisplayedAttributes, - >, - displayed_attributes, - "displayedAttributes", - DisplayedAttributesAnalytics -); - -make_setting_route!( - "/typo-tolerance", - patch, - meilisearch_types::settings::TypoSettings, - meilisearch_types::deserr::DeserrJsonError< - meilisearch_types::error::deserr_codes::InvalidSettingsTypoTolerance, - >, - typo_tolerance, - "typoTolerance", - TypoToleranceAnalytics -); - -make_setting_route!( - "/searchable-attributes", - put, - Vec, - meilisearch_types::deserr::DeserrJsonError< - meilisearch_types::error::deserr_codes::InvalidSettingsSearchableAttributes, - >, - searchable_attributes, - "searchableAttributes", - SearchableAttributesAnalytics -); - -make_setting_route!( - "/stop-words", - put, - std::collections::BTreeSet, - meilisearch_types::deserr::DeserrJsonError< - meilisearch_types::error::deserr_codes::InvalidSettingsStopWords, - >, - stop_words, - "stopWords", - StopWordsAnalytics -); - -make_setting_route!( - "/non-separator-tokens", - put, - std::collections::BTreeSet, - meilisearch_types::deserr::DeserrJsonError< - meilisearch_types::error::deserr_codes::InvalidSettingsNonSeparatorTokens, - >, - non_separator_tokens, - "nonSeparatorTokens", - NonSeparatorTokensAnalytics -); - -make_setting_route!( - "/separator-tokens", - put, - std::collections::BTreeSet, - meilisearch_types::deserr::DeserrJsonError< - meilisearch_types::error::deserr_codes::InvalidSettingsSeparatorTokens, - >, - separator_tokens, - "separatorTokens", - SeparatorTokensAnalytics -); - -make_setting_route!( - "/dictionary", - put, - std::collections::BTreeSet, - meilisearch_types::deserr::DeserrJsonError< - meilisearch_types::error::deserr_codes::InvalidSettingsDictionary, - >, - dictionary, - "dictionary", - DictionaryAnalytics -); - -make_setting_route!( - "/synonyms", - put, - std::collections::BTreeMap>, - meilisearch_types::deserr::DeserrJsonError< - meilisearch_types::error::deserr_codes::InvalidSettingsSynonyms, - >, - synonyms, - "synonyms", - SynonymsAnalytics -); - -make_setting_route!( - "/distinct-attribute", - put, - String, - meilisearch_types::deserr::DeserrJsonError< - meilisearch_types::error::deserr_codes::InvalidSettingsDistinctAttribute, - >, - distinct_attribute, - "distinctAttribute", - DistinctAttributeAnalytics -); - -make_setting_route!( - "/proximity-precision", - put, - meilisearch_types::settings::ProximityPrecisionView, - meilisearch_types::deserr::DeserrJsonError< - meilisearch_types::error::deserr_codes::InvalidSettingsProximityPrecision, - >, - proximity_precision, - "proximityPrecision", - ProximityPrecisionAnalytics -); - -make_setting_route!( - "/localized-attributes", - put, - Vec, - meilisearch_types::deserr::DeserrJsonError< - meilisearch_types::error::deserr_codes::InvalidSettingsLocalizedAttributes, - >, - localized_attributes, - "localizedAttributes", - LocalesAnalytics -); - -make_setting_route!( - "/ranking-rules", - put, - Vec, - meilisearch_types::deserr::DeserrJsonError< - meilisearch_types::error::deserr_codes::InvalidSettingsRankingRules, - >, - ranking_rules, - "rankingRules", - RankingRulesAnalytics -); - -make_setting_route!( - "/faceting", - patch, - meilisearch_types::settings::FacetingSettings, - meilisearch_types::deserr::DeserrJsonError< - meilisearch_types::error::deserr_codes::InvalidSettingsFaceting, - >, - faceting, - "faceting", - FacetingAnalytics -); - -make_setting_route!( - "/pagination", - patch, - meilisearch_types::settings::PaginationSettings, - meilisearch_types::deserr::DeserrJsonError< - meilisearch_types::error::deserr_codes::InvalidSettingsPagination, - >, - pagination, - "pagination", - PaginationAnalytics -); - -make_setting_route!( - "/embedders", - patch, - std::collections::BTreeMap>, - meilisearch_types::deserr::DeserrJsonError< - meilisearch_types::error::deserr_codes::InvalidSettingsEmbedders, - >, - embedders, - "embedders", - EmbeddersAnalytics -); - -make_setting_route!( - "/search-cutoff-ms", - put, - u64, - meilisearch_types::deserr::DeserrJsonError< - meilisearch_types::error::deserr_codes::InvalidSettingsSearchCutoffMs, - >, - search_cutoff_ms, - "searchCutoffMs", - SearchCutoffMsAnalytics -); - -make_setting_route!( - "/facet-search", - put, - bool, - meilisearch_types::deserr::DeserrJsonError< - meilisearch_types::error::deserr_codes::InvalidSettingsFacetSearch, - >, - facet_search, - "facetSearch", - FacetSearchAnalytics -); - -make_setting_route!( - "/prefix-search", - put, - meilisearch_types::settings::PrefixSearchSettings, - meilisearch_types::deserr::DeserrJsonError< - meilisearch_types::error::deserr_codes::InvalidSettingsPrefixSearch, - >, - prefix_search, - "prefixSearch", - PrefixSearchAnalytics -); - -macro_rules! generate_configure { - ($($mod:ident),*) => { - pub fn configure(cfg: &mut web::ServiceConfig) { - use crate::extractors::sequential_extractor::SeqHandler; - cfg.service( - web::resource("") - .route(web::patch().to(SeqHandler(update_all))) - .route(web::get().to(SeqHandler(get_all))) - .route(web::delete().to(SeqHandler(delete_all)))) - $(.service($mod::resources()))*; - } - }; -} - -generate_configure!( - filterable_attributes, - sortable_attributes, - displayed_attributes, - localized_attributes, - searchable_attributes, - distinct_attribute, - proximity_precision, - stop_words, - separator_tokens, - non_separator_tokens, - dictionary, - synonyms, - ranking_rules, - typo_tolerance, - pagination, - faceting, - embedders, - search_cutoff_ms +make_setting_routes!( + { + route: "/filterable-attributes", + update_verb: put, + value_type: std::collections::BTreeSet, + err_type: meilisearch_types::deserr::DeserrJsonError< + meilisearch_types::error::deserr_codes::InvalidSettingsFilterableAttributes, + >, + attr: filterable_attributes, + camelcase_attr: "filterableAttributes", + analytics: FilterableAttributesAnalytics + }, + { + route: "/sortable-attributes", + update_verb: put, + value_type: std::collections::BTreeSet, + err_type: meilisearch_types::deserr::DeserrJsonError< + meilisearch_types::error::deserr_codes::InvalidSettingsSortableAttributes, + >, + attr: sortable_attributes, + camelcase_attr: "sortableAttributes", + analytics: SortableAttributesAnalytics + }, + { + route: "/displayed-attributes", + update_verb: put, + value_type: Vec, + err_type: meilisearch_types::deserr::DeserrJsonError< + meilisearch_types::error::deserr_codes::InvalidSettingsDisplayedAttributes, + >, + attr: displayed_attributes, + camelcase_attr: "displayedAttributes", + analytics: DisplayedAttributesAnalytics + }, + { + route: "/typo-tolerance", + update_verb: patch, + value_type: meilisearch_types::settings::TypoSettings, + err_type: meilisearch_types::deserr::DeserrJsonError< + meilisearch_types::error::deserr_codes::InvalidSettingsTypoTolerance, + >, + attr: typo_tolerance, + camelcase_attr: "typoTolerance", + analytics: TypoToleranceAnalytics + }, + { + route: "/searchable-attributes", + update_verb: put, + value_type: Vec, + err_type: meilisearch_types::deserr::DeserrJsonError< + meilisearch_types::error::deserr_codes::InvalidSettingsSearchableAttributes, + >, + attr: searchable_attributes, + camelcase_attr: "searchableAttributes", + analytics: SearchableAttributesAnalytics + }, + { + route: "/stop-words", + update_verb: put, + value_type: std::collections::BTreeSet, + err_type: meilisearch_types::deserr::DeserrJsonError< + meilisearch_types::error::deserr_codes::InvalidSettingsStopWords, + >, + attr: stop_words, + camelcase_attr: "stopWords", + analytics: StopWordsAnalytics + }, + { + route: "/non-separator-tokens", + update_verb: put, + value_type: std::collections::BTreeSet, + err_type: meilisearch_types::deserr::DeserrJsonError< + meilisearch_types::error::deserr_codes::InvalidSettingsNonSeparatorTokens, + >, + attr: non_separator_tokens, + camelcase_attr: "nonSeparatorTokens", + analytics: NonSeparatorTokensAnalytics + }, + { + route: "/separator-tokens", + update_verb: put, + value_type: std::collections::BTreeSet, + err_type: meilisearch_types::deserr::DeserrJsonError< + meilisearch_types::error::deserr_codes::InvalidSettingsSeparatorTokens, + >, + attr: separator_tokens, + camelcase_attr: "separatorTokens", + analytics: SeparatorTokensAnalytics + }, + { + route: "/dictionary", + update_verb: put, + value_type: std::collections::BTreeSet, + err_type: meilisearch_types::deserr::DeserrJsonError< + meilisearch_types::error::deserr_codes::InvalidSettingsDictionary, + >, + attr: dictionary, + camelcase_attr: "dictionary", + analytics: DictionaryAnalytics + }, + { + route: "/synonyms", + update_verb: put, + value_type: std::collections::BTreeMap>, + err_type: meilisearch_types::deserr::DeserrJsonError< + meilisearch_types::error::deserr_codes::InvalidSettingsSynonyms, + >, + attr: synonyms, + camelcase_attr: "synonyms", + analytics: SynonymsAnalytics + }, + { + route: "/distinct-attribute", + update_verb: put, + value_type: String, + err_type: meilisearch_types::deserr::DeserrJsonError< + meilisearch_types::error::deserr_codes::InvalidSettingsDistinctAttribute, + >, + attr: distinct_attribute, + camelcase_attr: "distinctAttribute", + analytics: DistinctAttributeAnalytics + }, + { + route: "/proximity-precision", + update_verb: put, + value_type: meilisearch_types::settings::ProximityPrecisionView, + err_type: meilisearch_types::deserr::DeserrJsonError< + meilisearch_types::error::deserr_codes::InvalidSettingsProximityPrecision, + >, + attr: proximity_precision, + camelcase_attr: "proximityPrecision", + analytics: ProximityPrecisionAnalytics + }, + { + route: "/localized-attributes", + update_verb: put, + value_type: Vec, + err_type: meilisearch_types::deserr::DeserrJsonError< + meilisearch_types::error::deserr_codes::InvalidSettingsLocalizedAttributes, + >, + attr: localized_attributes, + camelcase_attr: "localizedAttributes", + analytics: LocalesAnalytics + }, + { + route: "/ranking-rules", + update_verb: put, + value_type: Vec, + err_type: meilisearch_types::deserr::DeserrJsonError< + meilisearch_types::error::deserr_codes::InvalidSettingsRankingRules, + >, + attr: ranking_rules, + camelcase_attr: "rankingRules", + analytics: RankingRulesAnalytics + }, + { + route: "/faceting", + update_verb: patch, + value_type: meilisearch_types::settings::FacetingSettings, + err_type: meilisearch_types::deserr::DeserrJsonError< + meilisearch_types::error::deserr_codes::InvalidSettingsFaceting, + >, + attr: faceting, + camelcase_attr: "faceting", + analytics: FacetingAnalytics + }, + { + route: "/pagination", + update_verb: patch, + value_type: meilisearch_types::settings::PaginationSettings, + err_type: meilisearch_types::deserr::DeserrJsonError< + meilisearch_types::error::deserr_codes::InvalidSettingsPagination, + >, + attr: pagination, + camelcase_attr: "pagination", + analytics: PaginationAnalytics + }, + { + route: "/embedders", + update_verb: patch, + value_type: std::collections::BTreeMap>, + err_type: meilisearch_types::deserr::DeserrJsonError< + meilisearch_types::error::deserr_codes::InvalidSettingsEmbedders, + >, + attr: embedders, + camelcase_attr: "embedders", + analytics: EmbeddersAnalytics + }, + { + route: "/search-cutoff-ms", + update_verb: put, + value_type: u64, + err_type: meilisearch_types::deserr::DeserrJsonError< + meilisearch_types::error::deserr_codes::InvalidSettingsSearchCutoffMs, + >, + attr: search_cutoff_ms, + camelcase_attr: "searchCutoffMs", + analytics: SearchCutoffMsAnalytics + }, + { + route: "/facet-search", + update_verb: put, + value_type: bool, + err_type: meilisearch_types::deserr::DeserrJsonError< + meilisearch_types::error::deserr_codes::InvalidSettingsFacetSearch, + >, + attr: facet_search, + camelcase_attr: "facetSearch", + analytics: FacetSearchAnalytics + }, + { + route: "/prefix-search", + update_verb: put, + value_type: meilisearch_types::settings::PrefixSearchSettings, + err_type: meilisearch_types::deserr::DeserrJsonError< + meilisearch_types::error::deserr_codes::InvalidSettingsPrefixSearch, + >, + attr: prefix_search, + camelcase_attr: "prefixSearch", + analytics: PrefixSearchAnalytics + }, ); pub async fn update_all( diff --git a/crates/meilisearch/tests/settings/get_settings.rs b/crates/meilisearch/tests/settings/get_settings.rs index 1b1964680..55d9441ee 100644 --- a/crates/meilisearch/tests/settings/get_settings.rs +++ b/crates/meilisearch/tests/settings/get_settings.rs @@ -1,44 +1,185 @@ -use std::collections::HashMap; - -use once_cell::sync::Lazy; - -use crate::common::{Server, Value}; +use crate::common::Server; use crate::json; -static DEFAULT_SETTINGS_VALUES: Lazy> = Lazy::new(|| { - let mut map = HashMap::new(); - map.insert("displayed_attributes", json!(["*"])); - map.insert("searchable_attributes", json!(["*"])); - map.insert("localized_attributes", json!(null)); - map.insert("filterable_attributes", json!([])); - map.insert("distinct_attribute", json!(null)); - map.insert( - "ranking_rules", - json!(["words", "typo", "proximity", "attribute", "sort", "exactness"]), - ); - map.insert("stop_words", json!([])); - map.insert("non_separator_tokens", json!([])); - map.insert("separator_tokens", json!([])); - map.insert("dictionary", json!([])); - map.insert("synonyms", json!({})); - map.insert( - "faceting", - json!({ - "maxValuesPerFacet": json!(100), - "sortFacetValuesBy": { - "*": "alpha" +macro_rules! test_setting_routes { + ($({setting: $setting:ident, update_verb: $update_verb:ident, default_value: $default_value:tt},) *) => { + $( + mod $setting { + use crate::common::Server; + + #[actix_rt::test] + async fn get_unexisting_index() { + let server = Server::new().await; + let url = format!("/indexes/test/settings/{}", + stringify!($setting) + .chars() + .map(|c| if c == '_' { '-' } else { c }) + .collect::()); + let (_response, code) = server.service.get(url).await; + assert_eq!(code, 404); + } + + #[actix_rt::test] + async fn update_unexisting_index() { + let server = Server::new().await; + let url = format!("/indexes/test/settings/{}", + stringify!($setting) + .chars() + .map(|c| if c == '_' { '-' } else { c }) + .collect::()); + let (response, code) = server.service.$update_verb(url, serde_json::Value::Null.into()).await; + assert_eq!(code, 202, "{}", response); + server.index("").wait_task(0).await; + let (response, code) = server.index("test").get().await; + assert_eq!(code, 200, "{}", response); + } + + #[actix_rt::test] + async fn delete_unexisting_index() { + let server = Server::new().await; + let url = format!("/indexes/test/settings/{}", + stringify!($setting) + .chars() + .map(|c| if c == '_' { '-' } else { c }) + .collect::()); + let (_, code) = server.service.delete(url).await; + assert_eq!(code, 202); + let response = server.index("").wait_task(0).await; + assert_eq!(response["status"], "failed"); + } + + #[actix_rt::test] + async fn get_default() { + let server = Server::new().await; + let index = server.index("test"); + let (response, code) = index.create(None).await; + assert_eq!(code, 202, "{}", response); + index.wait_task(0).await; + let url = format!("/indexes/test/settings/{}", + stringify!($setting) + .chars() + .map(|c| if c == '_' { '-' } else { c }) + .collect::()); + let (response, code) = server.service.get(url).await; + assert_eq!(code, 200, "{}", response); + let expected = crate::json!($default_value); + assert_eq!(expected, response); + } } - }), - ); - map.insert( - "pagination", - json!({ - "maxTotalHits": json!(1000), - }), - ); - map.insert("search_cutoff_ms", json!(null)); - map -}); + )* + + #[actix_rt::test] + async fn all_setting_tested() { + let expected = std::collections::BTreeSet::from_iter(meilisearch::routes::indexes::settings::ALL_SETTINGS_NAMES.iter()); + let tested = std::collections::BTreeSet::from_iter([$(stringify!($setting)),*].iter()); + let diff: Vec<_> = expected.difference(&tested).collect(); + assert!(diff.is_empty(), "Not all settings were tested, please add the following settings to the `test_setting_routes!` macro: {:?}", diff); + } + }; +} + +test_setting_routes!( + { + setting: filterable_attributes, + update_verb: put, + default_value: [] + }, + { + setting: displayed_attributes, + update_verb: put, + default_value: ["*"] + }, + { + setting: localized_attributes, + update_verb: put, + default_value: null + }, + { + setting: searchable_attributes, + update_verb: put, + default_value: ["*"] + }, + { + setting: distinct_attribute, + update_verb: put, + default_value: null + }, + { + setting: stop_words, + update_verb: put, + default_value: [] + }, + { + setting: separator_tokens, + update_verb: put, + default_value: [] + }, + { + setting: non_separator_tokens, + update_verb: put, + default_value: [] + }, + { + setting: dictionary, + update_verb: put, + default_value: [] + }, + { + setting: ranking_rules, + update_verb: put, + default_value: ["words", "typo", "proximity", "attribute", "sort", "exactness"] + }, + { + setting: synonyms, + update_verb: put, + default_value: {} + }, + { + setting: pagination, + update_verb: patch, + default_value: {"maxTotalHits": 1000} + }, + { + setting: faceting, + update_verb: patch, + default_value: {"maxValuesPerFacet": 100, "sortFacetValuesBy": {"*": "alpha"}} + }, + { + setting: search_cutoff_ms, + update_verb: put, + default_value: null + }, + { + setting: embedders, + update_verb: patch, + default_value: null + }, + { + setting: facet_search, + update_verb: put, + default_value: true + }, + { + setting: prefix_search, + update_verb: put, + default_value: "indexingTime" + }, + { + setting: proximity_precision, + update_verb: put, + default_value: "byWord" + }, + { + setting: sortable_attributes, + update_verb: put, + default_value: [] + }, + { + setting: typo_tolerance, + update_verb: patch, + default_value: {"enabled": true, "minWordSizeForTypos": {"oneTypo": 5, "twoTypos": 9}, "disableOnWords": [], "disableOnAttributes": []} + }, +); #[actix_rt::test] async fn get_settings_unexisting_index() { @@ -342,93 +483,6 @@ async fn error_update_setting_unexisting_index_invalid_uid() { "###); } -macro_rules! test_setting_routes { - ($($setting:ident $write_method:ident), *) => { - $( - mod $setting { - use crate::common::Server; - use super::DEFAULT_SETTINGS_VALUES; - - #[actix_rt::test] - async fn get_unexisting_index() { - let server = Server::new().await; - let url = format!("/indexes/test/settings/{}", - stringify!($setting) - .chars() - .map(|c| if c == '_' { '-' } else { c }) - .collect::()); - let (_response, code) = server.service.get(url).await; - assert_eq!(code, 404); - } - - #[actix_rt::test] - async fn update_unexisting_index() { - let server = Server::new().await; - let url = format!("/indexes/test/settings/{}", - stringify!($setting) - .chars() - .map(|c| if c == '_' { '-' } else { c }) - .collect::()); - let (response, code) = server.service.$write_method(url, serde_json::Value::Null.into()).await; - assert_eq!(code, 202, "{}", response); - server.index("").wait_task(0).await; - let (response, code) = server.index("test").get().await; - assert_eq!(code, 200, "{}", response); - } - - #[actix_rt::test] - async fn delete_unexisting_index() { - let server = Server::new().await; - let url = format!("/indexes/test/settings/{}", - stringify!($setting) - .chars() - .map(|c| if c == '_' { '-' } else { c }) - .collect::()); - let (_, code) = server.service.delete(url).await; - assert_eq!(code, 202); - let response = server.index("").wait_task(0).await; - assert_eq!(response["status"], "failed"); - } - - #[actix_rt::test] - async fn get_default() { - let server = Server::new().await; - let index = server.index("test"); - let (response, code) = index.create(None).await; - assert_eq!(code, 202, "{}", response); - index.wait_task(0).await; - let url = format!("/indexes/test/settings/{}", - stringify!($setting) - .chars() - .map(|c| if c == '_' { '-' } else { c }) - .collect::()); - let (response, code) = server.service.get(url).await; - assert_eq!(code, 200, "{}", response); - let expected = DEFAULT_SETTINGS_VALUES.get(stringify!($setting)).unwrap(); - assert_eq!(expected, &response); - } - } - )* - }; -} - -test_setting_routes!( - filterable_attributes put, - displayed_attributes put, - localized_attributes put, - searchable_attributes put, - distinct_attribute put, - stop_words put, - separator_tokens put, - non_separator_tokens put, - dictionary put, - ranking_rules put, - synonyms put, - pagination patch, - faceting patch, - search_cutoff_ms put -); - #[actix_rt::test] async fn error_set_invalid_ranking_rules() { let server = Server::new().await;