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