Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions crates/lune-roblox/src/datatypes/conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ impl<'lua> LuaToDomValue<'lua> for LuaValue<'lua> {
(LuaValue::String(s), DomType::BinaryString) => {
Ok(DomValue::BinaryString(s.as_ref().into()))
}
(LuaValue::String(s), DomType::Content) => {
Ok(DomValue::Content(s.to_str()?.to_string().into()))
(LuaValue::String(s), DomType::ContentId) => {
Ok(DomValue::ContentId(s.to_str()?.to_string().into()))
}

// NOTE: Some values are either optional or default and we
Expand Down Expand Up @@ -200,6 +200,7 @@ impl<'lua> DomValueToLua<'lua> for LuaAnyUserData<'lua> {
DomValue::Color3(value) => dom_to_userdata!(lua, value => Color3),
DomValue::Color3uint8(value) => dom_to_userdata!(lua, value => Color3),
DomValue::ColorSequence(value) => dom_to_userdata!(lua, value => ColorSequence),
DomValue::Content(value) => dom_to_userdata!(lua, value => Content),
DomValue::Faces(value) => dom_to_userdata!(lua, value => Faces),
DomValue::Font(value) => dom_to_userdata!(lua, value => Font),
DomValue::NumberRange(value) => dom_to_userdata!(lua, value => NumberRange),
Expand Down Expand Up @@ -256,6 +257,7 @@ impl<'lua> LuaToDomValue<'lua> for LuaAnyUserData<'lua> {
DomType::Color3 => userdata_to_dom!(self as Color3 => dom::Color3),
DomType::Color3uint8 => userdata_to_dom!(self as Color3 => dom::Color3uint8),
DomType::ColorSequence => userdata_to_dom!(self as ColorSequence => dom::ColorSequence),
DomType::Content => userdata_to_dom!(self as Content => dom::Content),
DomType::Enum => userdata_to_dom!(self as EnumItem => dom::Enum),
DomType::Faces => userdata_to_dom!(self as Faces => dom::Faces),
DomType::Font => userdata_to_dom!(self as Font => dom::Font),
Expand Down
1 change: 1 addition & 0 deletions crates/lune-roblox/src/datatypes/extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ impl DomValueExt for DomType {
Color3uint8 => "Color3uint8",
ColorSequence => "ColorSequence",
Content => "Content",
ContentId => "ContentId",
Enum => "Enum",
Faces => "Faces",
Float32 => "Float32",
Expand Down
120 changes: 120 additions & 0 deletions crates/lune-roblox/src/datatypes/types/content.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use core::fmt;

use mlua::prelude::*;
use rbx_dom_weak::types::{Content as DomContent, ContentType};

use lune_utils::TableBuilder;

use crate::{exports::LuaExportsTable, instance::Instance};

use super::{super::*, EnumItem};

/**
An implementation of the [Content](https://create.roblox.com/docs/reference/engine/datatypes/Content) Roblox datatype.

This implements all documented properties, methods & constructors of the Content type as of April 2025.
*/
#[derive(Debug, Clone, PartialEq)]
pub struct Content(ContentType);

impl LuaExportsTable<'_> for Content {
const EXPORT_NAME: &'static str = "Content";

fn create_exports_table(lua: &'_ Lua) -> LuaResult<LuaTable<'_>> {
let from_uri = |_, uri: String| Ok(Self(ContentType::Uri(uri)));

let from_object = |_, obj: LuaUserDataRef<Instance>| {
let database = rbx_reflection_database::get();
let instance_descriptor = database
.classes
.get("Instance")
.expect("the reflection database should always have Instance in it");
let param_descriptor = database.classes.get(obj.get_class_name()).expect(
"you should not be able to construct an Instance that is not known to Lune",
);
if database.has_superclass(param_descriptor, instance_descriptor) {
Err(LuaError::runtime("the provided object is a descendant class of 'Instance', expected one that was only an 'Object'"))
} else {
Ok(Content(ContentType::Object(obj.dom_ref)))
}
};

TableBuilder::new(lua)?
.with_value("none", Content(ContentType::None))?
.with_function("fromUri", from_uri)?
.with_function("fromObject", from_object)?
.build_readonly()
}
}

impl LuaUserData for Content {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("SourceType", |_, this| {
let variant_name = match &this.0 {
ContentType::None => "None",
ContentType::Uri(_) => "Uri",
ContentType::Object(_) => "Object",
other => {
return Err(LuaError::runtime(format!(
"cannot get SourceType: unknown ContentType variant '{other:?}'"
)))
}
};
Ok(EnumItem::from_enum_name_and_name(
"ContentSourceType",
variant_name,
))
});
fields.add_field_method_get("Uri", |_, this| {
if let ContentType::Uri(uri) = &this.0 {
Ok(Some(uri.to_owned()))
} else {
Ok(None)
}
});
fields.add_field_method_get("Object", |_, this| {
if let ContentType::Object(referent) = &this.0 {
Ok(Instance::new_opt(*referent))
} else {
Ok(None)
}
});
}

fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}
}

