use crate::prelude::*;
use crate::{Error, Result, WasmFeatures, require_feature};
use core::cmp::Ordering;
use core::fmt;
use core::hash::{Hash, Hasher};
use core::ops::Deref;
use semver::Version;
#[derive(Debug, Eq, Clone, Copy)]
#[repr(transparent)]
pub struct KebabStr<'a>(&'a str);
impl<'a> KebabStr<'a> {
pub fn new(s: &'a str) -> Option<Self> {
let s = Self::new_unchecked(s);
if s.is_kebab_case() { Some(s) } else { None }
}
pub(crate) fn new_unchecked(s: &'a str) -> Self {
Self(s)
}
pub fn as_str(&self) -> &'a str {
self.0
}
pub fn slice<I>(&self, index: I) -> KebabStr<'a>
where
I: core::slice::SliceIndex<str, Output = str>,
{
KebabStr(&self.0[index])
}
pub fn to_kebab_string(&self) -> KebabString {
KebabString(self.to_string())
}
fn canonical_chars(&self) -> impl Iterator<Item = char> + '_ {
self.chars()
.filter(|c| *c != '-')
.map(|c| c.to_ascii_lowercase())
}
fn is_kebab_case(&self) -> bool {
let mut lower = false;
let mut upper = false;
let mut is_first = true;
let mut has_digit = false;
for c in self.chars() {
match c {
'a'..='z' if !lower && !upper => lower = true,
'A'..='Z' if !lower && !upper => upper = true,
'0'..='9' if !lower && !upper && !is_first => has_digit = true,
'a'..='z' if lower => {}
'A'..='Z' if upper => {}
'0'..='9' if lower || upper => has_digit = true,
'-' if lower || upper || has_digit => {
lower = false;
upper = false;
is_first = false;
has_digit = false;
}
_ => return false,
}
}
!self.is_empty() && !self.ends_with('-')
}
}
impl Deref for KebabStr<'_> {
type Target = str;
fn deref(&self) -> &str {
self.as_str()
}
}
impl PartialEq for KebabStr<'_> {
fn eq(&self, other: &Self) -> bool {
self.canonical_chars().eq(other.canonical_chars())
}
}
impl PartialEq<KebabString> for KebabStr<'_> {
fn eq(&self, other: &KebabString) -> bool {
self.eq(&other.as_kebab_str())
}
}
impl Ord for KebabStr<'_> {
fn cmp(&self, other: &Self) -> Ordering {
self.canonical_chars().cmp(other.canonical_chars())
}
}
impl PartialOrd for KebabStr<'_> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Hash for KebabStr<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.canonical_chars().count().hash(state);
for c in self.canonical_chars() {
c.hash(state);
}
}
}
impl fmt::Display for KebabStr<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.as_str().fmt(f)
}
}
#[derive(Debug, Clone, Eq)]
pub struct KebabString(String);
impl KebabString {
pub fn new(s: impl Into<String>) -> Option<Self> {
let s = s.into();
if KebabStr::new(&s).is_some() {
Some(Self(s))
} else {
None
}
}
pub fn new_unchecked(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
pub fn as_kebab_str(&self) -> KebabStr<'_> {
KebabStr::new_unchecked(self.as_str())
}
}
impl Deref for KebabString {
type Target = str;
fn deref(&self) -> &str {
self.as_str()
}
}
impl Ord for KebabString {
fn cmp(&self, other: &Self) -> Ordering {
self.as_kebab_str().cmp(&other.as_kebab_str())
}
}
impl PartialOrd for KebabString {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.as_kebab_str().partial_cmp(&other.as_kebab_str())
}
}
impl PartialEq for KebabString {
fn eq(&self, other: &Self) -> bool {
self.as_kebab_str().eq(&other.as_kebab_str())
}
}
impl PartialEq<KebabStr<'_>> for KebabString {
fn eq(&self, other: &KebabStr<'_>) -> bool {
self.as_kebab_str().eq(other)
}
}
impl Hash for KebabString {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_kebab_str().hash(state)
}
}
impl fmt::Display for KebabString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.as_kebab_str().fmt(f)
}
}
impl From<KebabString> for String {
fn from(s: KebabString) -> String {
s.0
}
}
#[derive(Clone)]
pub struct ComponentName {
raw: String,
kind: ParsedComponentNameKind,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum ParsedComponentNameKind {
Plain,
Interface,
}
#[derive(Debug, Clone)]
pub enum ComponentNameKind<'a> {
Plain(PlainName<'a>),
#[allow(missing_docs)]
Interface(InterfaceName<'a>),
}
const CONSTRUCTOR: &str = "[constructor]";
const METHOD: &str = "[method]";
const STATIC: &str = "[static]";
const GET: &str = "[get]";
const SET: &str = "[set]";
impl ComponentName {
pub fn new(name: &str, offset: u64) -> Result<ComponentName> {
Self::new_with_features(name, offset, WasmFeatures::default())
}
pub fn new_with_features(name: &str, offset: u64, features: WasmFeatures) -> Result<Self> {
let mut parser = ComponentNameParser {
next: name,
offset,
features,
};
let kind = parser.parse()?;
if !parser.next.is_empty() {
bail!(offset, "trailing characters found: `{}`", parser.next);
}
Ok(ComponentName {
raw: name.to_string(),
kind,
})
}
pub fn kind(&self) -> ComponentNameKind<'_> {
use ComponentNameKind::*;
use ParsedComponentNameKind as PK;
match self.kind {
PK::Plain => Plain(PlainName::new(&self.raw)),
PK::Interface => Interface(InterfaceName(&self.raw)),
}
}
pub fn as_str(&self) -> &str {
&self.raw
}
}
impl From<ComponentName> for String {
fn from(name: ComponentName) -> String {
name.raw
}
}
impl Hash for ComponentName {
fn hash<H: Hasher>(&self, hasher: &mut H) {
self.kind().hash(hasher)
}
}
impl PartialEq for ComponentName {
fn eq(&self, other: &ComponentName) -> bool {
self.kind().eq(&other.kind())
}
}
impl Eq for ComponentName {}
impl Ord for ComponentName {
fn cmp(&self, other: &ComponentName) -> Ordering {
self.kind().cmp(&other.kind())
}
}
impl PartialOrd for ComponentName {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Display for ComponentName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.raw.fmt(f)
}
}
impl fmt::Debug for ComponentName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.raw.fmt(f)
}
}
impl ComponentNameKind<'_> {
fn kind(&self) -> ParsedComponentNameKind {
match self {
Self::Plain(_) => ParsedComponentNameKind::Plain,
Self::Interface(_) => ParsedComponentNameKind::Interface,
}
}
}
impl Ord for ComponentNameKind<'_> {
fn cmp(&self, other: &Self) -> Ordering {
use ComponentNameKind::*;
match (self, other) {
(Plain(lhs), Plain(rhs)) => lhs.cmp(rhs),
(Interface(lhs), Interface(rhs)) => lhs.cmp(rhs),
(Plain(_), _) | (Interface(_), _) => self.kind().cmp(&other.kind()),
}
}
}
impl PartialOrd for ComponentNameKind<'_> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Hash for ComponentNameKind<'_> {
fn hash<H: Hasher>(&self, hasher: &mut H) {
use ComponentNameKind::*;
match self {
Plain(name) => (0u8, name).hash(hasher),
Interface(name) => (1u8, name).hash(hasher),
}
}
}
impl PartialEq for ComponentNameKind<'_> {
fn eq(&self, other: &ComponentNameKind<'_>) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl Eq for ComponentNameKind<'_> {}
#[derive(Debug, Clone)]
pub struct ResourceFunc<'a>(&'a str);
impl<'a> ResourceFunc<'a> {
pub fn as_str(&self) -> &'a str {
self.0
}
pub fn resource(&self) -> KebabStr<'a> {
let dot = self.0.find('.').unwrap();
KebabStr::new_unchecked(&self.0[..dot])
}
pub fn method(&self) -> KebabStr<'a> {
let dot = self.0.find('.').unwrap();
KebabStr::new_unchecked(&self.0[dot + 1..])
}
}
impl Ord for ResourceFunc<'_> {
fn cmp(&self, other: &Self) -> Ordering {
(self.resource(), self.method()).cmp(&(other.resource(), other.method()))
}
}
impl PartialOrd for ResourceFunc<'_> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for ResourceFunc<'_> {
fn eq(&self, other: &Self) -> bool {
self.resource() == other.resource() && self.method() == other.method()
}
}
impl Eq for ResourceFunc<'_> {}
impl Hash for ResourceFunc<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.resource().hash(state);
self.method().hash(state);
}
}
#[derive(Debug, Clone)]
pub struct PlainName<'a> {
unannotated: KebabStr<'a>,
pub resource_func: Option<ResourceFuncKind>,
pub accessor: Option<AccessorKind>,
}
impl<'a> PlainName<'a> {
pub fn new(raw: &'a str) -> PlainName<'a> {
use AccessorKind as AK;
use ResourceFuncKind as RF;
let mut raw_unannotated = raw;
let rf;
if raw_unannotated.starts_with(CONSTRUCTOR) {
rf = Some(RF::Constructor);
raw_unannotated = &raw_unannotated[CONSTRUCTOR.len()..];
} else if raw.starts_with(METHOD) {
rf = Some(RF::Method);
raw_unannotated = &raw_unannotated[METHOD.len()..];
} else if raw.starts_with(STATIC) {
rf = Some(RF::Static);
raw_unannotated = &raw_unannotated[STATIC.len()..];
} else {
rf = None;
}
let ak;
if raw_unannotated.starts_with(GET) {
ak = Some(AK::Get);
raw_unannotated = &raw_unannotated[GET.len()..];
} else if raw_unannotated.starts_with(SET) {
ak = Some(AK::Set);
raw_unannotated = &raw_unannotated[SET.len()..];
} else {
ak = None;
}
PlainName {
unannotated: KebabStr::new_unchecked(raw_unannotated),
resource_func: rf,
accessor: ak,
}
}
fn canonicalized(&self) -> KebabString {
let lower: String = self.unannotated.canonical_chars().collect();
if let Some(dot) = lower.find('.') {
if lower[..dot] == lower[dot + 1..] {
return KebabString::new_unchecked(&lower[..dot]);
}
}
KebabString::new_unchecked(
if self.resource_func == Some(ResourceFuncKind::Constructor) {
format!("{CONSTRUCTOR}{lower}")
} else if self.accessor == Some(AccessorKind::Set) {
format!("{SET}{lower}")
} else {
lower
},
)
}
pub fn as_str(&self) -> &'a str {
self.unannotated.0
}
pub fn resource(&self) -> Option<KebabStr<'a>> {
use ResourceFuncKind as RF;
match self.resource_func {
Some(RF::Constructor) => Some(self.unannotated),
Some(RF::Method) | Some(RF::Static) => {
let dot = self.unannotated.find('.').unwrap();
Some(self.unannotated.slice(..dot))
}
None => None,
}
}
pub fn name(&self) -> KebabStr<'a> {
let after_dot = match self.unannotated.find('.') {
Some(dot) => dot + 1,
None => 0,
};
self.unannotated.slice(after_dot..)
}
pub fn is_bare(&self) -> bool {
self.resource_func.is_none() && self.accessor.is_none()
}
pub fn getter_for_setter(&self) -> String {
assert!(self.accessor == Some(AccessorKind::Set));
let prefix = match self.resource_func {
Some(ResourceFuncKind::Method) => METHOD,
Some(ResourceFuncKind::Static) => STATIC,
None => "",
_ => unreachable!(),
};
format!("{prefix}{GET}{}", self.unannotated)
}
}
impl Ord for PlainName<'_> {
fn cmp(&self, other: &Self) -> Ordering {
self.canonicalized().cmp(&other.canonicalized())
}
}
impl PartialOrd for PlainName<'_> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for PlainName<'_> {
fn eq(&self, other: &PlainName<'_>) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl Eq for PlainName<'_> {}
impl Hash for PlainName<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.canonicalized().hash(state);
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum ResourceFuncKind {
Constructor,
Method,
Static,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum AccessorKind {
Get,
Set,
}
#[derive(Debug, Clone)]
pub struct InterfaceName<'a>(&'a str);
#[derive(Ord, PartialOrd, Eq, PartialEq, Hash)]
enum InterfaceNameComponent<'a> {
Namespace(KebabStr<'a>),
Projection(KebabStr<'a>),
VersionPrefix(&'a str),
}
impl<'a> InterfaceName<'a> {
pub fn as_str(&self) -> &'a str {
self.0
}
pub fn namespace(&self) -> KebabStr<'a> {
let colon = self.0.rfind(':').unwrap();
KebabStr::new_unchecked(&self.0[..colon])
}
pub fn package(&self) -> KebabStr<'a> {
let colon = self.0.rfind(':').unwrap();
let slash = self.0.find('/').unwrap();
KebabStr::new_unchecked(&self.0[colon + 1..slash])
}
pub fn interface(&self) -> KebabStr<'a> {
let projection = self.projection();
let slash = projection.find('/').unwrap_or(projection.len());
KebabStr::new_unchecked(&projection.0[..slash])
}
pub fn projection(&self) -> KebabStr<'a> {
let slash = self.0.find('/').unwrap();
let at = self.0.find('@').unwrap_or(self.0.len());
KebabStr::new_unchecked(&self.0[slash + 1..at])
}
pub fn version(&self, suffix: Option<&str>) -> Result<Option<Version>> {
let invalid = |e: &str| Error::new(e, 0);
match (self.version_prefix(), suffix) {
(None, None) => Ok(None),
(None, Some(_)) => Err(invalid(
"a version suffix requires the name to have a version",
)),
(Some(prefix), None) => Ok(Some(
Version::parse(prefix).map_err(|e| invalid(&e.to_string()))?,
)),
(Some(prefix), Some(suffix)) => {
let full = format!("{prefix}{suffix}");
let version = Version::parse(&full).map_err(|e| invalid(&e.to_string()))?;
if split_canonical_version(&full) != Some((prefix, Some(suffix))) {
return Err(invalid(&format!(
"`{prefix}` is not the canonical version of `{full}`"
)));
}
Ok(Some(version))
}
}
}
fn version_prefix(&self) -> Option<&'a str> {
let at = self.0.find('@')?;
Some(&self.0[at + 1..])
}
fn components(&self) -> impl Iterator<Item = InterfaceNameComponent<'a>> {
let mut next = self.0;
let mut prev_char = None;
core::iter::from_fn(move || {
if next.is_empty() {
return None;
}
match next.find([':', '/', '@']) {
Some(i) => {
let ch = next.as_bytes()[i];
let name = KebabStr::new_unchecked(&next[..i]);
next = &next[i + 1..];
prev_char = Some(ch);
if ch == b':' {
Some(InterfaceNameComponent::Namespace(name))
} else {
Some(InterfaceNameComponent::Projection(name))
}
}
None => {
let name = next;
next = "";
if prev_char == Some(b'@') {
Some(InterfaceNameComponent::VersionPrefix(name))
} else {
let name = KebabStr::new_unchecked(name);
Some(InterfaceNameComponent::Projection(name))
}
}
}
})
}
}
impl Ord for InterfaceName<'_> {
fn cmp(&self, other: &Self) -> Ordering {
self.components().cmp(other.components())
}
}
impl PartialOrd for InterfaceName<'_> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for InterfaceName<'_> {
fn eq(&self, other: &Self) -> bool {
self.components().eq(other.components())
}
}
impl Eq for InterfaceName<'_> {}
impl Hash for InterfaceName<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.components().count().hash(state);
for component in self.components() {
component.hash(state);
}
}
}
pub fn split_canonical_version(version: &str) -> Option<(&str, Option<&str>)> {
let is_identifier =
|s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-');
let (without_build, build) = match version.split_once('+') {
Some((a, b)) => (a, Some(b)),
None => (version, None),
};
if build.is_some_and(|b| !b.split('.').all(is_identifier)) {
return None;
}
let (core, pre) = match without_build.split_once('-') {
Some((a, b)) => (a, Some(b)),
None => (without_build, None),
};
if pre.is_some_and(|p| !p.split('.').all(is_identifier)) {
return None;
}
let mut numbers = core.split('.');
let (Some(major), Some(minor), Some(patch), None) = (
numbers.next(),
numbers.next(),
numbers.next(),
numbers.next(),
) else {
return None;
};
if ![major, minor, patch].into_iter().all(is_version_number) {
return None;
}
let split = if pre.is_none() && major != "0" {
major.len()
} else if pre.is_none() && minor != "0" {
major.len() + 1 + minor.len()
} else {
without_build.len()
};
match version.split_at(split) {
(canonical, "") => Some((canonical, None)),
(canonical, suffix) => Some((canonical, Some(suffix))),
}
}
pub fn is_canonical_version(version: &str) -> bool {
match version.split_once('.') {
None => version != "0" && is_version_number(version),
Some(("0", minor)) if minor != "0" && is_version_number(minor) => true,
_ => split_canonical_version(version) == Some((version, None)),
}
}
fn is_version_number(s: &str) -> bool {
!s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()) && (s == "0" || !s.starts_with('0'))
}
struct ComponentNameParser<'a> {
next: &'a str,
offset: u64,
features: WasmFeatures,
}
impl<'a> ComponentNameParser<'a> {
fn parse(&mut self) -> Result<ParsedComponentNameKind> {
if self.next.contains(':') {
self.interface_name()?;
return Ok(ParsedComponentNameKind::Interface);
}
let resource_func_kind;
if self.eat_str(CONSTRUCTOR) {
resource_func_kind = Some(ResourceFuncKind::Constructor);
} else if self.eat_str(METHOD) {
resource_func_kind = Some(ResourceFuncKind::Method);
} else if self.eat_str(STATIC) {
resource_func_kind = Some(ResourceFuncKind::Static);
} else {
resource_func_kind = None;
}
let accessor_kind;
if self.eat_str(GET) {
accessor_kind = Some(AccessorKind::Get);
} else if self.eat_str(SET) {
accessor_kind = Some(AccessorKind::Set);
} else {
accessor_kind = None;
}
if accessor_kind.is_some() {
require_feature::cm_accessors(
self.features,
"`[get]` and `[set]` names require the component model accessors feature",
self.offset,
)?;
}
match resource_func_kind {
Some(ResourceFuncKind::Constructor) => {
if accessor_kind.is_some() {
bail!(
self.offset,
"names with [constructor] cannot contain [get] or [set]"
);
}
}
Some(ResourceFuncKind::Method) | Some(ResourceFuncKind::Static) => {
let resource = self.take_until('.')?;
self.kebab(resource)?;
}
None => {}
}
self.expect_kebab()?;
Ok(ParsedComponentNameKind::Plain)
}
fn interface_name(&mut self) -> Result<()> {
self.pkg_path()?;
if self.eat_str("@") {
self.take_rest();
}
Ok(())
}
fn pkg_path(&mut self) -> Result<()> {
self.take_lowercase_kebab()?;
self.expect_str(":")?;
self.take_lowercase_kebab()?;
if self.features.cm_nested_names() {
while self.next.starts_with(':') {
self.expect_str(":")?;
self.take_lowercase_kebab()?;
}
}
if self.next.starts_with('/') {
self.expect_str("/")?;
self.take_kebab()?;
if self.features.cm_nested_names() {
while self.next.starts_with('/') {
self.expect_str("/")?;
self.take_kebab()?;
}
}
} else {
bail!(self.offset, "expected `/` after package name");
}
Ok(())
}
fn eat_str(&mut self, prefix: &str) -> bool {
match self.next.strip_prefix(prefix) {
Some(rest) => {
self.next = rest;
true
}
None => false,
}
}
fn expect_str(&mut self, prefix: &str) -> Result<()> {
if self.eat_str(prefix) {
Ok(())
} else {
bail!(self.offset, "expected `{prefix}` at `{}`", self.next);
}
}
fn eat_until(&mut self, c: char) -> Option<&'a str> {
let ret = self.eat_up_to(c);
if ret.is_some() {
self.next = &self.next[c.len_utf8()..];
}
ret
}
fn eat_up_to(&mut self, c: char) -> Option<&'a str> {
let i = self.next.find(c)?;
let (a, b) = self.next.split_at(i);
self.next = b;
Some(a)
}
fn kebab(&self, s: &'a str) -> Result<KebabStr<'a>> {
match KebabStr::new(s) {
Some(name) => Ok(name),
None => bail!(self.offset, "`{s}` is not in kebab case"),
}
}
fn take_until(&mut self, c: char) -> Result<&'a str> {
match self.eat_until(c) {
Some(s) => Ok(s),
None => bail!(self.offset, "failed to find `{c}` character"),
}
}
fn take_rest(&mut self) -> &'a str {
let ret = self.next;
self.next = "";
ret
}
fn take_kebab(&mut self) -> Result<KebabStr<'a>> {
self.next
.find(|c| !matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-'))
.map(|i| {
let (kebab, next) = self.next.split_at(i);
self.next = next;
self.kebab(kebab)
})
.unwrap_or_else(|| self.expect_kebab())
}
fn take_lowercase_kebab(&mut self) -> Result<KebabStr<'a>> {
let kebab = self.take_kebab()?;
if let Some(c) = kebab
.chars()
.find(|c| c.is_alphabetic() && !c.is_lowercase())
{
bail!(
self.offset,
"character `{c}` is not lowercase in package name/namespace"
);
}
Ok(kebab)
}
fn expect_kebab(&mut self) -> Result<KebabStr<'a>> {
let s = self.take_rest();
self.kebab(s)
}
}
#[cfg(test)]
mod tests {
use alloc::borrow::ToOwned;
use super::*;
use std::collections::HashSet;
fn parse_kebab_name(s: &str) -> Option<ComponentName> {
let features = WasmFeatures::default() | WasmFeatures::CM_ACCESSORS;
ComponentName::new_with_features(s, 0, features).ok()
}
#[test]
fn accessors_gated() {
fn parse_default(s: &str) -> Option<ComponentName> {
ComponentName::new(s, 0).ok()
}
assert!(parse_default("a").is_some());
assert!(parse_default("[get]a").is_none());
assert!(parse_default("[set]a").is_none());
assert!(parse_default("[method][get]a.b").is_none());
assert!(parse_default("[method][set]a.b").is_none());
assert!(parse_default("[static][get]a.b").is_none());
assert!(parse_default("[static][set]a.b").is_none());
}
#[test]
fn kebab_smoke() {
assert!(KebabStr::new("").is_none());
assert!(KebabStr::new("a").is_some());
assert!(KebabStr::new("aB").is_none());
assert!(KebabStr::new("a-B").is_some());
assert!(KebabStr::new("a-").is_none());
assert!(KebabStr::new("-").is_none());
assert!(KebabStr::new("¶").is_none());
assert!(KebabStr::new("0").is_none());
assert!(KebabStr::new("a0").is_some());
assert!(KebabStr::new("a-0").is_some());
assert!(KebabStr::new("0-a").is_none());
assert!(KebabStr::new("a-b--c").is_none());
assert!(KebabStr::new("a0-000-3d4a-54FF").is_some());
assert!(KebabStr::new("a0-000-3d4A-54Ff").is_none());
}
#[test]
fn name_smoke() {
assert!(parse_kebab_name("a").is_some());
assert!(parse_kebab_name("[get]a").is_some());
assert!(parse_kebab_name("[set]a").is_some());
assert!(parse_kebab_name("[foo]a").is_none());
assert!(parse_kebab_name("[constructor]a").is_some());
assert!(parse_kebab_name("[constructor][get]a").is_none());
assert!(parse_kebab_name("[constructor][set]a").is_none());
assert!(parse_kebab_name("[method]a").is_none());
assert!(parse_kebab_name("[method]a.b").is_some());
assert!(parse_kebab_name("[method]a-0.b-1").is_some());
assert!(parse_kebab_name("[method]a.b.c").is_none());
assert!(parse_kebab_name("[method][get]a.b").is_some());
assert!(parse_kebab_name("[method][set]a.b").is_some());
assert!(parse_kebab_name("[method][foo]a.b").is_none());
assert!(parse_kebab_name("[static]a").is_none());
assert!(parse_kebab_name("[static]a.b").is_some());
assert!(parse_kebab_name("[static][get]a.b").is_some());
assert!(parse_kebab_name("[static][set]a.b").is_some());
assert!(parse_kebab_name("[static][foo]a.b").is_none());
}
#[test]
fn name_equality() {
assert_eq!(parse_kebab_name("a"), parse_kebab_name("a"));
assert_ne!(parse_kebab_name("a"), parse_kebab_name("b"));
assert_eq!(
parse_kebab_name("[constructor]a"),
parse_kebab_name("[constructor]a")
);
assert_ne!(
parse_kebab_name("[constructor]a"),
parse_kebab_name("[constructor]b")
);
assert_eq!(
parse_kebab_name("[method]a.b"),
parse_kebab_name("[method]a.b")
);
assert_ne!(
parse_kebab_name("[method]a.b"),
parse_kebab_name("[method]b.b")
);
assert_eq!(
parse_kebab_name("[static]a.b"),
parse_kebab_name("[static]a.b")
);
assert_ne!(
parse_kebab_name("[static]a.b"),
parse_kebab_name("[static]b.b")
);
assert_eq!(
parse_kebab_name("[static]a.b"),
parse_kebab_name("[method]a.b")
);
assert_eq!(
parse_kebab_name("[method]a.b"),
parse_kebab_name("[static]a.b")
);
assert_ne!(
parse_kebab_name("[method]b.b"),
parse_kebab_name("[static]a.b")
);
{
let mut s = HashSet::new();
assert!(s.insert(parse_kebab_name("a")));
assert!(s.insert(parse_kebab_name("[constructor]a")));
assert!(s.insert(parse_kebab_name("[method]a.b")));
assert!(!s.insert(parse_kebab_name("[static]a.b")));
assert!(s.insert(parse_kebab_name("[static]b.b")));
}
{
let mut s: HashSet<ComponentName> = HashSet::new();
assert!(s.insert(parse_kebab_name("foo").unwrap()));
assert!(s.insert(parse_kebab_name("foo-bar").unwrap()));
assert!(s.insert(parse_kebab_name("[constructor]foo").unwrap()));
assert!(s.insert(parse_kebab_name("[method]foo.bar").unwrap()));
assert!(s.insert(parse_kebab_name("[static]foo.baz").unwrap()));
assert!(s.insert(parse_kebab_name("foo:bar/baz").unwrap()));
assert!(s.insert(parse_kebab_name("[get]prop").unwrap()));
assert!(s.insert(parse_kebab_name("[set]prop").unwrap()));
assert!(s.insert(parse_kebab_name("[method][get]foo.prop").unwrap()));
assert!(s.insert(parse_kebab_name("[method][set]foo.prop").unwrap()));
assert!(s.insert(parse_kebab_name("[static][get]foo.prop-2").unwrap()));
assert!(s.insert(parse_kebab_name("[static][set]foo.prop-2").unwrap()));
assert!(s.insert(parse_kebab_name("[method]foo.get-prop").unwrap()));
assert!(s.insert(parse_kebab_name("[method]foo.set-prop").unwrap()));
assert!(!s.insert(parse_kebab_name("foo").unwrap()));
assert!(!s.insert(parse_kebab_name("FOO").unwrap()));
assert!(!s.insert(parse_kebab_name("[method]foo.foo").unwrap()));
assert!(!s.insert(parse_kebab_name("[get]foo").unwrap()));
assert!(!s.insert(parse_kebab_name("[method][get]foo.foo").unwrap()));
assert!(!s.insert(parse_kebab_name("[static][set]foo.FOO").unwrap()));
assert!(!s.insert(parse_kebab_name("foo-BAR").unwrap()));
assert!(!s.insert(parse_kebab_name("foobar").unwrap()));
assert!(!s.insert(parse_kebab_name("foob-ar").unwrap()));
assert!(!s.insert(parse_kebab_name("[static]foo-BAR.FOO-bar").unwrap()));
assert!(!s.insert(parse_kebab_name("[constructor]FOO").unwrap()));
assert!(!s.insert(parse_kebab_name("[method]foo.BAR").unwrap()));
assert!(!s.insert(parse_kebab_name("[static]foo.bar").unwrap()));
assert!(!s.insert(parse_kebab_name("[method]foo.baz").unwrap()));
assert!(!s.insert(parse_kebab_name("foo:bar/BAZ").unwrap()));
assert!(!s.insert(parse_kebab_name("prop").unwrap()));
assert!(!s.insert(parse_kebab_name("[set]PROP").unwrap()));
assert!(!s.insert(parse_kebab_name("[method]foo.prop").unwrap()));
assert!(!s.insert(parse_kebab_name("[static]foo.PROP").unwrap()));
assert!(!s.insert(parse_kebab_name("[method][get]foo.PROP").unwrap()));
assert!(!s.insert(parse_kebab_name("[static][get]foo.prop").unwrap()));
assert!(!s.insert(parse_kebab_name("[method][set]foo.PROP").unwrap()));
assert!(!s.insert(parse_kebab_name("[static][set]foo.prop").unwrap()));
}
}
#[test]
fn getter_for_setter() {
fn getter(s: &str) -> String {
match parse_kebab_name(s).unwrap().kind() {
ComponentNameKind::Plain(p) if p.accessor == Some(AccessorKind::Set) => {
p.getter_for_setter()
}
_ => "INVALID".to_owned(),
}
}
assert_eq!(getter("foo"), "INVALID");
assert_eq!(getter("[get]foo"), "INVALID");
assert_eq!(getter("[constructor]foo"), "INVALID");
assert_eq!(getter("[method]foo.bar"), "INVALID");
assert_eq!(getter("[method][get]foo.bar"), "INVALID");
assert_eq!(getter("[static][get]foo.bar"), "INVALID");
assert_eq!(getter("foo:bar/baz"), "INVALID");
assert_eq!(getter("[set]foo"), "[get]foo");
assert_eq!(getter("[set]foo-BAR"), "[get]foo-BAR");
assert_eq!(getter("[method][set]foo.bar"), "[method][get]foo.bar");
assert_eq!(
getter("[static][set]foo-a.bar-B"),
"[static][get]foo-a.bar-B"
);
}
#[test]
fn test_split_canonical_version() {
let split = split_canonical_version;
assert_eq!(split("1.2.3"), Some(("1", Some(".2.3"))));
assert_eq!(split("1.0.0"), Some(("1", Some(".0.0"))));
assert_eq!(split("101.201.301"), Some(("101", Some(".201.301"))));
assert_eq!(split("1.2.3+abc"), Some(("1", Some(".2.3+abc"))));
assert_eq!(split("0.2.1"), Some(("0.2", Some(".1"))));
assert_eq!(split("0.10.0+b.1"), Some(("0.10", Some(".0+b.1"))));
assert_eq!(split("0.0.1"), Some(("0.0.1", None)));
assert_eq!(split("0.0.0"), Some(("0.0.0", None)));
assert_eq!(split("0.0.1+b"), Some(("0.0.1", Some("+b"))));
assert_eq!(split("1.0.0-rc1"), Some(("1.0.0-rc1", None)));
assert_eq!(split("0.2.6-rc.1"), Some(("0.2.6-rc.1", None)));
assert_eq!(split("1.0.0-rc1+b"), Some(("1.0.0-rc1", Some("+b"))));
assert_eq!(split("1.0.0-a-b+c-d"), Some(("1.0.0-a-b", Some("+c-d"))));
assert_eq!(split("0.1.0-a+b-c+d"), None);
}
}