diff options
Diffstat (limited to 'validator_derive/src/lib.rs')
| -rw-r--r-- | validator_derive/src/lib.rs | 672 |
1 files changed, 155 insertions, 517 deletions
diff --git a/validator_derive/src/lib.rs b/validator_derive/src/lib.rs index 6bc8313..1298006 100644 --- a/validator_derive/src/lib.rs +++ b/validator_derive/src/lib.rs @@ -1,33 +1,31 @@ #![recursion_limit = "128"] -#[macro_use] extern crate quote; +#[macro_use] +extern crate quote; extern crate proc_macro; extern crate syn; +#[macro_use] +extern crate if_chain; extern crate validator; use std::collections::HashMap; use proc_macro::TokenStream; use quote::ToTokens; -use validator::{Validator}; +use validator::Validator; -static RANGE_TYPES: [&'static str; 24] = [ - "usize", "u8", "u16", "u32", "u64", - "isize", "i8", "i16", "i32", "i64", - "f32", "f64", - "Option<usize>", "Option<u8>", "Option<u16>", "Option<u32>", "Option<u64>", - "Option<isize>", "Option<i8>", "Option<i16>", "Option<i32>", "Option<i64>", - "Option<f32>", "Option<f64>", -]; +mod lit; +mod validation; +mod asserts; +mod quoting; -#[derive(Debug)] -struct SchemaValidation { - function: String, - skip_on_field_errors: bool, -} +use lit::*; +use validation::*; +use asserts::{assert_string_type, assert_type_matches, assert_has_len, assert_has_range}; +use quoting::{FieldQuoter, quote_field_validation, quote_schema_validation}; #[proc_macro_derive(Validate, attributes(validate))] @@ -36,12 +34,13 @@ pub fn derive_validation(input: TokenStream) -> TokenStream { // Parse the string representation to an AST let ast = syn::parse_macro_input(&source).unwrap(); - let expanded = expand_validation(&ast); + let expanded = impl_validate(&ast); expanded.parse().unwrap() } -fn expand_validation(ast: &syn::MacroInput) -> quote::Tokens { +fn impl_validate(ast: &syn::MacroInput) -> quote::Tokens { + // Ensure the macro is on a struct with named fields let fields = match ast.body { syn::Body::Struct(syn::VariantData::Struct(ref fields)) => { if fields.iter().any(|field| field.ident.is_none()) { @@ -57,228 +56,17 @@ fn expand_validation(ast: &syn::MacroInput) -> quote::Tokens { let field_types = find_fields_type(&fields); for field in fields { - let field_ident = match field.ident { - Some(ref i) => i, - None => unreachable!() - }; - - let (name, validators) = find_validators_for_field(field, &field_types); - let field_name = field_types.get(&field_ident.to_string()).unwrap(); - // Don't put a & in front a pointer - let validator_param = if field_name.starts_with("&") { - quote!(self.#field_ident) - } else { - quote!(&self.#field_ident) - }; - // same but for the ident used in a if let block - let optional_validator_param = quote!(#field_ident); - // same but for the ident used in a if let Some variable - let optional_pattern_matched = if field_name.starts_with("Option<&") { - quote!(#field_ident) - } else { - quote!(ref #field_ident) - }; + let field_ident = field.ident.clone().unwrap(); + let (name, field_validations) = find_validators_for_field(field, &field_types); + let field_type = field_types.get(&field_ident.to_string()).cloned().unwrap(); + let field_quoter = FieldQuoter::new(field_ident, name, field_type); - for validator in &validators { - validations.push(match validator { - &Validator::Length {min, max, equal} => { - // Can't interpolate None - let min_tokens = option_u64_to_tokens(min); - let max_tokens = option_u64_to_tokens(max); - let equal_tokens = option_u64_to_tokens(equal); - // wrap in if-let if we have an option - if field_name.starts_with("Option<") { - quote!( - if let Some(#optional_pattern_matched) = self.#field_ident { - if !::validator::validate_length( - ::validator::Validator::Length { - min: #min_tokens, - max: #max_tokens, - equal: #equal_tokens - }, - #optional_validator_param - ) { - errors.add(#name, "length"); - } - } - ) - } else { - quote!( - if !::validator::validate_length( - ::validator::Validator::Length { - min: #min_tokens, - max: #max_tokens, - equal: #equal_tokens - }, - #validator_param - ) { - errors.add(#name, "length"); - } - ) - } - }, - &Validator::Range {min, max} => { - // wrap in if-let if we have an option - if field_name.starts_with("Option<") { - quote!( - if let Some(#field_ident) = self.#field_ident { - if !::validator::validate_range( - ::validator::Validator::Range {min: #min, max: #max}, - #field_ident as f64 - ) { - errors.add(#name, "range"); - } - } - ) - } else { - quote!( - if !::validator::validate_range( - ::validator::Validator::Range {min: #min, max: #max}, - self.#field_ident as f64 - ) { - errors.add(#name, "range"); - } - ) - } - }, - &Validator::Email => { - // wrap in if-let if we have an option - if field_name.starts_with("Option<") { - quote!( - if let Some(#optional_pattern_matched) = self.#field_ident { - if !::validator::validate_email(#optional_validator_param) { - errors.add(#name, "email"); - } - } - ) - } else { - quote!( - if !::validator::validate_email(#validator_param) { - errors.add(#name, "email"); - } - ) - } - } - &Validator::Url => { - // wrap in if-let if we have an option - if field_name.starts_with("Option<") { - quote!( - if let Some(#optional_pattern_matched) = self.#field_ident { - if !::validator::validate_url(#optional_validator_param) { - errors.add(#name, "url"); - } - } - ) - } else { - quote!( - if !::validator::validate_url(#validator_param) { - errors.add(#name, "url"); - } - ) - } - }, - &Validator::MustMatch(ref f) => { - let other_ident = syn::Ident::new(f.clone()); - quote!( - if !::validator::validate_must_match(&self.#field_ident, &self.#other_ident) { - errors.add(#name, "no_match"); - } - ) - }, - &Validator::Custom(ref f) => { - let fn_ident = syn::Ident::new(f.clone()); - // wrap in if-let if we have an option - if field_name.starts_with("Option<") { - quote!( - if let Some(#optional_pattern_matched) = self.#field_ident { - match #fn_ident(#optional_validator_param) { - ::std::option::Option::Some(s) => { - errors.add(#name, &s); - }, - ::std::option::Option::None => (), - }; - } - ) - } else { - quote!( - match #fn_ident(#validator_param) { - ::std::option::Option::Some(s) => { - errors.add(#name, &s); - }, - ::std::option::Option::None => (), - }; - ) - } - }, - &Validator::Contains(ref n) => { - // wrap in if-let if we have an option - if field_name.starts_with("Option<") { - quote!( - if let Some(#optional_pattern_matched) = self.#field_ident { - if !::validator::validate_contains(#optional_validator_param, &#n) { - errors.add(#name, "contains"); - } - } - ) - } else { - quote!( - if !::validator::validate_contains(#validator_param, &#n) { - errors.add(#name, "contains"); - } - ) - } - }, - &Validator::Regex(ref re) => { - let re_ident = syn::Ident::new(re.clone()); - // wrap in if-let if we have an option - if field_name.starts_with("Option<") { - quote!( - if let Some(#optional_pattern_matched) = self.#field_ident { - if !#re_ident.is_match(#optional_validator_param) { - errors.add(#name, "regex"); - } - } - ) - } else { - quote!( - if !#re_ident.is_match(#validator_param) { - errors.add(#name, "regex"); - } - ) - } - }, - }); + for validation in &field_validations { + validations.push(quote_field_validation(&field_quoter, validation)); } } - let struct_validation = find_struct_validation(&ast.attrs); - let struct_validation_tokens = match struct_validation { - Some(s) => { - let fn_ident = syn::Ident::new(s.function); - if s.skip_on_field_errors { - quote!( - if errors.is_empty() { - match #fn_ident(self) { - ::std::option::Option::Some((key, val)) => { - errors.add(&key, &val); - }, - ::std::option::Option::None => (), - } - } - ) - } else { - quote!( - match #fn_ident(self) { - ::std::option::Option::Some((key, val)) => { - errors.add(&key, &val); - }, - ::std::option::Option::None => (), - } - ) - } - }, - None => quote!() - }; + let schema_validation = quote_schema_validation(find_struct_validation(&ast.attrs)); let ident = &ast.ident; @@ -286,12 +74,12 @@ fn expand_validation(ast: &syn::MacroInput) -> quote::Tokens { let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); let impl_ast = quote!( impl #impl_generics Validate for #ident #ty_generics #where_clause { - fn validate(&self) -> ::std::result::Result<(), ::validator::Errors> { - let mut errors = ::validator::Errors::new(); + fn validate(&self) -> ::std::result::Result<(), ::validator::ValidationErrors> { + let mut errors = ::validator::ValidationErrors::new(); #(#validations)* - #struct_validation_tokens + #schema_validation if errors.is_empty() { ::std::result::Result::Ok(()) @@ -316,61 +104,80 @@ fn find_struct_validation(struct_attrs: &Vec<syn::Attribute>) -> Option<SchemaVa if attr.value.name() != "validate" { continue; } - match attr.value { - syn::MetaItem::List(_, ref meta_items) => { - match meta_items[0] { - syn::NestedMetaItem::MetaItem(ref item) => match item { - &syn::MetaItem::List(ref ident2, ref args) => { - if ident2 != "schema" { - error("Only `schema` is allowed as validator on a struct") - } - let mut function = "".to_string(); - let mut skip_on_field_errors = true; - for arg in args { - match *arg { - syn::NestedMetaItem::MetaItem(ref item) => match *item { - syn::MetaItem::NameValue(ref name, ref val) => { - match name.to_string().as_ref() { - "function" => { - function = match lit_to_string(val) { - Some(s) => s, - None => error("invalid argument type for `function` \ - : only a string is allowed"), - }; - }, - "skip_on_field_errors" => { - skip_on_field_errors = match lit_to_bool(val) { - Some(s) => s, - None => error("invalid argument type for `skip_on_field_errors` \ - : only a bool is allowed"), - }; - }, - _ => error("Unknown argument") - } - - }, - _ => error("Unexpected args") - }, - _ => error("Unexpected args") - } - } + if_chain! { + if let syn::MetaItem::List(_, ref meta_items) = attr.value; + if let syn::NestedMetaItem::MetaItem(ref item) = meta_items[0]; + if let &syn::MetaItem::List(ref ident2, ref args) = item; + + then { + if ident2 != "schema" { + error("Only `schema` is allowed as validator on a struct") + } - if function == "" { - error("`function` is required"); + let mut function = String::new(); + let mut skip_on_field_errors = true; + let mut code = None; + let mut message = None; + + for arg in args { + if_chain! { + if let syn::NestedMetaItem::MetaItem(ref item) = *arg; + if let syn::MetaItem::NameValue(ref name, ref val) = *item; + + then { + match name.to_string().as_ref() { + "function" => { + function = match lit_to_string(val) { + Some(s) => s, + None => error("invalid argument type for `function` \ + : only a string is allowed"), + }; + }, + "skip_on_field_errors" => { + skip_on_field_errors = match lit_to_bool(val) { + Some(s) => s, + None => error("invalid argument type for `skip_on_field_errors` \ + : only a bool is allowed"), + }; + }, + "code" => { + code = match lit_to_string(val) { + Some(s) => Some(s), + None => error("invalid argument type for `code` \ + : only a string is allowed"), + }; + }, + "message" => { + message = match lit_to_string(val) { + Some(s) => Some(s), + None => error("invalid argument type for `message` \ + : only a string is allowed"), + }; + }, + _ => error("Unknown argument") } + } else { + error("Unexpected args") + } + } + } - return Some(SchemaValidation { - function: function, - skip_on_field_errors: skip_on_field_errors - }); - }, - _ => error("Unexpected struct validator") - }, - _ => error("Unexpected struct validator") + if function == "" { + error("`function` is required"); } - }, - _ => error("Unexpected struct validator") + + return Some( + SchemaValidation { + function, + skip_on_field_errors, + code, + message, + } + ); + } else { + error("Unexpected struct validator") + } } } @@ -378,16 +185,13 @@ fn find_struct_validation(struct_attrs: &Vec<syn::Attribute>) -> Option<SchemaVa } -// Find all the types (as string) for each field of the struct -// Needed for the `must_match` filter +/// Find the types (as string) for each field of the struct +/// Needed for the `must_match` filter fn find_fields_type(fields: &Vec<syn::Field>) -> HashMap<String, String> { let mut types = HashMap::new(); for field in fields { - let field_name = match field.ident { - Some(ref s) => s.to_string(), - None => unreachable!(), - }; + let field_ident = field.ident.clone().unwrap().to_string(); let field_type = match field.ty { syn::Ty::Path(_, ref p) => { let mut tokens = quote::Tokens::new(); @@ -404,114 +208,31 @@ fn find_fields_type(fields: &Vec<syn::Field>) -> HashMap<String, String> { } name }, - _ => panic!("Type `{:?}` of field `{}` not supported", field.ty, field_name) + _ => panic!("Type `{:?}` of field `{}` not supported", field.ty, field_ident) }; + //println!("{:?}", field_type); - types.insert(field_name, field_type); + types.insert(field_ident, field_type); } types } -/// Find everything we need to know about a Field. -fn find_validators_for_field(field: &syn::Field, field_types: &HashMap<String, String>) -> (String, Vec<Validator>) { - let mut field_name = match field.ident { - Some(ref s) => s.to_string(), - None => unreachable!(), - }; +/// Find everything we need to know about a field: its real name if it's changed from the serialization +/// and the list of validators to run on it +fn find_validators_for_field(field: &syn::Field, field_types: &HashMap<String, String>) -> (String, Vec<FieldValidation>) { + let rust_ident = field.ident.clone().unwrap().to_string(); + let mut field_ident = field.ident.clone().unwrap().to_string(); let error = |msg: &str| -> ! { panic!("Invalid attribute #[validate] on field `{}`: {}", field.ident.clone().unwrap().to_string(), msg); }; - let field_type = field_types.get(&field_name).unwrap(); + + let field_type = field_types.get(&field_ident).unwrap(); let mut validators = vec![]; let mut has_validate = false; - let find_struct_validator = |name: String, meta_items: &Vec<syn::NestedMetaItem>| -> Validator { - match name.as_ref() { - "length" => { - let mut min = None; - let mut max = None; - let mut equal = None; - - for meta_item in meta_items { - match *meta_item { - syn::NestedMetaItem::MetaItem(ref item) => match *item { - syn::MetaItem::NameValue(ref name, ref val) => { - match name.to_string().as_ref() { - "min" => { - min = match lit_to_int(val) { - Some(s) => Some(s), - None => error("invalid argument type for `min` of `length` validator: only integers are allowed"), - }; - }, - "max" => { - max = match lit_to_int(val) { - Some(s) => Some(s), - None => error("invalid argument type for `max` of `length` validator: only integers are allowed"), - }; - }, - "equal" => { - equal = match lit_to_int(val) { - Some(s) => Some(s), - None => error("invalid argument type for `equal` of `length` validator: only integers are allowed"), - }; - }, - _ => error(&format!( - "unknown argument `{}` for validator `length` (it only has `min`, `max`, `equal`)", - name.to_string() - )) - } - }, - _ => panic!("unexpected item {:?} while parsing `length` validator", item) - }, - _=> unreachable!() - } - } - if equal.is_some() && (min.is_some() || max.is_some()) { - error("both `equal` and `min` or `max` have been set in `length` validator: probably a mistake"); - } - Validator::Length { min: min, max: max, equal: equal } - }, - "range" => { - let mut min = 0.0; - let mut max = 0.0; - for meta_item in meta_items { - match *meta_item { - syn::NestedMetaItem::MetaItem(ref item) => match *item { - syn::MetaItem::NameValue(ref name, ref val) => { - match name.to_string().as_ref() { - "min" => { - min = match lit_to_float(val) { - Some(s) => s, - None => error("invalid argument type for `min` of `range` validator: only integers are allowed") - }; - }, - "max" => { - max = match lit_to_float(val) { - Some(s) => s, - None => error("invalid argument type for `max` of `range` validator: only integers are allowed") - }; - }, - _ => error(&format!( - "unknown argument `{}` for validator `range` (it only has `min`, `max`)", - name.to_string() - )) - } - }, - _ => panic!("unexpected item {:?} while parsing `range` validator", item) - }, - _=> unreachable!() - } - } - - Validator::Range { min: min, max: max} - } - _ => panic!("unexpected list validator: {:?}", name) - } - }; - for attr in &field.attrs { if attr.name() != "validate" && attr.name() != "serde" { continue; @@ -523,11 +244,11 @@ fn find_validators_for_field(field: &syn::Field, field_types: &HashMap<String, S match attr.value { syn::MetaItem::List(_, ref meta_items) => { + // original name before serde rename if attr.name() == "serde" { - match find_original_field_name(meta_items) { - Some(s) => { field_name = s }, - None => () - }; + if let Some(s) = find_original_field_name(meta_items) { + field_ident = s; + } continue; } @@ -538,58 +259,41 @@ fn find_validators_for_field(field: &syn::Field, field_types: &HashMap<String, S // email, url syn::MetaItem::Word(ref name) => match name.to_string().as_ref() { "email" => { - if field_type != "String" - && field_type != "&str" - && field_type != "Option<String>" - && !(field_type.starts_with("Option<") && field_type.ends_with("str>")) { - panic!("`email` validator can only be used on String or &str"); - } - validators.push(Validator::Email); + assert_string_type("email", field_type); + validators.push(FieldValidation::new(Validator::Email)); }, "url" => { - if field_type != "String" - && field_type != "&str" - && field_type != "Option<String>" - && !(field_type.starts_with("Option<") && field_type.ends_with("str>")) { - panic!("`url` validator can only be used on String or &str"); - } - validators.push(Validator::Url); + assert_string_type("url", field_type); + validators.push(FieldValidation::new(Validator::Url)); }, - _ => panic!("Unexpected word validator: {}", name) + _ => panic!("Unexpected validator: {}", name) }, - // custom, contains, must_match + // custom, contains, must_match, regex syn::MetaItem::NameValue(ref name, ref val) => { match name.to_string().as_ref() { "custom" => { match lit_to_string(val) { - Some(s) => validators.push(Validator::Custom(s)), + Some(s) => validators.push(FieldValidation::new(Validator::Custom(s))), None => error("invalid argument for `custom` validator: only strings are allowed"), }; }, "contains" => { match lit_to_string(val) { - Some(s) => validators.push(Validator::Contains(s)), + Some(s) => validators.push(FieldValidation::new(Validator::Contains(s))), None => error("invalid argument for `contains` validator: only strings are allowed"), }; }, "regex" => { match lit_to_string(val) { - Some(s) => validators.push(Validator::Regex(s)), + Some(s) => validators.push(FieldValidation::new(Validator::Regex(s))), None => error("invalid argument for `regex` validator: only strings are allowed"), }; } "must_match" => { match lit_to_string(val) { Some(s) => { - if let Some(t2) = field_types.get(&s) { - if field_type == t2 { - validators.push(Validator::MustMatch(s)); - } else { - error("invalid argument for `must_match` validator: types of field can't match"); - } - } else { - error("invalid argument for `must_match` validator: field doesn't exist in struct"); - } + assert_type_matches(rust_ident.clone(), field_type, field_types.get(&s)); + validators.push(FieldValidation::new(Validator::MustMatch(s))); }, None => error("invalid argument for `must_match` validator: only strings are allowed"), }; @@ -597,49 +301,43 @@ fn find_validators_for_field(field: &syn::Field, field_types: &HashMap<String, S _ => panic!("unexpected name value validator: {:?}", name), }; }, - // validators with args: length for example - syn::MetaItem::List(ref name, ref meta_items) => { - // Some sanity checking first - if name == "length" { - if field_type != "String" - && !field_type.starts_with("Vec<") - && !field_type.starts_with("Option<Vec<") - && field_type != "Option<String>" - // a bit ugly - && !(field_type.starts_with("Option<") && field_type.ends_with("str>")) - && field_type != "&str" { - error(&format!( - "Validator `length` can only be used on types `String`, `&str` or `Vec` but found `{}`", - field_type - )); - } - - if meta_items.len() == 0 { - error("Validator `length` requires at least 1 argument out of `min`, `max` and `equal`"); - } - } - - if name == "range" { - if !RANGE_TYPES.contains(&field_type.as_ref()) { - error(&format!( - "Validator `range` can only be used on number types but found `{}`", - field_type - )); - } - - if meta_items.len() != 2 { - error("Validator `range` requires 2 arguments: `min` and `max`"); + // Validators with several args + syn::MetaItem::List(ref name, ref meta_items) => match name.to_string().as_ref() { + "length" => { + assert_has_len(rust_ident.clone(), field_type); + validators.push(extract_length_validation(rust_ident.clone(), meta_items)); + }, + "range" => { + assert_has_range(rust_ident.clone(), field_type); + validators.push(extract_range_validation(rust_ident.clone(), meta_items)); + }, + "email" | "url" => { + validators.push(extract_argless_validation(name.to_string(), rust_ident.clone(), meta_items)); + }, + "custom" => { + validators.push(extract_one_arg_validation("function", name.to_string(), rust_ident.clone(), meta_items)); + }, + "contains" => { + validators.push(extract_one_arg_validation("pattern", name.to_string(), rust_ident.clone(), meta_items)); + }, + "regex" => { + validators.push(extract_one_arg_validation("path", name.to_string(), rust_ident.clone(), meta_items)); + }, + "must_match" => { + let validation = extract_one_arg_validation("other", name.to_string(), rust_ident.clone(), meta_items); + if let Validator::MustMatch(ref t2) = validation.validator { + assert_type_matches(rust_ident.clone(), field_type, field_types.get(t2)); } - } - - validators.push(find_struct_validator(name.to_string(), meta_items)); + validators.push(validation); + }, + _ => panic!("unexpected list validator: {:?}", name.to_string()) }, }, _ => unreachable!("Found a non MetaItem while looking for validators") }; } }, - _ => unreachable!("Got something other than a list of attributes while checking field `{}`", field_name), + _ => unreachable!("Got something other than a list of attributes while checking field `{}`", field_ident), } } @@ -647,14 +345,14 @@ fn find_validators_for_field(field: &syn::Field, field_types: &HashMap<String, S error("it needs at least one validator"); } - (field_name, validators) + (field_ident, validators) } /// Serde can be used to rename fields on deserialization but most of the times /// we want the error on the original field. /// /// For example a JS frontend might send camelCase fields and Rust converts them to snake_case -/// but we want to send the errors back to the frontend with the original name +/// but we want to send the errors back with the original name fn find_original_field_name(meta_items: &Vec<syn::NestedMetaItem>) -> Option<String> { let mut original_name = None; @@ -666,9 +364,7 @@ fn find_original_field_name(meta_items: &Vec<syn::NestedMetaItem>) -> Option<Str if name == "rename" { original_name = Some(lit_to_string(val).unwrap()); } - }, - // length syn::MetaItem::List(_, ref meta_items) => { return find_original_field_name(meta_items); } @@ -684,61 +380,3 @@ fn find_original_field_name(meta_items: &Vec<syn::NestedMetaItem>) -> Option<Str original_name } - -fn lit_to_string(lit: &syn::Lit) -> Option<String> { - match *lit { - syn::Lit::Str(ref s, _) => Some(s.to_string()), - _ => None, - } -} - -fn lit_to_int(lit: &syn::Lit) -> Option<u64> { - match *lit { - syn::Lit::Int(ref s, _) => Some(*s), - // TODO: remove when attr_literals is stable - syn::Lit::Str(ref s, _) => Some(s.parse::<u64>().unwrap()), - _ => None, - } -} - -fn lit_to_float(lit: &syn::Lit) -> Option<f64> { - match *lit { - syn::Lit::Float(ref s, _) => Some(s.parse::<f64>().unwrap()), - syn::Lit::Int(ref s, _) => Some(*s as f64), - // TODO: remove when attr_literals is stable - syn::Lit::Str(ref s, _) => Some(s.parse::<f64>().unwrap()), - _ => None, - } -} - -fn lit_to_bool(lit: &syn::Lit) -> Option<bool> { - match *lit { - syn::Lit::Bool(ref s) => Some(*s), - // TODO: remove when attr_literals is stable - syn::Lit::Str(ref s, _) => if s == "true" { Some(true) } else { Some(false) }, - _ => None, - } -} - -fn option_u64_to_tokens(opt: Option<u64>) -> quote::Tokens { - let mut tokens = quote::Tokens::new(); - tokens.append("::"); - tokens.append("std"); - tokens.append("::"); - tokens.append("option"); - tokens.append("::"); - tokens.append("Option"); - tokens.append("::"); - match opt { - Some(ref t) => { - tokens.append("Some"); - tokens.append("("); - t.to_tokens(&mut tokens); - tokens.append(")"); - } - None => { - tokens.append("None"); - } - } - tokens -} |