impl fmt::Display for Content {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Regardless of the actual content of the Content, Roblox just emits
// `Content` when casting it to a string. We do not do that.
write!(f, "Content(")?;
match &self.0 {
ContentType::None => write!(f, "None")?,
ContentType::Uri(uri) => write!(f, "Uri={uri}")?,
ContentType::Object(_) => write!(f, "Object")?,
other => write!(f, "UnknownType({other:?})")?,
}
write!(f, ")")
}
}

impl From<DomContent> for Content {
fn from(value: DomContent) -> Self {
Self(value.value().clone())
}
}

impl From<Content> for DomContent {
fn from(value: Content) -> Self {
match value.0 {
ContentType::None => Self::none(),
ContentType::Uri(uri) => Self::from_uri(uri),
ContentType::Object(referent) => Self::from_referent(referent),
other => unimplemented!("unknown variant of ContentType: {other:?}"),
}
}
}
2 changes: 2 additions & 0 deletions crates/lune-roblox/src/datatypes/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod cframe;
mod color3;
mod color_sequence;
mod color_sequence_keypoint;
mod content;
mod r#enum;
mod r#enum_item;
mod r#enums;
Expand All @@ -30,6 +31,7 @@ pub use cframe::CFrame;
pub use color3::Color3;
pub use color_sequence::ColorSequence;
pub use color_sequence_keypoint::ColorSequenceKeypoint;
pub use content::Content;
pub use faces::Faces;
pub use font::Font;
pub use number_range::NumberRange;
Expand Down
1 change: 1 addition & 0 deletions crates/lune-roblox/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ fn create_all_exports(lua: &Lua) -> LuaResult<Vec<(&'static str, LuaValue)>> {
export::<Color3>(lua)?,
export::<ColorSequence>(lua)?,
export::<ColorSequenceKeypoint>(lua)?,
export::<Content>(lua)?,
export::<Faces>(lua)?,
export::<Font>(lua)?,
export::<NumberRange>(lua)?,
Expand Down
1 change: 1 addition & 0 deletions crates/lune/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ create_tests! {
roblox_datatype_color3: "roblox/datatypes/Color3",
roblox_datatype_color_sequence: "roblox/datatypes/ColorSequence",
roblox_datatype_color_sequence_keypoint: "roblox/datatypes/ColorSequenceKeypoint",
roblox_datatype_content: "roblox/datatypes/Content",
roblox_datatype_enum: "roblox/datatypes/Enum",
roblox_datatype_faces: "roblox/datatypes/Faces",
roblox_datatype_font: "roblox/datatypes/Font",
Expand Down
62 changes: 62 additions & 0 deletions tests/roblox/datatypes/Content.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
local roblox = require("@lune/roblox") :: any
local Content = roblox.Content
local Instance = roblox.Instance
local Enum = roblox.Enum

assert(Content.none, "Content.none did not exist")
assert(
Content.none.SourceType == Enum.ContentSourceType.None,
"Content.none's SourceType was wrong"
)
assert(Content.none.Uri == nil, "Content.none's Uri field was wrong")
assert(Content.none.Object == nil, "Content.none's Object field was wrong")

local uri = Content.fromUri("test uri")
assert(uri.SourceType == Enum.ContentSourceType.Uri, "URI Content's SourceType was wrong")
assert(uri.Uri == "test uri", "URI Content's Uri field was wrong")
assert(uri.Object == nil, "URI Content's Object field was wrong")

assert(not pcall(Content.fromUri), "Content.fromUri accepted no argument")
assert(not pcall(Content.fromUri, false), "Content.fromUri accepted a boolean argument")
assert(not pcall(Content.fromUri, Enum), "Content.fromUri accepted a UserData as an argument")
assert(
not pcall(Content.fromUri, buffer.create(0)),
"Content.fromUri accepted a buffer as an argument"
)

-- It feels weird that this is allowed because `EditableImage` is very much
-- not an Instance. But what can you do?
local target = Instance.new("EditableImage")
local object = Content.fromObject(target)
assert(object.SourceType == Enum.ContentSourceType.Object, "Object Content's SourceType was wrong")
assert(object.Uri == nil, "Object Content's Uri field was wrong")
assert(object.Object == target, "Object Content's Object field was wrong")

assert(not pcall(Content.fromObject), "Content.fromObject accepted no argument")
assert(not pcall(Content.fromObject, false), "Content.fromObject accepted a boolean argument")
assert(
not pcall(Content.fromObject, Enum),
"Content.fromObject accepted a non-Instance/Object UserData as an argument"
)
assert(
not pcall(Content.fromObject, buffer.create(0)),
"Content.fromObject accepted a buffer as an argument"
)

assert(
not pcall(Content.fromObject, Instance.new("Folder")),
"Content.fromObject accepted an Instance as an argument"
)

assert(
tostring(Content.none) == "Content(None)",
`expected tostring(Content.none) to be Content(None), it was actually {Content.none}`
)
assert(
tostring(uri) == "Content(Uri=test uri)",
`expected tostring(URI Content) to be Content(Uri=...), it was actually {uri}`
)
assert(
tostring(object) == "Content(Object)",
`expected tostring(Object Content) to be Content(Object), it was actually {object}`
)
Loading