use std::{
cmp::Ordering,
fmt::{self, Display},
hash::Hash,
marker,
marker::PhantomData,
};
use gazebo::{
any::AnyLifetime,
coerce::{coerce_ref, Coerce},
};
use serde::{ser::SerializeMap, Serialize};
use crate::{
self as starlark,
collections::{SmallMap, StarlarkHasher},
environment::{Methods, MethodsStatic},
values::{
comparison::{compare_small_map, equals_small_map},
display::display_keyed_container,
docs,
docs::DocItem,
error::ValueError,
layout::typed::string::StringValueLike,
AllocValue, Freeze, FrozenValue, Heap, StarlarkValue, StringValue, Trace, UnpackValue,
Value, ValueLike, ValueOf,
},
};
impl<'v, V: ValueLike<'v>> StructGen<'v, V> {
pub const TYPE: &'static str = "struct";
pub fn new(fields: SmallMap<V::String, V>) -> Self {
Self {
fields,
_marker: marker::PhantomData,
}
}
}
starlark_complex_value!(pub Struct<'v>);
#[derive(Clone, Default, Debug, Trace, Freeze, AnyLifetime)]
#[repr(C)]
pub struct StructGen<'v, V: ValueLike<'v>> {
pub fields: SmallMap<V::String, V>,
_marker: marker::PhantomData<&'v String>,
}
unsafe impl<'v> Coerce<StructGen<'v, Value<'v>>> for StructGen<'static, FrozenValue> {}
impl<'v, V: ValueLike<'v>> Display for StructGen<'v, V> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
display_keyed_container(
f,
"struct(",
")",
"=",
self.fields
.iter()
.map(|(name, value)| (name.to_string_value().as_str(), value)),
)
}
}
pub struct StructBuilder<'v>(&'v Heap, SmallMap<StringValue<'v>, Value<'v>>);
impl<'v> StructBuilder<'v> {
pub fn with_capacity(heap: &'v Heap, capacity: usize) -> Self {
Self(heap, SmallMap::with_capacity(capacity))
}
pub fn new(heap: &'v Heap) -> Self {
Self(heap, SmallMap::new())
}
pub fn add(&mut self, key: &str, val: impl AllocValue<'v>) {
self.1.insert(self.0.alloc_str(key), self.0.alloc(val));
}
pub fn build(self) -> Struct<'v> {
Struct {
fields: self.1,
_marker: marker::PhantomData,
}
}
}
impl<'v, V: ValueLike<'v>> StarlarkValue<'v> for StructGen<'v, V>
where
Self: AnyLifetime<'v>,
{
starlark_type!(Struct::TYPE);
fn get_methods(&self) -> Option<&'static Methods> {
static RES: MethodsStatic = MethodsStatic::new();
RES.methods(crate::stdlib::structs::struct_methods)
}
fn extra_memory(&self) -> usize {
self.fields.extra_memory()
}
fn collect_repr_cycle(&self, collector: &mut String) {
collector.push_str("struct(...)");
}
fn equals(&self, other: Value<'v>) -> anyhow::Result<bool> {
match Struct::from_value(other) {
None => Ok(false),
Some(other) => {
equals_small_map(coerce_ref(&self.fields), &other.fields, |x, y| x.equals(*y))
}
}
}
fn compare(&self, other: Value<'v>) -> anyhow::Result<Ordering> {
match Struct::from_value(other) {
None => ValueError::unsupported_with(self, "cmp()", other),
Some(other) => compare_small_map(
coerce_ref(&self.fields),
&other.fields,
|k| k.to_string_value().as_str(),
|x, y| x.compare(*y),
),
}
}
fn get_attr(&self, attribute: &str, _heap: &'v Heap) -> Option<Value<'v>> {
coerce_ref(&self.fields).get(attribute).copied()
}
fn write_hash(&self, hasher: &mut StarlarkHasher) -> anyhow::Result<()> {
for (k, v) in self.fields.iter_hashed() {
Hash::hash(&k, hasher);
v.write_hash(hasher)?;
}
Ok(())
}
fn has_attr(&self, attribute: &str) -> bool {
coerce_ref(&self.fields).contains_key(attribute)
}
fn dir_attr(&self) -> Vec<String> {
self.fields
.keys()
.map(|x| x.to_string_value().as_str().to_owned())
.collect()
}
fn documentation(&self) -> Option<DocItem> {
let members = self
.fields
.iter()
.map(|(k, v)| {
let name = k.to_string_value().as_str().to_owned();
match v.to_value().documentation() {
Some(DocItem::Function(f)) => (name, docs::Member::Function(f)),
_ => (
name,
docs::Member::Property(docs::Property {
docs: None,
typ: None,
}),
),
}
})
.collect();
Some(DocItem::Object(docs::Object {
docs: None,
members,
}))
}
}
impl<'v, V: ValueLike<'v>> Serialize for StructGen<'v, V>
where
Self: AnyLifetime<'v>,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map_serialize = serializer.serialize_map(Some(self.fields.len()))?;
for (k, v) in self.fields.iter() {
map_serialize.serialize_entry(k.to_string_value().as_str(), &v.to_value())?;
}
map_serialize.end()
}
}
pub struct StructOf<'v, V: UnpackValue<'v>> {
value: ValueOf<'v, &'v Struct<'v>>,
_marker: PhantomData<V>,
}
impl<'v, V: UnpackValue<'v>> UnpackValue<'v> for StructOf<'v, V> {
fn expected() -> String {
format!("struct with fields of type {}", V::expected())
}
fn unpack_value(value: Value<'v>) -> Option<StructOf<'v, V>> {
let value = ValueOf::<&Struct>::unpack_value(value)?;
for (_k, &v) in &value.typed.fields {
V::unpack_value(v)?;
}
Some(StructOf {
value,
_marker: marker::PhantomData,
})
}
}
impl<'v, V: UnpackValue<'v>> StructOf<'v, V> {
pub fn to_value(&self) -> Value<'v> {
self.value.value
}
pub fn as_struct(&self) -> &Struct<'v> {
self.value.typed
}
pub fn to_map(&self) -> SmallMap<StringValue<'v>, V> {
self.as_struct()
.fields
.iter()
.map(|(&k, &v)| (k, V::unpack_value(v).expect("validated at construction")))
.collect()
}
}
#[cfg(test)]
mod tests {
use crate::{
assert,
values::{
docs,
docs::{DocItem, DocString, DocStringKind},
},
};
#[test]
fn test_repr() {
assert::eq("repr(struct(a=1, b=[]))", "'struct(a=1, b=[])'");
assert::eq("str(struct(a=1, b=[]))", "'struct(a=1, b=[])'");
}
#[test]
fn test_repr_cycle() {
assert::eq(
"l = []; s = struct(f=l); l.append(s); repr(s)",
"'struct(f=[struct(...)])'",
);
assert::eq(
"l = []; s = struct(f=l); l.append(s); str(s)",
"'struct(f=[struct(...)])'",
);
}
#[test]
fn test_to_json_cycle() {
assert::fail(
"l = []; s = struct(f=l); l.append(s); s.to_json()",
"Cycle detected when serializing value of type `struct` to JSON",
);
}
#[test]
fn test_to_json() {
assert::all_true(
r#"
struct(key = None).to_json() == '{"key":null}'
struct(key = True).to_json() == '{"key":true}'
struct(key = False).to_json() == '{"key":false}'
struct(key = 42).to_json() == '{"key":42}'
struct(key = 'value').to_json() == '{"key":"value"}'
struct(key = 'value"').to_json() == '{"key":"value\\\""}'
struct(key = 'value\\').to_json() == '{"key":"value\\\\"}'
struct(key = 'value/').to_json() == '{"key":"value/"}'
struct(key = 'value\u0008').to_json() == '{"key":"value\\b"}'
struct(key = 'value\u000C').to_json() == '{"key":"value\\f"}'
struct(key = 'value\n').to_json() == '{"key":"value\\n"}'
struct(key = 'value\r').to_json() == '{"key":"value\\r"}'
struct(key = 'value\t').to_json() == '{"key":"value\\t"}'
struct(foo = 42, bar = "some").to_json() == '{"foo":42,"bar":"some"}'
struct(foo = struct(bar = "some")).to_json() == '{"foo":{"bar":"some"}}'
struct(foo = ["bar/", "some"]).to_json() == '{"foo":["bar/","some"]}'
struct(foo = [struct(bar = "some")]).to_json() == '{"foo":[{"bar":"some"}]}'
"#,
);
}
#[test]
fn test_docs() {
let expected = DocItem::Object(docs::Object {
docs: None,
members: vec![
(
"member".to_owned(),
docs::Member::Property(docs::Property {
docs: None,
typ: None,
}),
),
(
"some_func".to_owned(),
docs::Member::Function(docs::Function {
docs: DocString::from_docstring(DocStringKind::Starlark, "some_func docs"),
params: vec![docs::Param::Arg {
name: "v".to_owned(),
docs: None,
typ: Some(docs::Type {
raw_type: "\"\"".to_owned(),
}),
default_value: None,
}],
ret: docs::Return {
docs: None,
typ: Some(docs::Type {
raw_type: "\"\"".to_owned(),
}),
},
}),
),
],
});
let s = assert::pass(
r#"
def some_func(v: "") -> "":
""" some_func docs """
return v
struct(
member = "some string",
some_func = some_func,
)"#,
);
let docs = s.value().documentation().expect("some docs");
assert_eq!(expected, docs);
}
}