diff --git a/examples/get-nth.rs b/examples/get-nth.rs index ee5ac59..2c0bf88 100644 --- a/examples/get-nth.rs +++ b/examples/get-nth.rs @@ -16,10 +16,39 @@ fn maybe_forty_two<'a>(edn: &'a Edn<'a>) -> Option<&'a Edn<'a>> { .nth(2) } +fn namespace_get_contains() { + // (def edn-data (edn/read-string "#:thingy {:foo \"bar\" :baz/bar \"qux\" 42 24}")) + let edn_data = edn::read_string(r#"#:thingy {:foo "bar" :baz/bar "qux" 42 24}"#).unwrap(); + + // (get edn-data 42) -> 24 + assert_eq!(edn_data.get(&Edn::Int(42)), Some(&Edn::Int(24))); + // (get edn-data :foo) -> nil + assert_eq!(edn_data.get(&Edn::Key("foo")), None); + // (get edn-data :thingy/foo) -> "bar" + assert_eq!(edn_data.get(&Edn::Key("thingy/foo")), Some(&Edn::Str("bar"))); + // (get edn-data :baz/bar) -> "qux" + assert_eq!(edn_data.get(&Edn::Key("baz/bar")), Some(&Edn::Str("qux"))); + + // (contains? edn-data 42) -> true + assert!(edn_data.contains(&Edn::Int(42))); + // (contains? edn-data "42") -> false + assert!(!edn_data.contains(&Edn::Str("42"))); + // (contains? edn-data :foo) -> false + assert!(!edn_data.contains(&Edn::Key("foo"))); + // (contains? edn-data :thingy/foo) -> true + assert!(edn_data.contains(&Edn::Key("thingy/foo"))); + // (contains? edn-data :baz/bar) -> true + assert!(edn_data.contains(&Edn::Key("baz/bar"))); + // (contains? edn-data :bar/baz) -> false + assert!(!edn_data.contains(&Edn::Key("bar/baz"))); +} + fn main() { let e = edn::read_string("{:foo {猫 {{:foo :bar} [1 2 42 3]}}}").unwrap(); let edn = maybe_forty_two(&e).unwrap(); assert_eq!(edn, &Edn::Int(42)); + + namespace_get_contains(); } #[test] diff --git a/src/edn.rs b/src/edn.rs index bf2ec9c..049b60b 100644 --- a/src/edn.rs +++ b/src/edn.rs @@ -74,12 +74,47 @@ pub fn read(edn: &str) -> Result<(Edn<'_>, &str), error::Error> { Ok((r.0, r.1)) } +fn get_tag<'a>(tag: &'a str, key: &'a str) -> Option<&'a str> { + // Break out early if there's no namespaces + if !key.contains('/') { + return None; + } + + // ignore the leading ':' + if !tag.starts_with(':') { + return None; + } + let tag = tag.get(1..)?; + Some(tag) +} + +fn check_key<'a>(tag: &'a str, key: &'a str) -> &'a str { + // check if the Key starts with the saved Tag + if key.starts_with(tag) { + let (_, key) = key.rsplit_once(tag).expect("Tag must exist, because it starts with it."); + + // ensure there's a '/' and strip it + if let Some(k) = key.strip_prefix('/') { + return k; + } + } + key +} + impl Edn<'_> { pub fn get(&self, e: &Self) -> Option<&Self> { if let Edn::Map(m) = self { - if let Some(l) = m.get(e) { - return Some(l); + return m.get(e); + } else if let Edn::Tagged(tag, m) = self { + if let Edn::Key(key) = e { + let tag = get_tag(tag, key)?; + let key = check_key(tag, key); + + return m.get(&Edn::Key(key)); } + + // Cover cases where it's not a keyword + return m.get(e); } None } @@ -92,6 +127,27 @@ impl Edn<'_> { vec.get(i) } + + pub fn contains(&self, e: &Self) -> bool { + match self { + Edn::Map(m) => m.contains_key(e), + Edn::Tagged(tag, m) => { + if let Edn::Key(key) = e { + let Some(tag) = get_tag(tag, key) else { return false }; + let key = check_key(tag, key); + + return m.contains(&Edn::Key(key)); + } + + // Cover cases where it's not a keyword + m.contains(e) + } + Edn::Vector(v) => v.contains(e), + Edn::Set(s) => s.contains(e), + Edn::List(l) => l.contains(e), + _ => false, + } + } } pub(crate) const fn char_to_edn(c: char) -> Option<&'static str> { diff --git a/src/parse.rs b/src/parse.rs index eaca281..1c490ad 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -98,10 +98,11 @@ impl Walker { let starting_ptr = self.ptr; loop { - if let Some(c) = self.nibble_next(slice) { - if c == ' ' { - return Ok(&slice[starting_ptr..self.ptr - 1]); + if let Some(c) = self.peek_next(slice) { + if c.is_whitespace() || DELIMITERS.contains(&c) { + return Ok(&slice[starting_ptr..self.ptr]); } + let _ = self.nibble_next(slice); } else { return Err(Error { code: Code::UnexpectedEOF, diff --git a/tests/navigation.rs b/tests/navigation.rs index 8775e35..35404f9 100644 --- a/tests/navigation.rs +++ b/tests/navigation.rs @@ -1,3 +1,7 @@ +extern crate alloc; + +use alloc::collections::BTreeMap; + use clojure_reader::edn::{self, Edn}; #[test] @@ -23,3 +27,96 @@ fn nth() { assert_eq!(e.nth(3), Some(&Edn::Int(42))); assert_eq!(e.nth(42), None); } + +#[test] +fn default_map_namespace_syntax() { + // see https://github.com/Grinkers/clojure-reader/issues/2 + let variations = [ + "{:thingy #:foo{:bar \"baz\"} :more \"stuff\"}", + "{:thingy #:foo {:bar \"baz\"} :more \"stuff\"}", + "{:more \"stuff\" :thingy #:foo{:bar \"baz\"}}", + "{:more \"stuff\" :thingy # :foo{:bar \"baz\"}}", + ]; + for v in variations { + let cfg = edn::read_string(&v).unwrap(); + + let Edn::Map(cfg) = cfg else { panic!() }; + assert_eq!( + cfg.get(&Edn::Key("thingy")), + Some(&Edn::Tagged( + ":foo", + Box::new(Edn::Map(BTreeMap::from([(Edn::Key("bar"), Edn::Str("baz"))]))) + )) + ); + assert_eq!(cfg.get(&Edn::Key("more")), Some(&Edn::Str("stuff"))); + } + + // without keyword `:` symbol. + // the tag is parsed/preserved, but we don't support custom readers + let variations = [ + "{:thingy #foo{:bar \"baz\"} :more \"stuff\"}", + "{:thingy #foo {:bar \"baz\"} :more \"stuff\"}", + "{:more \"stuff\" :thingy #foo{:bar \"baz\"}}", + "{:more \"stuff\" :thingy # foo{:bar \"baz\"}}", + ]; + for v in variations { + let cfg = edn::read_string(&v).unwrap(); + + let Edn::Map(cfg) = cfg else { panic!() }; + assert_eq!( + cfg.get(&Edn::Key("thingy")), + Some(&Edn::Tagged( + "foo", + Box::new(Edn::Map(BTreeMap::from([(Edn::Key("bar"), Edn::Str("baz"))]))) + )) + ); + assert_eq!(cfg.get(&Edn::Key("more")), Some(&Edn::Str("stuff"))); + } +} + +#[test] +fn namespace_syntax_edge_cases() { + let edn_data = edn::read_string(r#"#:thingy {:f#猫o "bar" :baz/bar "qux" 42 24}"#).unwrap(); + + assert_eq!(edn_data.get(&Edn::Key("thingy/f#猫o")), Some(&Edn::Str("bar"))); + assert_eq!(edn_data.get(&Edn::Key("baz/bar")), Some(&Edn::Str("qux"))); + assert_eq!(edn_data.get(&Edn::Key("foo")), None); + assert_eq!(edn_data.get(&Edn::Key("baz")), None); + assert_eq!(edn_data.get(&Edn::Key(":baz/bar")), None); + assert_eq!(edn_data.get(&Edn::Key("thingy/")), None); + assert_eq!(edn_data.get(&Edn::Key("thingy")), None); + assert_eq!(edn_data.get(&Edn::Key("thingything")), None); + + let edn_data = edn::read_string(r#"#thingy {:f#猫o "bar" :baz/bar "qux" 42 24}"#).unwrap(); + assert_eq!(edn_data.get(&Edn::Key("thingy/f#猫o")), None); + assert_eq!(edn_data.get(&Edn::Key("baz/bar")), None); +} + +#[test] +fn get_contains() { + let edn_data = edn::read_string(r#"{:f#猫o "bar" :baz/bar "qux" 42 24}"#).unwrap(); + assert_eq!(edn_data.get(&Edn::Key("f#猫o")), Some(&Edn::Str("bar"))); + assert_eq!(edn_data.contains(&Edn::Key("f#猫o")), true); + assert_eq!(edn_data.get(&Edn::Key("foo")), None); + assert_eq!(edn_data.contains(&Edn::Key("foo")), false); + + let edn_data = edn::read_string(r#"#{:f#猫o "bar" :baz/bar "qux" 42 24}"#).unwrap(); + assert_eq!(edn_data.contains(&Edn::Key("f#猫o")), true); + assert_eq!(edn_data.contains(&Edn::Int(42)), true); + assert_eq!(edn_data.contains(&Edn::Key("foo")), false); + + let edn_data = edn::read_string(r#"[:f#猫o "bar" :baz/bar "qux" 42 24]"#).unwrap(); + assert_eq!(edn_data.contains(&Edn::Key("f#猫o")), true); + assert_eq!(edn_data.contains(&Edn::Int(42)), true); + assert_eq!(edn_data.contains(&Edn::Key("foo")), false); + + let edn_data = edn::read_string(r#"(:f#猫o "bar" :baz/bar "qux" 42 24)"#).unwrap(); + assert_eq!(edn_data.contains(&Edn::Key("f#猫o")), true); + assert_eq!(edn_data.contains(&Edn::Int(42)), true); + assert_eq!(edn_data.contains(&Edn::Key("foo")), false); + + let edn_data = edn::read_string(r#"42"#).unwrap(); + assert_eq!(edn_data.contains(&Edn::Key("f#猫o")), false); + assert_eq!(edn_data.contains(&Edn::Int(42)), false); + assert_eq!(edn_data.contains(&Edn::Key("foo")), false); +} diff --git a/tests/read.rs b/tests/read.rs index 7a25076..cd28bf3 100644 --- a/tests/read.rs +++ b/tests/read.rs @@ -139,8 +139,10 @@ fn lisp_quoted() { } #[test] -fn numeric_like_symbols() { +fn numeric_like_symbols_keywords() { assert_eq!(edn::read_string("-foobar").unwrap(), Edn::Symbol("-foobar")); + assert_eq!(edn::read_string("-:thi#n=g").unwrap(), Edn::Symbol("-:thi#n=g")); + assert_eq!(edn::read_string(":thi#n=g").unwrap(), Edn::Key("thi#n=g")); assert_eq!( edn::read_string("(+foobar +foo+bar+ +'- '-+)").unwrap(),