diff options
| author | eichlan <eichlan@jotunn.office.penny-arcade.com> | 2026-06-23 15:31:51 -0700 |
|---|---|---|
| committer | eichlan <eichlan@jotunn.office.penny-arcade.com> | 2026-06-23 15:31:51 -0700 |
| commit | 062048ef6e3e060bcf841ea2ec92e026f09d8da0 (patch) | |
| tree | 22f7dad062b16f25fcfa1a28af13ac2937348ef0 /crimtag/src/lib.rs | |
| parent | 7c299c86bbac76ec2ac199d68eb4dda093a64e3a (diff) | |
| download | crimtag-062048ef6e3e060bcf841ea2ec92e026f09d8da0.tar.gz crimtag-062048ef6e3e060bcf841ea2ec92e026f09d8da0.tar.bz2 crimtag-062048ef6e3e060bcf841ea2ec92e026f09d8da0.tar.xz crimtag-062048ef6e3e060bcf841ea2ec92e026f09d8da0.zip | |
Roorganized into workspace and packages.
This is to accomidate a new proc_macro package, which can't have
anything else in it.
Diffstat (limited to 'crimtag/src/lib.rs')
| -rw-r--r-- | crimtag/src/lib.rs | 544 |
1 files changed, 544 insertions, 0 deletions
diff --git a/crimtag/src/lib.rs b/crimtag/src/lib.rs new file mode 100644 index 0000000..e8f9e9f --- /dev/null +++ b/crimtag/src/lib.rs | |||
| @@ -0,0 +1,544 @@ | |||
| 1 | use std::path::{PathBuf,Path}; | ||
| 2 | use std::collections::HashMap; | ||
| 3 | use core::error::Error; | ||
| 4 | use std::time::SystemTime; | ||
| 5 | |||
| 6 | mod lexer; | ||
| 7 | mod parser; | ||
| 8 | mod position; | ||
| 9 | mod error; | ||
| 10 | pub mod context; | ||
| 11 | |||
| 12 | pub use error::{CrimError,CrimResult}; | ||
| 13 | pub use position::*; | ||
| 14 | pub use context::{Context,Value,MappedStructure,StatefulContext,MappedValue,MappedList,MappedHash}; | ||
| 15 | |||
| 16 | #[derive(Debug)] | ||
| 17 | pub struct Crimtag { | ||
| 18 | views: HashMap<String, View>, | ||
| 19 | sources: Vec<ViewSource>, | ||
| 20 | } | ||
| 21 | |||
| 22 | #[derive(PartialEq,Eq,Debug,Clone)] | ||
| 23 | pub enum ViewSource { | ||
| 24 | File { | ||
| 25 | path: PathBuf, | ||
| 26 | loaded: SystemTime, | ||
| 27 | }, | ||
| 28 | Static, | ||
| 29 | External { | ||
| 30 | key: String, | ||
| 31 | }, | ||
| 32 | } | ||
| 33 | |||
| 34 | #[derive(Debug)] | ||
| 35 | struct View { | ||
| 36 | name: String, | ||
| 37 | source: usize, | ||
| 38 | outputs: Vec<Output>, | ||
| 39 | // theme: Option<String> | ||
| 40 | layout: Option<String>, | ||
| 41 | } | ||
| 42 | |||
| 43 | impl View { | ||
| 44 | fn process(&self, input: &dyn for<'a> MappedStructure<'a>) -> CrimResult<Context> { | ||
| 45 | let mut out_vars = Context::new(); | ||
| 46 | for output in &self.outputs { | ||
| 47 | let mut buf = String::new(); | ||
| 48 | let sc = StatefulContext::root( input ); | ||
| 49 | exec( &sc, &output.code, &mut buf )?; | ||
| 50 | out_vars.insert( output.name.clone(), Value::String(buf) ); | ||
| 51 | } | ||
| 52 | Ok(out_vars) | ||
| 53 | } | ||
| 54 | } | ||
| 55 | |||
| 56 | #[derive(Debug)] | ||
| 57 | struct Output { | ||
| 58 | name: String, | ||
| 59 | code: Vec<Token>, | ||
| 60 | } | ||
| 61 | |||
| 62 | type Properties = HashMap<String, String>; | ||
| 63 | |||
| 64 | #[derive(Debug)] | ||
| 65 | enum Token { | ||
| 66 | Loop(Identifier, Vec<Token>), | ||
| 67 | Show(Identifier,Properties), | ||
| 68 | If(Identifier, Vec<Token>, Vec<Token>), | ||
| 69 | Text(String), | ||
| 70 | } | ||
| 71 | |||
| 72 | #[derive(Debug,Clone,PartialEq)] | ||
| 73 | pub enum IdentifierValue { | ||
| 74 | Root, | ||
| 75 | Name(String), | ||
| 76 | Index(usize), | ||
| 77 | } | ||
| 78 | |||
| 79 | pub type Identifier = Vec<IdentifierValue>; | ||
| 80 | |||
| 81 | impl Crimtag { | ||
| 82 | pub fn new() -> Self { | ||
| 83 | Self { | ||
| 84 | views: HashMap::new(), | ||
| 85 | sources: vec![ViewSource::Static], | ||
| 86 | } | ||
| 87 | } | ||
| 88 | |||
| 89 | fn id_source(&mut self, src: &ViewSource ) -> usize { | ||
| 90 | for idx in 0..self.sources.len() { | ||
| 91 | if self.sources[idx] == *src { | ||
| 92 | return idx; | ||
| 93 | } | ||
| 94 | } | ||
| 95 | |||
| 96 | // Nothing found, add a new one | ||
| 97 | let idx = self.sources.len(); | ||
| 98 | self.sources.push( src.clone() ); | ||
| 99 | idx | ||
| 100 | } | ||
| 101 | |||
| 102 | pub fn load_file(&mut self, path: &Path) -> Result<(),Box::<dyn Error>> { | ||
| 103 | let time = if let Ok(meta) = path.metadata() { | ||
| 104 | if let Ok(t) = meta.modified() { | ||
| 105 | t | ||
| 106 | } else if let Ok(t) = meta.created() { | ||
| 107 | t | ||
| 108 | } else { | ||
| 109 | SystemTime::now() | ||
| 110 | } | ||
| 111 | } else { | ||
| 112 | SystemTime::now() | ||
| 113 | }; | ||
| 114 | let source_id = self.id_source( | ||
| 115 | &ViewSource::File { | ||
| 116 | path: path.to_path_buf(), | ||
| 117 | loaded: time, | ||
| 118 | } | ||
| 119 | ); | ||
| 120 | let p = parser::Parser::new(); | ||
| 121 | self.register_views( | ||
| 122 | p.parse( | ||
| 123 | &String::from_utf8( std::fs::read( path )? )?, | ||
| 124 | source_id | ||
| 125 | )?, | ||
| 126 | )?; | ||
| 127 | Ok(()) | ||
| 128 | } | ||
| 129 | |||
| 130 | pub fn load_static(&mut self, data: &str) -> CrimResult<()> { | ||
| 131 | let source_id = self.id_source( &ViewSource::Static ); | ||
| 132 | let p = parser::Parser::new(); | ||
| 133 | self.register_views( p.parse( &data, source_id )? ) | ||
| 134 | } | ||
| 135 | |||
| 136 | pub fn load_external(&mut self, key: &str, data: &str) -> CrimResult<()> { | ||
| 137 | let source_id = self.id_source( | ||
| 138 | &ViewSource::External{ key: key.to_string()} | ||
| 139 | ); | ||
| 140 | |||
| 141 | let p = parser::Parser::new(); | ||
| 142 | self.register_views( | ||
| 143 | p.parse( &data, source_id )?, | ||
| 144 | ) | ||
| 145 | } | ||
| 146 | |||
| 147 | fn register_views(&mut self, views: Vec<View>) -> CrimResult<()> { | ||
| 148 | for view in views { | ||
| 149 | self.views.insert( view.name.clone(), view ); | ||
| 150 | } | ||
| 151 | Ok(()) | ||
| 152 | } | ||
| 153 | |||
| 154 | pub fn get_view_source(&self, view: &str) -> Option<ViewSource> { | ||
| 155 | if let Some(view) = self.views.get(view) { | ||
| 156 | if view.source < self.sources.len() { | ||
| 157 | Some(self.sources[view.source].clone()) | ||
| 158 | } else { | ||
| 159 | None | ||
| 160 | } | ||
| 161 | } else { | ||
| 162 | None | ||
| 163 | } | ||
| 164 | } | ||
| 165 | |||
| 166 | pub fn render_partial(&self, view: &str, input: &dyn for<'a> MappedStructure<'a> ) -> CrimResult<Context> { | ||
| 167 | if let Some(view) = self.views.get(view) { | ||
| 168 | return view.process( input ) | ||
| 169 | } else { | ||
| 170 | return Err(CrimError::other("No such view found".into())); | ||
| 171 | } | ||
| 172 | } | ||
| 173 | |||
| 174 | pub fn render(&self, view: &str, input: &dyn for<'a> MappedStructure<'a> ) -> CrimResult<String> { | ||
| 175 | if let Some(view) = self.views.get( view ) { | ||
| 176 | //println!("::Token tree::\n{:?}", view ); | ||
| 177 | let mut output = view.process( input )?; | ||
| 178 | if let Some(l) = &view.layout { | ||
| 179 | self.render( l, &output ) | ||
| 180 | } else { | ||
| 181 | if let Some(content) = output.remove("content") && | ||
| 182 | let Value::String(s) = content { | ||
| 183 | |||
| 184 | Ok(s) | ||
| 185 | } else { | ||
| 186 | Err(CrimError::other("No content found in root layout.".into())) | ||
| 187 | } | ||
| 188 | } | ||
| 189 | |||
| 190 | } else { | ||
| 191 | Err(CrimError::other("No such view found".into())) | ||
| 192 | } | ||
| 193 | } | ||
| 194 | } | ||
| 195 | |||
| 196 | fn exec<'a>(input: &StatefulContext<'a>, tokens: &Vec<Token>, buf: &mut String) -> CrimResult<()> { | ||
| 197 | for token in tokens { | ||
| 198 | match token { | ||
| 199 | Token::Loop(ident,code) => { | ||
| 200 | //println!("!!! Loop over: {:?} {:?}", ident, input.get_value( &ident )); | ||
| 201 | if let Some(value) = input.get_value( &ident ) && | ||
| 202 | let MappedValue::List(list) = value { | ||
| 203 | let output = | ||
| 204 | &list.for_each(&|rec: &MappedValue, buf: &mut String| { | ||
| 205 | if let MappedValue::Struct(d) = rec { | ||
| 206 | exec( &input.local(*d), code, buf )?; | ||
| 207 | } | ||
| 208 | Ok(()) | ||
| 209 | })?; | ||
| 210 | //println!("!!! Output from loop: {}", output ); | ||
| 211 | buf.push_str( &output ); | ||
| 212 | } | ||
| 213 | } | ||
| 214 | Token::If(ident,code,other) => { | ||
| 215 | if let Some(value) = input.get_value( &ident ) && | ||
| 216 | let MappedValue::Bool(b) = value && b { | ||
| 217 | exec(input, code, buf)? | ||
| 218 | } else { | ||
| 219 | exec(input, other, buf)? | ||
| 220 | } | ||
| 221 | } | ||
| 222 | Token::Show(ident,p) => { | ||
| 223 | let format = if let Some(s) = p.get("format") { | ||
| 224 | s | ||
| 225 | } else { | ||
| 226 | &"".to_string() | ||
| 227 | }; | ||
| 228 | if let Some(value) = input.get_value( &ident ) { | ||
| 229 | match value { | ||
| 230 | MappedValue::Struct(_) => { | ||
| 231 | buf.push_str("Struct"); | ||
| 232 | } | ||
| 233 | MappedValue::List(_) => { | ||
| 234 | buf.push_str("List"); | ||
| 235 | } | ||
| 236 | MappedValue::Bool(b) => { | ||
| 237 | if b { | ||
| 238 | buf.push_str("true"); | ||
| 239 | } else { | ||
| 240 | buf.push_str("false"); | ||
| 241 | } | ||
| 242 | } | ||
| 243 | MappedValue::Str(s) => { | ||
| 244 | buf.push_str(s); | ||
| 245 | } | ||
| 246 | MappedValue::String(s) => { | ||
| 247 | buf.push_str(s.as_str()); | ||
| 248 | } | ||
| 249 | MappedValue::Int8(i) => { | ||
| 250 | buf.push_str(&i.to_string()); | ||
| 251 | } | ||
| 252 | MappedValue::Int16(i) => { | ||
| 253 | buf.push_str(&i.to_string()); | ||
| 254 | } | ||
| 255 | MappedValue::Int32(i) => { | ||
| 256 | buf.push_str(&i.to_string()); | ||
| 257 | } | ||
| 258 | MappedValue::Int64(i) => { | ||
| 259 | buf.push_str(&i.to_string()); | ||
| 260 | } | ||
| 261 | MappedValue::UInt8(i) => { | ||
| 262 | buf.push_str(&i.to_string()); | ||
| 263 | } | ||
| 264 | MappedValue::UInt16(i) => { | ||
| 265 | buf.push_str(&i.to_string()); | ||
| 266 | } | ||
| 267 | MappedValue::UInt32(i) => { | ||
| 268 | buf.push_str(&i.to_string()); | ||
| 269 | } | ||
| 270 | MappedValue::UInt64(i) => { | ||
| 271 | buf.push_str(&i.to_string()); | ||
| 272 | } | ||
| 273 | MappedValue::Float32(f) => { | ||
| 274 | if format == "" { | ||
| 275 | buf.push_str(&f.to_string()); | ||
| 276 | } else { | ||
| 277 | buf.push_str(&f.to_string()); | ||
| 278 | } | ||
| 279 | } | ||
| 280 | MappedValue::Float64(f) => { | ||
| 281 | if format == "" { | ||
| 282 | buf.push_str(&f.to_string()); | ||
| 283 | } else { | ||
| 284 | buf.push_str(&f.to_string()); | ||
| 285 | } | ||
| 286 | } | ||
| 287 | } | ||
| 288 | } | ||
| 289 | } | ||
| 290 | Token::Text(s) => { | ||
| 291 | buf.push_str( s ); | ||
| 292 | } | ||
| 293 | //_ => {} | ||
| 294 | } | ||
| 295 | } | ||
| 296 | Ok(()) | ||
| 297 | } | ||
| 298 | |||
| 299 | #[cfg(test)] | ||
| 300 | mod tests { | ||
| 301 | use super::*; | ||
| 302 | |||
| 303 | use crate::lexer::*; | ||
| 304 | |||
| 305 | #[test] | ||
| 306 | fn lexing() { | ||
| 307 | let data = r#"Leading comment: [|view "basic" something="yeup"|>Hello there <|view|] Trailing text"#; | ||
| 308 | let ll = lexer::Lexer::new( &data ); | ||
| 309 | for sym in ll { | ||
| 310 | if let SymbolType::Error{what} = sym.symbol() { | ||
| 311 | println!("Error {}:{}: {:?}", sym.start().line, sym.start().column, what ); | ||
| 312 | break; | ||
| 313 | } else { | ||
| 314 | println!("Symbol: {:?}", sym ); | ||
| 315 | } | ||
| 316 | } | ||
| 317 | } | ||
| 318 | |||
| 319 | #[test] | ||
| 320 | fn parsing() { | ||
| 321 | let mut ct = Crimtag::new(); | ||
| 322 | if let Err(e) = ct.load_static(r#"Here is a sample | ||
| 323 | [|view "index" theme="standard"|><html><body>Hi</body></html><|view|] | ||
| 324 | That was fun! now a placeholder: [|view "placeholder"|] and now one with an implicit [|view "simple"|>Body -> [|show content|] <- and end<|view|] aoeu | ||
| 325 | |||
| 326 | [|view "person" layout="simple"|>Welcome [|show person.last_name|], [|show person.first_name|]! It's lovely to see you again.<|view|] | ||
| 327 | |||
| 328 | Now with explicit outputs: | ||
| 329 | [|view "complex"|> | ||
| 330 | [|output "content"|> | ||
| 331 | Here's a [|show name|]. | ||
| 332 | <|output|] | ||
| 333 | [|output "sidebar"|> | ||
| 334 | What's up world? | ||
| 335 | <|output|] | ||
| 336 | [|output "footer"|> | ||
| 337 | Same, I guess! | ||
| 338 | <|output|] | ||
| 339 | <|view|]"#) { | ||
| 340 | println!("Error: {:?}", e ); | ||
| 341 | } else { | ||
| 342 | println!("It finished"); | ||
| 343 | if let Ok(v) = ct.render( | ||
| 344 | "person", | ||
| 345 | &Context::from([ | ||
| 346 | ("hi".to_string(),Value::String("hi".to_string())), | ||
| 347 | ("person".to_string(),Value::Dictionary(HashMap::from([ | ||
| 348 | ("first_name".to_string(), Value::String("Bob".to_string())), | ||
| 349 | ("last_name".to_string(), Value::String("Smith".to_string())), | ||
| 350 | ]))), | ||
| 351 | ]) | ||
| 352 | ) { | ||
| 353 | println!("View result: {:?}", v ); | ||
| 354 | } else { | ||
| 355 | println!("Error?"); | ||
| 356 | } | ||
| 357 | } | ||
| 358 | } | ||
| 359 | |||
| 360 | #[test] | ||
| 361 | fn loops() -> Result<(),CrimError> { | ||
| 362 | let mut ct = Crimtag::new(); | ||
| 363 | ct.load_static(r#"Looping code: | ||
| 364 | [|view "index"|>We will enumerate people here:[|loop people|> | ||
| 365 | - [|show last_name|], [|show first_name|][|if show_title |>: [|show title|] <|else|> **title redacted** <|if|] ::>[|loop tags|> [|show tag|]<|loop|] <:: <|loop|] | ||
| 366 | <|view|] | ||
| 367 | "#)?; | ||
| 368 | |||
| 369 | let ctx = Context::from([ | ||
| 370 | ("people".to_string(), vec![ | ||
| 371 | [ | ||
| 372 | ("first_name".into(), "Joe".into()), | ||
| 373 | ("last_name".into(), "Smith".into()), | ||
| 374 | ("show_title".into(), true.into()), | ||
| 375 | ("title".into(), "CEO".into()), | ||
| 376 | ("tags".into(), vec![ | ||
| 377 | Context::from([("tag".into(), "jerk".into()),]).into(), | ||
| 378 | Context::from([("tag".into(), "ugly".into()),]).into(), | ||
| 379 | ].into()), | ||
| 380 | ].into(), | ||
| 381 | [ | ||
| 382 | ("first_name".into(), "Chris".into()), | ||
| 383 | ("last_name".into(), "Perkens".into()), | ||
| 384 | ("show_title".into(), false.into()), | ||
| 385 | ("title".into(), "Baconeer".into()), | ||
| 386 | ("tags".into(), vec![ | ||
| 387 | Context::from([("tag".into(), "jerk".into()),]).into(), | ||
| 388 | Context::from([("tag".into(), "ugly".into()),]).into(), | ||
| 389 | ].into()), | ||
| 390 | ].into(), | ||
| 391 | [ | ||
| 392 | ("first_name".into(), "Will".into()), | ||
| 393 | ("last_name".into(), "Power".into()), | ||
| 394 | ("show_title".into(), false.into()), | ||
| 395 | ("title".into(), "CFO".into()), | ||
| 396 | ("tags".into(), vec![ | ||
| 397 | Context::from([("tag".into(), "jerk".into()),]).into(), | ||
| 398 | Context::from([("tag".into(), "ugly".into()),]).into(), | ||
| 399 | ].into()), | ||
| 400 | ].into(), | ||
| 401 | [ | ||
| 402 | ("first_name".into(), "Justin".into()), | ||
| 403 | ("last_name".into(), "Time".into()), | ||
| 404 | ("show_title".into(), true.into()), | ||
| 405 | ("title".into(), "CIO".into()), | ||
| 406 | ("tags".into(), vec![ | ||
| 407 | Context::from([("tag".into(), "jerk".into()),]).into(), | ||
| 408 | Context::from([("tag".into(), "ugly".into()),]).into(), | ||
| 409 | ].into()), | ||
| 410 | ].into(), | ||
| 411 | ].into()), | ||
| 412 | ]); | ||
| 413 | |||
| 414 | println!("View result: {}", ct.render("index", &ctx)? ); | ||
| 415 | |||
| 416 | Ok(()) | ||
| 417 | } | ||
| 418 | |||
| 419 | #[test] | ||
| 420 | fn conditional() -> Result<(),CrimError> { | ||
| 421 | let mut ct = Crimtag::new(); | ||
| 422 | ct.load_static(r#"Hi there | ||
| 423 | [|view "index"|> | ||
| 424 | Color: [|if is_red|> red <|elif is_blue |> blue <|else|> green <|if|] | ||
| 425 | <|view|]"#)?; | ||
| 426 | |||
| 427 | let ctx = Context::from([ | ||
| 428 | ("is_red".into(), false.into()), | ||
| 429 | ("is_blue".into(), false.into()), | ||
| 430 | ("color".into(), "purple".into()), | ||
| 431 | ]); | ||
| 432 | |||
| 433 | println!("View result: {}", ct.render("index", &ctx)? ); | ||
| 434 | |||
| 435 | Ok(()) | ||
| 436 | } | ||
| 437 | |||
| 438 | struct Item { | ||
| 439 | pub id: i32, | ||
| 440 | pub title: String, | ||
| 441 | pub body: String, | ||
| 442 | } | ||
| 443 | |||
| 444 | struct Person { | ||
| 445 | pub id: i32, | ||
| 446 | pub username: String, | ||
| 447 | pub name: String, | ||
| 448 | } | ||
| 449 | |||
| 450 | struct Page { | ||
| 451 | pub user: Person, | ||
| 452 | pub total_items: i32, | ||
| 453 | pub total_pages: i32, | ||
| 454 | pub cur_page: i32, | ||
| 455 | pub items: Vec<Item>, | ||
| 456 | } | ||
| 457 | |||
| 458 | impl Page { | ||
| 459 | fn new() -> Page { | ||
| 460 | Page { | ||
| 461 | user: Person { | ||
| 462 | id: 443, | ||
| 463 | username: "eichlan".into(), | ||
| 464 | name: "Mike".into(), | ||
| 465 | }, | ||
| 466 | total_items: 4000, | ||
| 467 | total_pages: 400, | ||
| 468 | cur_page: 35, | ||
| 469 | items: vec![ | ||
| 470 | Item{ | ||
| 471 | id: 123, | ||
| 472 | title: "hi".into(), | ||
| 473 | body: "body".into(), | ||
| 474 | }, | ||
| 475 | Item{ | ||
| 476 | id: 124, | ||
| 477 | title: "bye".into(), | ||
| 478 | body: "wooo".into(), | ||
| 479 | }, | ||
| 480 | Item{ | ||
| 481 | id: 125, | ||
| 482 | title: "ciao".into(), | ||
| 483 | body: "whatever".into(), | ||
| 484 | } | ||
| 485 | ], | ||
| 486 | } | ||
| 487 | } | ||
| 488 | } | ||
| 489 | |||
| 490 | impl<'a> MappedStructure<'a> for Page { | ||
| 491 | fn get_value(&'a self, id: &str) -> Option<MappedValue<'a>> { | ||
| 492 | match id { | ||
| 493 | "user" => Some(MappedValue::Struct(&self.user)), | ||
| 494 | "total_items" => Some(MappedValue::Int32(self.total_items)), | ||
| 495 | "total_pages" => Some(MappedValue::Int32(self.total_pages)), | ||
| 496 | "cur_page" => Some(MappedValue::Int32(self.cur_page)), | ||
| 497 | "items" => Some(MappedValue::<'a>::List(&self.items)), | ||
| 498 | _ => None, | ||
| 499 | } | ||
| 500 | } | ||
| 501 | } | ||
| 502 | |||
| 503 | impl<'a> MappedStructure<'a> for Person { | ||
| 504 | fn get_value(&'a self, id: &str) -> Option<MappedValue<'a>> { | ||
| 505 | match id { | ||
| 506 | "id" => Some(MappedValue::Int32(self.id)), | ||
| 507 | "username" => Some(MappedValue::String(&self.username)), | ||
| 508 | "name" => Some(MappedValue::String(&self.name)), | ||
| 509 | _ => None, | ||
| 510 | } | ||
| 511 | } | ||
| 512 | } | ||
| 513 | |||
| 514 | impl<'a> MappedStructure<'a> for Item { | ||
| 515 | fn get_value(&'a self, id: &str) -> Option<MappedValue<'a>> { | ||
| 516 | match id { | ||
| 517 | "id" => Some(MappedValue::Int32(self.id)), | ||
| 518 | "title" => Some(MappedValue::String(&self.title)), | ||
| 519 | "body" => Some(MappedValue::String(&self.body)), | ||
| 520 | _ => None, | ||
| 521 | } | ||
| 522 | } | ||
| 523 | } | ||
| 524 | |||
| 525 | #[test] | ||
| 526 | fn custom() -> Result<(), CrimError> { | ||
| 527 | let page = Page::new(); | ||
| 528 | |||
| 529 | println!("{:?}", | ||
| 530 | page.get_value(&"total_pages") | ||
| 531 | ); | ||
| 532 | |||
| 533 | if let Some(MappedValue::List(l)) = page.get_value(&"items") { | ||
| 534 | l.for_each(&|x: &MappedValue, buf: &mut String| { | ||
| 535 | if let MappedValue::Struct(s) = x { | ||
| 536 | println!(" - {:?}", s.get_value(&"title")); | ||
| 537 | buf.push_str(&format!("{:?}",s.get_value(&"title"))); | ||
| 538 | } | ||
| 539 | Ok(()) | ||
| 540 | }).expect("loop"); | ||
| 541 | } | ||
| 542 | Ok(()) | ||
| 543 | } | ||
| 544 | } | ||
