diff --git a/examples/03-high-dpi/README.md b/examples/03-high-dpi/README.md new file mode 100644 index 0000000..af9605d --- /dev/null +++ b/examples/03-high-dpi/README.md @@ -0,0 +1,2 @@ +# High DPI Example +This is an example demonstrating how Tarmac detects high DPI variants of assets and automatically generates code to pick the correct variant. \ No newline at end of file diff --git a/examples/03-high-dpi/assets/hello.png b/examples/03-high-dpi/assets/hello.png new file mode 100644 index 0000000..ef1b80a Binary files /dev/null and b/examples/03-high-dpi/assets/hello.png differ diff --git a/examples/03-high-dpi/assets/hello@2x.png b/examples/03-high-dpi/assets/hello@2x.png new file mode 100644 index 0000000..d61d8b3 Binary files /dev/null and b/examples/03-high-dpi/assets/hello@2x.png differ diff --git a/examples/03-high-dpi/assets/hello@3x.png b/examples/03-high-dpi/assets/hello@3x.png new file mode 100644 index 0000000..162341b Binary files /dev/null and b/examples/03-high-dpi/assets/hello@3x.png differ diff --git a/examples/03-high-dpi/src/assets.lua b/examples/03-high-dpi/src/assets.lua new file mode 100644 index 0000000..e8339ad --- /dev/null +++ b/examples/03-high-dpi/src/assets.lua @@ -0,0 +1,12 @@ +-- This file was @generated by Tarmac. It is not intended for manual editing. +return { + hello = function(dpiScale) + if dpiScale >= 3 then + return "rbxassetid://4967220945" + elseif dpiScale >= 2 then + return "rbxassetid://4967220902" + else + return "rbxassetid://4967166732" + end + end, +} \ No newline at end of file diff --git a/examples/03-high-dpi/tarmac-manifest.toml b/examples/03-high-dpi/tarmac-manifest.toml new file mode 100644 index 0000000..df01f64 --- /dev/null +++ b/examples/03-high-dpi/tarmac-manifest.toml @@ -0,0 +1,14 @@ +[inputs."assets/hello.png"] +hash = "55ef7fd70ec96da73fa0f09bf530605961cce1a654f02684a1a0b0a6234b06cb" +id = 4967166732 +packable = false + +[inputs."assets/hello@2x.png"] +hash = "02845c418d2c3abe798d1babeecc2e124dddc4765a840cd03d171f32351be3af" +id = 4967220902 +packable = false + +[inputs."assets/hello@3x.png"] +hash = "1d075da844e76c9e3ec924c1a7fa8efaedbf35787038063664ef939470d7f57c" +id = 4967220945 +packable = false diff --git a/examples/03-high-dpi/tarmac.toml b/examples/03-high-dpi/tarmac.toml new file mode 100644 index 0000000..44398d3 --- /dev/null +++ b/examples/03-high-dpi/tarmac.toml @@ -0,0 +1,7 @@ +name = "03-high-dpi" + +[[inputs]] +glob = "assets/**/*.png" +codegen = true +base-path = "assets" +codegen-path = "src/assets.lua" \ No newline at end of file diff --git a/src/asset_name.rs b/src/asset_name.rs index 7ef307d..fc24a7a 100644 --- a/src/asset_name.rs +++ b/src/asset_name.rs @@ -35,12 +35,6 @@ impl AssetName { AssetName(displayed.into()) } - /// Spritesheets don't have any canonical name provided by Tarmac's inputs; - /// when we upload them, we want to give them a simple dummy name - pub fn spritesheet() -> Self { - AssetName("spritesheet.png".into()) - } - #[cfg(test)] pub(crate) fn new>(inner: S) -> Self { Self(inner.as_ref().into()) diff --git a/src/codegen.rs b/src/codegen.rs index 368dbf3..6082f30 100644 --- a/src/codegen.rs +++ b/src/codegen.rs @@ -13,7 +13,7 @@ use fs_err::File; use crate::{ data::ImageSlice, data::SyncInput, - lua_ast::{Expression, Statement, Table}, + lua_ast::{Block, Expression, Function, IfBlock, Statement, Table}, }; const CODEGEN_HEADER: &str = @@ -27,26 +27,41 @@ pub fn perform_codegen(output_path: Option<&Path>, inputs: &[&SyncInput]) -> io: } } +/// Tree used to track and group inputs hierarchically, before turning them into +/// Lua tables. +enum GroupedItem<'a> { + Folder { + children_by_name: BTreeMap>, + }, + InputGroup { + inputs_by_dpi_scale: BTreeMap, + }, +} + /// Perform codegen for a group of inputs who have `codegen_path` defined. /// /// We'll build up a Lua file containing nested tables that match the structure /// of the input's path with its base path stripped away. fn codegen_grouped(output_path: &Path, inputs: &[&SyncInput]) -> io::Result<()> { - /// Represents the tree of inputs as we're discovering them. - enum Item<'a> { - Folder(BTreeMap<&'a str, Item<'a>>), - Input(&'a SyncInput), - } - - let mut root_folder: BTreeMap<&str, Item<'_>> = BTreeMap::new(); + let mut root_folder: BTreeMap> = BTreeMap::new(); // First, collect all of the inputs and group them together into a tree // according to their relative paths. - for input in inputs { + for &input in inputs { + // Not all inputs will be marked for codegen. We can eliminate those + // right away. + if !input.config.codegen { + continue; + } + + // The extension portion of the path is not useful for code generation. + // By stripping it off, we generate the names that users expect. + let mut path_without_extension = input.path_without_dpi_scale.clone(); + path_without_extension.set_extension(""); + // If we can't construct a relative path, there isn't a sensible name // that we can use to refer to this input. - let relative_path = input - .path + let relative_path = path_without_extension .strip_prefix(&input.config.base_path) .expect("Input base path was not a base path for input"); @@ -57,7 +72,9 @@ fn codegen_grouped(output_path: &Path, inputs: &[&SyncInput]) -> io::Result<()> match component { path::Component::Prefix(_) | path::Component::RootDir - | path::Component::Normal(_) => segments.push(Path::new(component.as_os_str())), + | path::Component::Normal(_) => { + segments.push(component.as_os_str().to_str().unwrap()) + } path::Component::CurDir => {} path::Component::ParentDir => assert!(segments.pop().is_some()), } @@ -65,69 +82,86 @@ fn codegen_grouped(output_path: &Path, inputs: &[&SyncInput]) -> io::Result<()> // Navigate down the tree, creating any folder entries that don't exist // yet. - // - // This is basically an in-memory `mkdir -p` followed by `touch`. let mut current_dir = &mut root_folder; - for (i, segment) in segments.iter().enumerate() { + for (i, &segment) in segments.iter().enumerate() { if i == segments.len() - 1 { // We assume that the last segment of a path must be a file. - let name = segment.file_stem().unwrap().to_str().unwrap(); - current_dir.insert(name, Item::Input(input)); - } else { - let name = segment.to_str().unwrap(); - let next_entry = current_dir - .entry(name) - .or_insert_with(|| Item::Folder(BTreeMap::new())); - - match next_entry { - Item::Folder(next_dir) => { - current_dir = next_dir; - } - Item::Input(_) => { - log::error!( - "A path tried to traverse through a folder as if it were a file: {}", - input.path.display() - ); - log::error!("The path segment '{}' is a file because of previous inputs, not a file.", name); - break; + let input_group = match current_dir.get_mut(segment) { + Some(existing) => existing, + None => { + let input_group = GroupedItem::InputGroup { + inputs_by_dpi_scale: BTreeMap::new(), + }; + current_dir.insert(segment.to_owned(), input_group); + current_dir.get_mut(segment).unwrap() } + }; + + if let GroupedItem::InputGroup { + inputs_by_dpi_scale, + } = input_group + { + inputs_by_dpi_scale.insert(input.dpi_scale, input); + } else { + unreachable!(); + } + } else { + let next_entry = + current_dir + .entry(segment.to_owned()) + .or_insert_with(|| GroupedItem::Folder { + children_by_name: BTreeMap::new(), + }); + + if let GroupedItem::Folder { children_by_name } = next_entry { + current_dir = children_by_name; + } else { + unreachable!(); } } } } - fn build_item(item: &Item<'_>) -> Option { + fn build_item(item: &GroupedItem<'_>) -> Option { match item { - Item::Folder(children) => { - let entries = children + GroupedItem::Folder { children_by_name } => { + let entries = children_by_name .iter() - .filter_map(|(&name, child)| build_item(child).map(|item| (name.into(), item))) + .filter_map(|(name, child)| build_item(child).map(|item| (name.into(), item))) .collect(); Some(Expression::table(entries)) } - Item::Input(input) => { - if input.config.codegen { - if let Some(id) = input.id { - if let Some(slice) = input.slice { - let template = UrlAndSliceTemplate { id, slice }; - - return Some(template.to_lua()); - } else { - let template = AssetUrlTemplate { id }; - - return Some(template.to_lua()); - } + GroupedItem::InputGroup { + inputs_by_dpi_scale, + } => { + if inputs_by_dpi_scale.len() == 1 { + // If there is exactly one input in this group, we can + // generate code knowing that there are no high DPI variants + // to choose from. + + let input = inputs_by_dpi_scale.values().next().unwrap(); + + match (input.id, input.slice) { + (Some(id), Some(slice)) => Some(codegen_url_and_slice(id, slice)), + (Some(id), None) => Some(codegen_just_asset_url(id)), + _ => None, } + } else { + // In this case, we have the same asset in multiple + // different DPI scales. We can generate code to pick + // between them at runtime. + Some(codegen_with_high_dpi_options(inputs_by_dpi_scale)) } - - None } } } - let root_item = build_item(&Item::Folder(root_folder)).unwrap(); + let root_item = build_item(&GroupedItem::Folder { + children_by_name: root_folder, + }) + .unwrap(); let ast = Statement::Return(root_item); let mut file = File::create(output_path)?; @@ -141,71 +175,85 @@ fn codegen_grouped(output_path: &Path, inputs: &[&SyncInput]) -> io::Result<()> /// defined, and so generate individual files. fn codegen_individual(inputs: &[&SyncInput]) -> io::Result<()> { for input in inputs { - let maybe_expression = if input.config.codegen { - if let Some(id) = input.id { - if let Some(slice) = input.slice { - let template = UrlAndSliceTemplate { id, slice }; - - Some(template.to_lua()) - } else { - let template = AssetUrlTemplate { id }; - - Some(template.to_lua()) - } - } else { - None - } - } else { - None + let expression = match (input.id, input.slice) { + (Some(id), Some(slice)) => codegen_url_and_slice(id, slice), + (Some(id), None) => codegen_just_asset_url(id), + _ => continue, }; - if let Some(expression) = maybe_expression { - let ast = Statement::Return(expression); + let ast = Statement::Return(expression); - let path = input.path.with_extension("lua"); + let path = input.path.with_extension("lua"); - let mut file = File::create(path)?; - writeln!(file, "{}", CODEGEN_HEADER)?; - write!(file, "{}", ast)?; - } + let mut file = File::create(path)?; + writeln!(file, "{}", CODEGEN_HEADER)?; + write!(file, "{}", ast)?; } Ok(()) } -/// Codegen template for CodegenKind::AssetUrl -pub(crate) struct AssetUrlTemplate { - pub id: u64, +fn codegen_url_and_slice(id: u64, slice: ImageSlice) -> Expression { + let offset = slice.min(); + let size = slice.size(); + + let mut table = Table::new(); + table.add_entry("Image", format!("rbxassetid://{}", id)); + table.add_entry( + "ImageRectOffset", + Expression::Raw(format!("Vector2.new({}, {})", offset.0, offset.1)), + ); + + table.add_entry( + "ImageRectSize", + Expression::Raw(format!("Vector2.new({}, {})", size.0, size.1)), + ); + + Expression::Table(table) } -impl AssetUrlTemplate { - fn to_lua(&self) -> Expression { - Expression::String(format!("rbxassetid://{}", self.id)) - } +fn codegen_just_asset_url(id: u64) -> Expression { + Expression::String(format!("rbxassetid://{}", id)) } -pub(crate) struct UrlAndSliceTemplate { - pub id: u64, - pub slice: ImageSlice, +fn codegen_dpi_option(input: &SyncInput) -> (Expression, Block) { + let condition = Expression::Raw(format!("dpiScale >= {}", input.dpi_scale)); + + // FIXME: We should probably pull data out of SyncInput at the start of + // codegen so that we can handle invariants like this. + let id = input.id.unwrap(); + + let value = match input.slice { + Some(slice) => codegen_url_and_slice(id, slice), + None => codegen_just_asset_url(id), + }; + + let body = Statement::Return(value); + + (condition, body.into()) } -impl UrlAndSliceTemplate { - fn to_lua(&self) -> Expression { - let offset = self.slice.min(); - let size = self.slice.size(); +fn codegen_with_high_dpi_options(inputs: &BTreeMap) -> Expression { + let args = "dpiScale".to_owned(); + + let mut options_high_to_low = inputs.values().rev().peekable(); - let mut table = Table::new(); - table.add_entry("Image", format!("rbxassetid://{}", self.id)); - table.add_entry( - "ImageRectOffset", - Expression::Raw(format!("Vector2.new({}, {})", offset.0, offset.1)), - ); + let highest_dpi_option = options_high_to_low.next().unwrap(); + let (highest_cond, highest_body) = codegen_dpi_option(highest_dpi_option); - table.add_entry( - "ImageRectSize", - Expression::Raw(format!("Vector2.new({}, {})", size.0, size.1)), - ); + let mut if_block = IfBlock::new(highest_cond, highest_body); - Expression::Table(table) + while let Some(dpi_option) = options_high_to_low.next() { + let (cond, body) = codegen_dpi_option(dpi_option); + + if options_high_to_low.peek().is_some() { + if_block.else_if_blocks.push((cond, body)); + } else { + if_block.else_block = Some(body); + } } + + let statements = vec![Statement::If(if_block)]; + + Expression::Function(Function::new(args, statements)) } diff --git a/src/commands/sync.rs b/src/commands/sync.rs index 4b91068..7487a8e 100644 --- a/src/commands/sync.rs +++ b/src/commands/sync.rs @@ -15,7 +15,7 @@ use crate::{ auth_cookie::get_auth_cookie, codegen::perform_codegen, data::{Config, ConfigError, ImageSlice, InputManifest, Manifest, ManifestError, SyncInput}, - dpi_scale::dpi_scale_for_path, + dpi_scale, image::Image, options::{GlobalOptions, SyncOptions, SyncTarget}, roblox_web_api::{RobloxApiClient, RobloxApiError}, @@ -241,6 +241,8 @@ impl SyncSession { let name = AssetName::from_paths(config_path, &path); log::trace!("Found input {}", name); + let path_info = dpi_scale::extract_path_info(&path); + let contents = fs::read(&path)?; let hash = generate_asset_hash(&contents); @@ -256,6 +258,8 @@ impl SyncSession { SyncInput { name, path, + path_without_dpi_scale: path_info.path_without_dpi_scale, + dpi_scale: path_info.dpi_scale, config: input_config.clone(), contents, hash, @@ -291,7 +295,7 @@ impl SyncSession { let kind = InputKind { packable: input.config.packable, - dpi_scale: dpi_scale_for_path(&input.path), + dpi_scale: input.dpi_scale, }; let input_group = compatible_input_groups.entry(kind).or_insert_with(Vec::new); @@ -422,7 +426,7 @@ impl SyncSession { let hash = generate_asset_hash(&encoded_image); let upload_data = UploadInfo { - name: AssetName::spritesheet(), + name: "spritesheet".to_owned(), contents: encoded_image, hash: hash.clone(), }; @@ -448,7 +452,7 @@ impl SyncSession { let input = self.inputs.get_mut(input_name).unwrap(); let upload_data = UploadInfo { - name: input_name.clone(), + name: input.human_name(), contents: input.contents.clone(), hash: input.hash.clone(), }; diff --git a/src/data/sync.rs b/src/data/sync.rs index 470d3be..6832e4c 100644 --- a/src/data/sync.rs +++ b/src/data/sync.rs @@ -11,11 +11,19 @@ use crate::{ /// results of network I/O, and from the previous Tarmac manifest file. #[derive(Debug)] pub struct SyncInput { + /// A unique name for this asset in the project. pub name: AssetName, /// The path on disk to the file this input originated from. pub path: PathBuf, + /// The input's path with DPI scale information stripped away. This is used + /// to group inputs that are just DPI variations of eachother. + pub path_without_dpi_scale: PathBuf, + + /// The DPI scale of this input, if it makes sense for this input type. + pub dpi_scale: u32, + /// The configuration that applied to this input when it was discovered. pub config: InputConfig, @@ -38,4 +46,20 @@ impl SyncInput { pub fn is_unchanged_since_last_sync(&self, old_manifest: &InputManifest) -> bool { self.hash == old_manifest.hash && self.config.packable == old_manifest.packable } + + /// Creates a non-unique, human-friendly name to refer to this input. + pub fn human_name(&self) -> String { + let file_stem = self + .path_without_dpi_scale + .file_stem() + .unwrap() + .to_str() + .unwrap(); + + if self.path == self.path_without_dpi_scale { + file_stem.to_owned() + } else { + format!("{} ({}x)", file_stem, self.dpi_scale) + } + } } diff --git a/src/dpi_scale.rs b/src/dpi_scale.rs index eee45fb..d60de9c 100644 --- a/src/dpi_scale.rs +++ b/src/dpi_scale.rs @@ -1,97 +1,63 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; use regex::Regex; #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct DpiAwarePathInfo { - pub(crate) file_stem: String, + pub(crate) path_without_dpi_scale: PathBuf, pub(crate) dpi_scale: u32, } impl DpiAwarePathInfo { #[cfg(test)] - fn new(file_stem: &str, dpi_scale: u32) -> Self { + fn new(path_without_dpi_scale: &str, dpi_scale: u32) -> Self { + let path_without_dpi_scale = PathBuf::from(path_without_dpi_scale); + Self { - file_stem: file_stem.to_owned(), + path_without_dpi_scale, dpi_scale, } } } -/// Given a path, extracts its file stem and DPI scale. -/// -/// If a DPI scale is found as part of the file name, is it removed from the -/// file stem. -pub(crate) fn extract_path_info>(path: P) -> Option { +/// Given a path, extracts its intended DPI scale and constructs a path without +/// DPI scale information in it. This can be used to group together multiple +/// versions of the same image. +pub(crate) fn extract_path_info>(path: P) -> DpiAwarePathInfo { lazy_static::lazy_static! { - static ref DPI_PATTERN: Regex = Regex::new(r"^(.+?)@(\d+)x$").unwrap(); + static ref DPI_PATTERN: Regex = Regex::new(r"^(.+?)@(\d+)x(.+?)$").unwrap(); } let path = path.as_ref(); - let file_stem = match path.file_stem().unwrap().to_str() { + let file_name = match path.file_name().unwrap().to_str() { Some(name) => name, // If the filename isn't valid Unicode, this is an error. None => { - log::warn!("Path {} had invalid Unicode", path.display()); - return None; + panic!("Path {} had invalid Unicode", path.display()); } }; - match DPI_PATTERN.captures(file_stem) { + match DPI_PATTERN.captures(file_name) { Some(captures) => { let file_stem = captures.get(1).unwrap().as_str().to_owned(); let scale_str = captures.get(2).unwrap().as_str(); + let suffix = captures.get(3).unwrap().as_str(); let dpi_scale = scale_str.parse().unwrap(); - Some(DpiAwarePathInfo { - file_stem, + let file_name_without_dpi_scale = format!("{}{}", file_stem, suffix); + let path_without_dpi_scale = path.with_file_name(&file_name_without_dpi_scale); + + DpiAwarePathInfo { + path_without_dpi_scale, dpi_scale, - }) + } } - None => Some(DpiAwarePathInfo { - file_stem: file_stem.to_owned(), + None => DpiAwarePathInfo { + path_without_dpi_scale: path.to_owned(), dpi_scale: 1, - }), - } -} - -/// Given a path, checks if it's marked as being high DPI. -/// -/// Examples of the convention Tarmac uses: -/// -/// - foo.png (1x) -/// - foo@1x.png (1x) -/// - foo@2x.png (2x) -/// - foo@3x.png (3x) -pub(crate) fn dpi_scale_for_path>(path: P) -> u32 { - lazy_static::lazy_static! { - static ref DPI_PATTERN: Regex = Regex::new(r"@(\d+)x\..+?$").unwrap(); - } - - let path = path.as_ref(); - - let file_name = match path.file_name().unwrap().to_str() { - Some(name) => name, - - // If the filename isn't valid Unicode, we'll assume it's a 1x asset. - None => { - log::warn!( - "Path {} had invalid Unicode, considering it a 1x asset...", - path.display() - ); - - return 1; - } - }; - - match DPI_PATTERN.captures(file_name) { - Some(captures) => { - let scale_str = captures.get(1).unwrap().as_str(); - scale_str.parse().unwrap() - } - None => 1, + }, } } @@ -101,79 +67,65 @@ mod test { #[test] fn no_attached_scale() { - assert_eq!(dpi_scale_for_path("foo.png"), 1); - assert_eq!(dpi_scale_for_path("foo.blah.png"), 1); - assert_eq!(dpi_scale_for_path("foo/bar/baz/hello.png"), 1); - assert_eq!( extract_path_info("foo.png"), - Some(DpiAwarePathInfo::new("foo", 1)) + DpiAwarePathInfo::new("foo.png", 1) ); assert_eq!( extract_path_info("foo.blah.png"), - Some(DpiAwarePathInfo::new("foo.blah", 1)) + DpiAwarePathInfo::new("foo.blah.png", 1) ); assert_eq!( extract_path_info("foo/bar/baz/hello.png"), - Some(DpiAwarePathInfo::new("hello", 1)) + DpiAwarePathInfo::new("foo/bar/baz/hello.png", 1) ); } #[test] fn explicit_1x() { - assert_eq!(dpi_scale_for_path("layerify@1x.png"), 1); - assert_eq!(dpi_scale_for_path("layerify.blah@1x.png"), 1); - assert_eq!(dpi_scale_for_path("layerify@1x.png.bak"), 1); - assert_eq!(dpi_scale_for_path("some/path/to/image/nice@1x.png"), 1); - assert_eq!( extract_path_info("layerify@1x.png"), - Some(DpiAwarePathInfo::new("layerify", 1)) + DpiAwarePathInfo::new("layerify.png", 1) ); assert_eq!( extract_path_info("layerify.blah@1x.png"), - Some(DpiAwarePathInfo::new("layerify.blah", 1)) + DpiAwarePathInfo::new("layerify.blah.png", 1) ); assert_eq!( extract_path_info("layerify@1x.png.bak"), - Some(DpiAwarePathInfo::new("layerify@1x.png", 1)), + DpiAwarePathInfo::new("layerify.png.bak", 1) ); assert_eq!( extract_path_info("some/path/to/image/nice@1x.png"), - Some(DpiAwarePathInfo::new("nice", 1)) + DpiAwarePathInfo::new("some/path/to/image/nice.png", 1) ); } #[test] fn explicit_not_1x() { - assert_eq!(dpi_scale_for_path("cool-company@2x.png"), 2); - assert_eq!(dpi_scale_for_path("engineers@10x.png"), 10); - assert_eq!(dpi_scale_for_path("we.like.dots@3x.png"), 3); - assert_eq!(dpi_scale_for_path("backup-your-stuff@4x.png.bak"), 4); - assert_eq!( extract_path_info("cool-company@2x.png"), - Some(DpiAwarePathInfo::new("cool-company", 2)) + DpiAwarePathInfo::new("cool-company.png", 2) ); assert_eq!( extract_path_info("engineers@10x.png"), - Some(DpiAwarePathInfo::new("engineers", 10)) + DpiAwarePathInfo::new("engineers.png", 10) ); assert_eq!( extract_path_info("we.like.dots@3x.png"), - Some(DpiAwarePathInfo::new("we.like.dots", 3)) + DpiAwarePathInfo::new("we.like.dots.png", 3) ); assert_eq!( extract_path_info("backup-your-stuff@4x.png.bak"), - Some(DpiAwarePathInfo::new("backup-your-stuff@4x.png", 1)) + DpiAwarePathInfo::new("backup-your-stuff.png.bak", 4) ); } } diff --git a/src/lua_ast.rs b/src/lua_ast.rs index b12df7d..d47dddf 100644 --- a/src/lua_ast.rs +++ b/src/lua_ast.rs @@ -41,6 +41,14 @@ pub(crate) struct Block { pub statements: Vec, } +impl From for Block { + fn from(statement: Statement) -> Self { + Self { + statements: vec![statement], + } + } +} + impl FmtLua for Block { fn fmt_lua(&self, output: &mut LuaStream<'_>) -> fmt::Result { for statement in &self.statements { @@ -90,7 +98,7 @@ impl FmtLua for Statement { output.unindent(); } - writeln!(output, "end") + write!(output, "end") } } } @@ -99,10 +107,21 @@ impl FmtLua for Statement { proxy_display!(Statement); pub(crate) struct IfBlock { - condition: Expression, - body: Block, - else_if_blocks: Vec<(Expression, Block)>, - else_block: Option, + pub condition: Expression, + pub body: Block, + pub else_if_blocks: Vec<(Expression, Block)>, + pub else_block: Option, +} + +impl IfBlock { + pub fn new, B: Into>(condition: E, body: B) -> Self { + Self { + condition: condition.into(), + body: body.into(), + else_if_blocks: Vec::new(), + else_block: None, + } + } } pub(crate) enum Expression { @@ -241,6 +260,12 @@ pub(crate) struct Function { pub body: Vec, } +impl Function { + pub fn new(args: String, body: Vec) -> Self { + Self { args, body } + } +} + impl FmtLua for Function { fn fmt_lua(&self, output: &mut LuaStream<'_>) -> fmt::Result { writeln!(output, "function({})", self.args)?; diff --git a/src/roblox_web_api.rs b/src/roblox_web_api.rs index 1e0d097..e590566 100644 --- a/src/roblox_web_api.rs +++ b/src/roblox_web_api.rs @@ -72,7 +72,10 @@ impl RobloxApiClient { let body = response.text().unwrap(); if response.status().is_success() { - Ok(serde_json::from_str(&body)?) + match serde_json::from_str(&body) { + Ok(response) => Ok(response), + Err(source) => Err(RobloxApiError::BadResponseJson { body, source }), + } } else { Err(RobloxApiError::ResponseError { status: response.status(), @@ -133,9 +136,9 @@ pub enum RobloxApiError { source: reqwest::Error, }, - #[error("Roblox API returned success, but had malformed JSON response")] + #[error("Roblox API returned success, but had malformed JSON response: {body}")] BadResponseJson { - #[from] + body: String, source: serde_json::Error, }, diff --git a/src/sync_backend.rs b/src/sync_backend.rs index baa87ff..4ef80c2 100644 --- a/src/sync_backend.rs +++ b/src/sync_backend.rs @@ -3,10 +3,7 @@ use std::{borrow::Cow, io, path::Path}; use fs_err as fs; use thiserror::Error; -use crate::{ - asset_name::AssetName, - roblox_web_api::{ImageUploadData, RobloxApiClient, RobloxApiError}, -}; +use crate::roblox_web_api::{ImageUploadData, RobloxApiClient, RobloxApiError}; pub trait SyncBackend { fn upload(&mut self, data: UploadInfo) -> Result; @@ -17,7 +14,7 @@ pub struct UploadResponse { } pub struct UploadInfo { - pub name: AssetName, + pub name: String, pub contents: Vec, pub hash: String, } @@ -38,7 +35,7 @@ impl<'a> SyncBackend for RobloxSyncBackend<'a> { let response = self.api_client.upload_image(ImageUploadData { image_data: Cow::Owned(data.contents), - name: data.name.as_ref(), + name: &data.name, description: "Uploaded by Tarmac.", })?; @@ -82,12 +79,7 @@ impl SyncBackend for DebugSyncBackend { let path = Path::new(".tarmac-debug"); fs::create_dir_all(path)?; - let mut file_path = path.join(id.to_string()); - - if let Some(ext) = Path::new(data.name.as_ref()).extension() { - file_path.set_extension(ext); - } - + let file_path = path.join(id.to_string()); fs::write(&file_path, &data.contents)?; Ok(UploadResponse { id })