mirror of
				https://github.com/meilisearch/meilisearch.git
				synced 2025-10-30 23:46:28 +00:00 
			
		
		
		
	Use the IndexUid and StarOr in meilisearch_auth::Key
				
					
				
			Move `meilisearch_http::routes::StarOr` to `meilisearch_types::star_or` Fixes #2158
This commit is contained in:
		| @@ -2,6 +2,8 @@ use crate::action::Action; | |||||||
| use crate::error::{AuthControllerError, Result}; | use crate::error::{AuthControllerError, Result}; | ||||||
| use crate::store::KeyId; | use crate::store::KeyId; | ||||||
|  |  | ||||||
|  | use meilisearch_types::index_uid::IndexUid; | ||||||
|  | use meilisearch_types::star_or::StarOr; | ||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
| use serde_json::{from_value, Value}; | use serde_json::{from_value, Value}; | ||||||
| use time::format_description::well_known::Rfc3339; | use time::format_description::well_known::Rfc3339; | ||||||
| @@ -17,7 +19,7 @@ pub struct Key { | |||||||
|     pub name: Option<String>, |     pub name: Option<String>, | ||||||
|     pub uid: KeyId, |     pub uid: KeyId, | ||||||
|     pub actions: Vec<Action>, |     pub actions: Vec<Action>, | ||||||
|     pub indexes: Vec<String>, |     pub indexes: Vec<StarOr<IndexUid>>, | ||||||
|     #[serde(with = "time::serde::rfc3339::option")] |     #[serde(with = "time::serde::rfc3339::option")] | ||||||
|     pub expires_at: Option<OffsetDateTime>, |     pub expires_at: Option<OffsetDateTime>, | ||||||
|     #[serde(with = "time::serde::rfc3339")] |     #[serde(with = "time::serde::rfc3339")] | ||||||
| @@ -136,7 +138,7 @@ impl Key { | |||||||
|             description: Some("Use it for anything that is not a search operation. Caution! Do not expose it on a public frontend".to_string()), |             description: Some("Use it for anything that is not a search operation. Caution! Do not expose it on a public frontend".to_string()), | ||||||
|             uid, |             uid, | ||||||
|             actions: vec![Action::All], |             actions: vec![Action::All], | ||||||
|             indexes: vec!["*".to_string()], |             indexes: vec![StarOr::Star], | ||||||
|             expires_at: None, |             expires_at: None, | ||||||
|             created_at: now, |             created_at: now, | ||||||
|             updated_at: now, |             updated_at: now, | ||||||
| @@ -151,7 +153,7 @@ impl Key { | |||||||
|             description: Some("Use it to search from the frontend".to_string()), |             description: Some("Use it to search from the frontend".to_string()), | ||||||
|             uid, |             uid, | ||||||
|             actions: vec![Action::Search], |             actions: vec![Action::Search], | ||||||
|             indexes: vec!["*".to_string()], |             indexes: vec![StarOr::Star], | ||||||
|             expires_at: None, |             expires_at: None, | ||||||
|             created_at: now, |             created_at: now, | ||||||
|             updated_at: now, |             updated_at: now, | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ mod key; | |||||||
| mod store; | mod store; | ||||||
|  |  | ||||||
| use std::collections::{HashMap, HashSet}; | use std::collections::{HashMap, HashSet}; | ||||||
|  | use std::ops::Deref; | ||||||
| use std::path::Path; | use std::path::Path; | ||||||
| use std::sync::Arc; | use std::sync::Arc; | ||||||
|  |  | ||||||
| @@ -16,6 +17,7 @@ use uuid::Uuid; | |||||||
| pub use action::{actions, Action}; | pub use action::{actions, Action}; | ||||||
| use error::{AuthControllerError, Result}; | use error::{AuthControllerError, Result}; | ||||||
| pub use key::Key; | pub use key::Key; | ||||||
|  | use meilisearch_types::star_or::StarOr; | ||||||
| use store::generate_key_as_base64; | use store::generate_key_as_base64; | ||||||
| pub use store::open_auth_store_env; | pub use store::open_auth_store_env; | ||||||
| use store::HeedAuthStore; | use store::HeedAuthStore; | ||||||
| @@ -87,20 +89,22 @@ impl AuthController { | |||||||
|             .get_api_key(uid)? |             .get_api_key(uid)? | ||||||
|             .ok_or_else(|| AuthControllerError::ApiKeyNotFound(uid.to_string()))?; |             .ok_or_else(|| AuthControllerError::ApiKeyNotFound(uid.to_string()))?; | ||||||
|  |  | ||||||
|         if !key.indexes.iter().any(|i| i.as_str() == "*") { |         if !key.indexes.iter().any(|i| i == &StarOr::Star) { | ||||||
|             filters.search_rules = match search_rules { |             filters.search_rules = match search_rules { | ||||||
|                 // Intersect search_rules with parent key authorized indexes. |                 // Intersect search_rules with parent key authorized indexes. | ||||||
|                 Some(search_rules) => SearchRules::Map( |                 Some(search_rules) => SearchRules::Map( | ||||||
|                     key.indexes |                     key.indexes | ||||||
|                         .into_iter() |                         .into_iter() | ||||||
|                         .filter_map(|index| { |                         .filter_map(|index| { | ||||||
|                             search_rules |                             search_rules.get_index_search_rules(index.deref()).map( | ||||||
|                                 .get_index_search_rules(&index) |                                 |index_search_rules| { | ||||||
|                                 .map(|index_search_rules| (index, Some(index_search_rules))) |                                     (String::from(index), Some(index_search_rules)) | ||||||
|  |                                 }, | ||||||
|  |                             ) | ||||||
|                         }) |                         }) | ||||||
|                         .collect(), |                         .collect(), | ||||||
|                 ), |                 ), | ||||||
|                 None => SearchRules::Set(key.indexes.into_iter().collect()), |                 None => SearchRules::Set(key.indexes.into_iter().map(String::from).collect()), | ||||||
|             }; |             }; | ||||||
|         } else if let Some(search_rules) = search_rules { |         } else if let Some(search_rules) = search_rules { | ||||||
|             filters.search_rules = search_rules; |             filters.search_rules = search_rules; | ||||||
|   | |||||||
| @@ -3,12 +3,14 @@ use std::cmp::Reverse; | |||||||
| use std::convert::TryFrom; | use std::convert::TryFrom; | ||||||
| use std::convert::TryInto; | use std::convert::TryInto; | ||||||
| use std::fs::create_dir_all; | use std::fs::create_dir_all; | ||||||
|  | use std::ops::Deref; | ||||||
| use std::path::Path; | use std::path::Path; | ||||||
| use std::str; | use std::str; | ||||||
| use std::sync::Arc; | use std::sync::Arc; | ||||||
|  |  | ||||||
| use enum_iterator::IntoEnumIterator; | use enum_iterator::IntoEnumIterator; | ||||||
| use hmac::{Hmac, Mac}; | use hmac::{Hmac, Mac}; | ||||||
|  | use meilisearch_types::star_or::StarOr; | ||||||
| use milli::heed::types::{ByteSlice, DecodeIgnore, SerdeJson}; | use milli::heed::types::{ByteSlice, DecodeIgnore, SerdeJson}; | ||||||
| use milli::heed::{Database, Env, EnvOpenOptions, RwTxn}; | use milli::heed::{Database, Env, EnvOpenOptions, RwTxn}; | ||||||
| use sha2::{Digest, Sha256}; | use sha2::{Digest, Sha256}; | ||||||
| @@ -92,7 +94,7 @@ impl HeedAuthStore { | |||||||
|             key.actions.clone() |             key.actions.clone() | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         let no_index_restriction = key.indexes.contains(&"*".to_owned()); |         let no_index_restriction = key.indexes.contains(&StarOr::Star); | ||||||
|         for action in actions { |         for action in actions { | ||||||
|             if no_index_restriction { |             if no_index_restriction { | ||||||
|                 // If there is no index restriction we put None. |                 // If there is no index restriction we put None. | ||||||
| @@ -102,7 +104,7 @@ impl HeedAuthStore { | |||||||
|                 for index in key.indexes.iter() { |                 for index in key.indexes.iter() { | ||||||
|                     db.put( |                     db.put( | ||||||
|                         &mut wtxn, |                         &mut wtxn, | ||||||
|                         &(&uid, &action, Some(index.as_bytes())), |                         &(&uid, &action, Some(index.deref().as_bytes())), | ||||||
|                         &key.expires_at, |                         &key.expires_at, | ||||||
|                     )?; |                     )?; | ||||||
|                 } |                 } | ||||||
|   | |||||||
| @@ -151,7 +151,7 @@ impl KeyView { | |||||||
|             key: generated_key, |             key: generated_key, | ||||||
|             uid: key.uid, |             uid: key.uid, | ||||||
|             actions: key.actions, |             actions: key.actions, | ||||||
|             indexes: key.indexes, |             indexes: key.indexes.into_iter().map(String::from).collect(), | ||||||
|             expires_at: key.expires_at, |             expires_at: key.expires_at, | ||||||
|             created_at: key.created_at, |             created_at: key.created_at, | ||||||
|             updated_at: key.updated_at, |             updated_at: key.updated_at, | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ use meilisearch_lib::index_controller::{DocumentAdditionFormat, Update}; | |||||||
| use meilisearch_lib::milli::update::IndexDocumentsMethod; | use meilisearch_lib::milli::update::IndexDocumentsMethod; | ||||||
| use meilisearch_lib::MeiliSearch; | use meilisearch_lib::MeiliSearch; | ||||||
| use meilisearch_types::error::ResponseError; | use meilisearch_types::error::ResponseError; | ||||||
|  | use meilisearch_types::star_or::StarOr; | ||||||
| use mime::Mime; | use mime::Mime; | ||||||
| use once_cell::sync::Lazy; | use once_cell::sync::Lazy; | ||||||
| use serde::Deserialize; | use serde::Deserialize; | ||||||
| @@ -22,7 +23,7 @@ use crate::error::MeilisearchHttpError; | |||||||
| use crate::extractors::authentication::{policies::*, GuardedData}; | use crate::extractors::authentication::{policies::*, GuardedData}; | ||||||
| use crate::extractors::payload::Payload; | use crate::extractors::payload::Payload; | ||||||
| use crate::extractors::sequential_extractor::SeqHandler; | use crate::extractors::sequential_extractor::SeqHandler; | ||||||
| use crate::routes::{fold_star_or, PaginationView, StarOr}; | use crate::routes::{fold_star_or, PaginationView}; | ||||||
| use crate::task::SummarizedTaskView; | use crate::task::SummarizedTaskView; | ||||||
|  |  | ||||||
| static ACCEPTED_CONTENT_TYPE: Lazy<Vec<String>> = Lazy::new(|| { | static ACCEPTED_CONTENT_TYPE: Lazy<Vec<String>> = Lazy::new(|| { | ||||||
|   | |||||||
| @@ -1,5 +1,3 @@ | |||||||
| use std::str::FromStr; |  | ||||||
|  |  | ||||||
| use actix_web::{web, HttpResponse}; | use actix_web::{web, HttpResponse}; | ||||||
| use log::debug; | use log::debug; | ||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
| @@ -9,6 +7,7 @@ use time::OffsetDateTime; | |||||||
| use meilisearch_lib::index::{Settings, Unchecked}; | use meilisearch_lib::index::{Settings, Unchecked}; | ||||||
| use meilisearch_lib::MeiliSearch; | use meilisearch_lib::MeiliSearch; | ||||||
| use meilisearch_types::error::ResponseError; | use meilisearch_types::error::ResponseError; | ||||||
|  | use meilisearch_types::star_or::StarOr; | ||||||
|  |  | ||||||
| use crate::extractors::authentication::{policies::*, GuardedData}; | use crate::extractors::authentication::{policies::*, GuardedData}; | ||||||
|  |  | ||||||
| @@ -27,26 +26,6 @@ pub fn configure(cfg: &mut web::ServiceConfig) { | |||||||
|         .service(web::scope("/indexes").configure(indexes::configure)); |         .service(web::scope("/indexes").configure(indexes::configure)); | ||||||
| } | } | ||||||
|  |  | ||||||
| /// A type that tries to match either a star (*) or |  | ||||||
| /// any other thing that implements `FromStr`. |  | ||||||
| #[derive(Debug)] |  | ||||||
| pub enum StarOr<T> { |  | ||||||
|     Star, |  | ||||||
|     Other(T), |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl<T: FromStr> FromStr for StarOr<T> { |  | ||||||
|     type Err = T::Err; |  | ||||||
|  |  | ||||||
|     fn from_str(s: &str) -> Result<Self, Self::Err> { |  | ||||||
|         if s.trim() == "*" { |  | ||||||
|             Ok(StarOr::Star) |  | ||||||
|         } else { |  | ||||||
|             T::from_str(s).map(StarOr::Other) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Extracts the raw values from the `StarOr` types and | /// Extracts the raw values from the `StarOr` types and | ||||||
| /// return None if a `StarOr::Star` is encountered. | /// return None if a `StarOr::Star` is encountered. | ||||||
| pub fn fold_star_or<T, O>(content: impl IntoIterator<Item = StarOr<T>>) -> Option<O> | pub fn fold_star_or<T, O>(content: impl IntoIterator<Item = StarOr<T>>) -> Option<O> | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ use meilisearch_lib::tasks::TaskFilter; | |||||||
| use meilisearch_lib::MeiliSearch; | use meilisearch_lib::MeiliSearch; | ||||||
| use meilisearch_types::error::ResponseError; | use meilisearch_types::error::ResponseError; | ||||||
| use meilisearch_types::index_uid::IndexUid; | use meilisearch_types::index_uid::IndexUid; | ||||||
|  | use meilisearch_types::star_or::StarOr; | ||||||
| use serde::Deserialize; | use serde::Deserialize; | ||||||
| use serde_cs::vec::CS; | use serde_cs::vec::CS; | ||||||
| use serde_json::json; | use serde_json::json; | ||||||
| @@ -13,7 +14,7 @@ use crate::extractors::authentication::{policies::*, GuardedData}; | |||||||
| use crate::extractors::sequential_extractor::SeqHandler; | use crate::extractors::sequential_extractor::SeqHandler; | ||||||
| use crate::task::{TaskListView, TaskStatus, TaskType, TaskView}; | use crate::task::{TaskListView, TaskStatus, TaskType, TaskView}; | ||||||
|  |  | ||||||
| use super::{fold_star_or, StarOr}; | use super::fold_star_or; | ||||||
|  |  | ||||||
| const DEFAULT_LIMIT: fn() -> usize = || 20; | const DEFAULT_LIMIT: fn() -> usize = || 20; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -358,6 +358,32 @@ async fn error_add_api_key_invalid_parameters_indexes() { | |||||||
|     assert_eq!(response, expected_response); |     assert_eq!(response, expected_response); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[actix_rt::test] | ||||||
|  | async fn error_add_api_key_invalid_index_uids() { | ||||||
|  |     let mut server = Server::new_auth().await; | ||||||
|  |     server.use_api_key("MASTER_KEY"); | ||||||
|  |  | ||||||
|  |     let content = json!({ | ||||||
|  |         "description": Value::Null, | ||||||
|  |         "indexes": ["invalid index # / \\name with spaces"], | ||||||
|  |         "actions": [ | ||||||
|  |             "documents.add" | ||||||
|  |         ], | ||||||
|  |         "expiresAt": "2050-11-13T00:00:00" | ||||||
|  |     }); | ||||||
|  |     let (response, code) = server.add_api_key(content).await; | ||||||
|  |  | ||||||
|  |     let expected_response = json!({ | ||||||
|  |         "message": r#"`indexes` field value `["invalid index # / \\name with spaces"]` is invalid. It should be an array of string representing index names."#, | ||||||
|  |         "code": "invalid_api_key_indexes", | ||||||
|  |         "type": "invalid_request", | ||||||
|  |         "link": "https://docs.meilisearch.com/errors#invalid_api_key_indexes" | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     assert_eq!(response, expected_response); | ||||||
|  |     assert_eq!(code, 400); | ||||||
|  | } | ||||||
|  |  | ||||||
| #[actix_rt::test] | #[actix_rt::test] | ||||||
| async fn error_add_api_key_invalid_parameters_actions() { | async fn error_add_api_key_invalid_parameters_actions() { | ||||||
|     let mut server = Server::new_auth().await; |     let mut server = Server::new_auth().await; | ||||||
|   | |||||||
| @@ -1,2 +1,3 @@ | |||||||
| pub mod error; | pub mod error; | ||||||
| pub mod index_uid; | pub mod index_uid; | ||||||
|  | pub mod star_or; | ||||||
|   | |||||||
							
								
								
									
										138
									
								
								meilisearch-types/src/star_or.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								meilisearch-types/src/star_or.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,138 @@ | |||||||
|  | use serde::de::Visitor; | ||||||
|  | use serde::{Deserialize, Deserializer, Serialize, Serializer}; | ||||||
|  | use std::fmt::{Display, Formatter}; | ||||||
|  | use std::marker::PhantomData; | ||||||
|  | use std::ops::Deref; | ||||||
|  | use std::str::FromStr; | ||||||
|  |  | ||||||
|  | /// A type that tries to match either a star (*) or | ||||||
|  | /// any other thing that implements `FromStr`. | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub enum StarOr<T> { | ||||||
|  |     Star, | ||||||
|  |     Other(T), | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl<T: FromStr> FromStr for StarOr<T> { | ||||||
|  |     type Err = T::Err; | ||||||
|  |  | ||||||
|  |     fn from_str(s: &str) -> Result<Self, Self::Err> { | ||||||
|  |         if s.trim() == "*" { | ||||||
|  |             Ok(StarOr::Star) | ||||||
|  |         } else { | ||||||
|  |             T::from_str(s).map(StarOr::Other) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl<T: Deref<Target = str>> Deref for StarOr<T> { | ||||||
|  |     type Target = str; | ||||||
|  |  | ||||||
|  |     fn deref(&self) -> &Self::Target { | ||||||
|  |         match self { | ||||||
|  |             Self::Star => "*", | ||||||
|  |             Self::Other(t) => t.deref(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl<T: Into<String>> From<StarOr<T>> for String { | ||||||
|  |     fn from(s: StarOr<T>) -> Self { | ||||||
|  |         match s { | ||||||
|  |             StarOr::Star => "*".to_string(), | ||||||
|  |             StarOr::Other(t) => t.into(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl<T: PartialEq> PartialEq for StarOr<T> { | ||||||
|  |     fn eq(&self, other: &Self) -> bool { | ||||||
|  |         match (self, other) { | ||||||
|  |             (Self::Star, Self::Star) => true, | ||||||
|  |             (Self::Other(left), Self::Other(right)) if left.eq(right) => true, | ||||||
|  |             _ => false, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl<T: PartialEq + Eq> Eq for StarOr<T> {} | ||||||
|  |  | ||||||
|  | impl<'de, T, E> Deserialize<'de> for StarOr<T> | ||||||
|  | where | ||||||
|  |     T: FromStr<Err = E>, | ||||||
|  |     E: Display, | ||||||
|  | { | ||||||
|  |     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> | ||||||
|  |     where | ||||||
|  |         D: Deserializer<'de>, | ||||||
|  |     { | ||||||
|  |         /// Serde can't differentiate between `StarOr::Star` and `StarOr::Other` without a tag. | ||||||
|  |         /// Simply using `#[serde(untagged)]` + `#[serde(rename="*")]` will lead to attempting to | ||||||
|  |         /// deserialize everything as a `StarOr::Other`, including "*". | ||||||
|  |         /// [`#[serde(other)]`](https://serde.rs/variant-attrs.html#other) might have helped but is | ||||||
|  |         /// not supported on untagged enums. | ||||||
|  |         struct StarOrVisitor<T>(PhantomData<T>); | ||||||
|  |  | ||||||
|  |         impl<'de, T, FE> Visitor<'de> for StarOrVisitor<T> | ||||||
|  |         where | ||||||
|  |             T: FromStr<Err = FE>, | ||||||
|  |             FE: Display, | ||||||
|  |         { | ||||||
|  |             type Value = StarOr<T>; | ||||||
|  |  | ||||||
|  |             fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { | ||||||
|  |                 formatter.write_str("a string") | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             fn visit_str<SE>(self, v: &str) -> Result<Self::Value, SE> | ||||||
|  |             where | ||||||
|  |                 SE: serde::de::Error, | ||||||
|  |             { | ||||||
|  |                 match v { | ||||||
|  |                     "*" => Ok(StarOr::Star), | ||||||
|  |                     v => { | ||||||
|  |                         let other = FromStr::from_str(v).map_err(|e: T::Err| { | ||||||
|  |                             SE::custom(format!("Invalid `other` value: {}", e)) | ||||||
|  |                         })?; | ||||||
|  |                         Ok(StarOr::Other(other)) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         deserializer.deserialize_str(StarOrVisitor(PhantomData)) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl<T> Serialize for StarOr<T> | ||||||
|  | where | ||||||
|  |     T: Deref<Target = str>, | ||||||
|  | { | ||||||
|  |     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> | ||||||
|  |     where | ||||||
|  |         S: Serializer, | ||||||
|  |     { | ||||||
|  |         match self { | ||||||
|  |             StarOr::Star => serializer.serialize_str("*"), | ||||||
|  |             StarOr::Other(other) => serializer.serialize_str(other.deref()), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[cfg(test)] | ||||||
|  | mod tests { | ||||||
|  |     use super::*; | ||||||
|  |     use serde_json::{json, Value}; | ||||||
|  |  | ||||||
|  |     #[test] | ||||||
|  |     fn star_or_serde_roundtrip() { | ||||||
|  |         fn roundtrip(content: Value, expected: StarOr<String>) { | ||||||
|  |             let deserialized: StarOr<String> = serde_json::from_value(content.clone()).unwrap(); | ||||||
|  |             assert_eq!(deserialized, expected); | ||||||
|  |             assert_eq!(content, serde_json::to_value(deserialized).unwrap()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         roundtrip(json!("products"), StarOr::Other("products".to_string())); | ||||||
|  |         roundtrip(json!("*"), StarOr::Star); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -206,7 +206,7 @@ fn create_value(value: &Document, mut selectors: HashSet<&str>) -> Document { | |||||||
|     new_value |     new_value | ||||||
| } | } | ||||||
|  |  | ||||||
| fn create_array(array: &Vec<Value>, selectors: &HashSet<&str>) -> Vec<Value> { | fn create_array(array: &[Value], selectors: &HashSet<&str>) -> Vec<Value> { | ||||||
|     let mut res = Vec::new(); |     let mut res = Vec::new(); | ||||||
|  |  | ||||||
|     for value in array { |     for value in array { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user