Shuffle around Rojo's public API

This commit is contained in:
Lucien Greathouse
2019-12-17 13:58:46 -08:00
parent ce338a2a72
commit 16c9a23d55
11 changed files with 24 additions and 29 deletions

118
src/cli/build.rs Normal file
View File

@@ -0,0 +1,118 @@
use std::{
fs::File,
io::{self, BufWriter, Write},
};
use failure::Fail;
use crate::{
cli::BuildCommand,
common_setup,
project::ProjectError,
vfs::{FsError, RealFetcher, Vfs, WatchMode},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OutputKind {
Rbxmx,
Rbxlx,
Rbxm,
Rbxl,
}
fn detect_output_kind(options: &BuildCommand) -> Option<OutputKind> {
let extension = options.output.extension()?.to_str()?;
match extension {
"rbxlx" => Some(OutputKind::Rbxlx),
"rbxmx" => Some(OutputKind::Rbxmx),
"rbxl" => Some(OutputKind::Rbxl),
"rbxm" => Some(OutputKind::Rbxm),
_ => None,
}
}
#[derive(Debug, Fail)]
pub enum BuildError {
#[fail(display = "Could not detect what kind of file to create")]
UnknownOutputKind,
#[fail(display = "IO error: {}", _0)]
IoError(#[fail(cause)] io::Error),
#[fail(display = "XML model error: {}", _0)]
XmlModelEncodeError(#[fail(cause)] rbx_xml::EncodeError),
#[fail(display = "Binary model error: {:?}", _0)]
BinaryModelEncodeError(rbx_binary::EncodeError),
#[fail(display = "{}", _0)]
ProjectError(#[fail(cause)] ProjectError),
#[fail(display = "{}", _0)]
FsError(#[fail(cause)] FsError),
}
impl_from!(BuildError {
io::Error => IoError,
rbx_xml::EncodeError => XmlModelEncodeError,
rbx_binary::EncodeError => BinaryModelEncodeError,
ProjectError => ProjectError,
FsError => FsError,
});
fn xml_encode_config() -> rbx_xml::EncodeOptions {
rbx_xml::EncodeOptions::new().property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown)
}
pub fn build(options: BuildCommand) -> Result<(), BuildError> {
let output_kind = detect_output_kind(&options).ok_or(BuildError::UnknownOutputKind)?;
log::debug!("Hoping to generate file of type {:?}", output_kind);
log::trace!("Constructing in-memory filesystem");
let vfs = Vfs::new(RealFetcher::new(WatchMode::Disabled));
let (_maybe_project, tree) = common_setup::start(&options.project, &vfs);
let root_id = tree.get_root_id();
log::trace!("Opening output file for write");
let mut file = BufWriter::new(File::create(&options.output)?);
match output_kind {
OutputKind::Rbxmx => {
// Model files include the root instance of the tree and all its
// descendants.
rbx_xml::to_writer(&mut file, tree.inner(), &[root_id], xml_encode_config())?;
}
OutputKind::Rbxlx => {
// Place files don't contain an entry for the DataModel, but our
// RbxTree representation does.
let root_instance = tree.get_instance(root_id).unwrap();
let top_level_ids = root_instance.children();
rbx_xml::to_writer(&mut file, tree.inner(), top_level_ids, xml_encode_config())?;
}
OutputKind::Rbxm => {
rbx_binary::encode(tree.inner(), &[root_id], &mut file)?;
}
OutputKind::Rbxl => {
log::warn!("Support for building binary places (rbxl) is still experimental.");
log::warn!("Using the XML place format (rbxlx) is recommended instead.");
log::warn!("For more info, see https://github.com/LPGhatguy/rojo/issues/180");
let root_instance = tree.get_instance(root_id).unwrap();
let top_level_ids = root_instance.children();
rbx_binary::encode(tree.inner(), top_level_ids, &mut file)?;
}
}
file.flush()?;
log::trace!("Done!");
Ok(())
}

17
src/cli/init.rs Normal file
View File

@@ -0,0 +1,17 @@
use failure::Fail;
use crate::{cli::InitCommand, project::ProjectError};
#[derive(Debug, Fail)]
pub enum InitError {
#[fail(display = "Project init error: {}", _0)]
ProjectError(#[fail(cause)] ProjectError),
}
impl_from!(InitError {
ProjectError => ProjectError,
});
pub fn init(_options: InitCommand) -> Result<(), InitError> {
unimplemented!("init command");
}

199
src/cli/mod.rs Normal file
View File

@@ -0,0 +1,199 @@
//! Defines Rojo's CLI through structopt types.
mod build;
mod init;
mod serve;
mod upload;
use std::{env, error::Error, fmt, path::PathBuf, str::FromStr};
use structopt::StructOpt;
pub use self::build::*;
pub use self::init::*;
pub use self::serve::*;
pub use self::upload::*;
/// Trick used with structopt to get the initial working directory of the
/// process and store it for use in default values.
fn working_dir() -> &'static str {
lazy_static::lazy_static! {
static ref INITIAL_WORKING_DIR: String = {
env::current_dir().unwrap().display().to_string()
};
}
&INITIAL_WORKING_DIR
}
/// Command line options that Rojo accepts, defined using the structopt crate.
#[derive(Debug, StructOpt)]
#[structopt(name = "Rojo", about, author)]
pub struct Options {
/// Sets verbosity level. Can be specified multiple times.
#[structopt(long = "verbose", short, parse(from_occurrences))]
pub verbosity: u8,
/// Subcommand to run in this invocation.
#[structopt(subcommand)]
pub subcommand: Subcommand,
}
/// All of Rojo's subcommands.
#[derive(Debug, StructOpt)]
pub enum Subcommand {
/// Creates a new Rojo project.
Init(InitCommand),
/// Serves the project's files for use with the Rojo Studio plugin.
Serve(ServeCommand),
/// Generates a model or place file from the project.
Build(BuildCommand),
/// Generates a place or model file out of the project and uploads it to Roblox.
Upload(UploadCommand),
}
/// Initializes a new Rojo project.
#[derive(Debug, StructOpt)]
pub struct InitCommand {
/// Path to the place to create the project. Defaults to the current directory.
#[structopt(default_value = &working_dir())]
pub path: PathBuf,
/// The kind of project to create, 'place' or 'model'. Defaults to place.
#[structopt(long, default_value = "place")]
pub kind: InitKind,
}
/// The templates we support for initializing a Rojo project.
#[derive(Debug, Clone, Copy)]
pub enum InitKind {
/// A place that matches what File -> New does in Roblox Studio.
Place,
/// An empty model, suitable for a library or plugin.
Model,
}
impl FromStr for InitKind {
type Err = InitKindParseError;
fn from_str(source: &str) -> Result<Self, Self::Err> {
match source {
"place" => Ok(InitKind::Place),
"model" => Ok(InitKind::Model),
_ => Err(InitKindParseError {
attempted: source.to_owned(),
}),
}
}
}
/// Error type for failing to parse an `InitKind`.
#[derive(Debug)]
pub struct InitKindParseError {
attempted: String,
}
impl Error for InitKindParseError {}
impl fmt::Display for InitKindParseError {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(
formatter,
"Invalid init kind '{}'. Valid kinds are: place, model",
self.attempted
)
}
}
/// Expose a Rojo project through a web server that can communicate with the
/// Rojo Roblox Studio plugin, or be visited by the user in the browser.
#[derive(Debug, StructOpt)]
pub struct ServeCommand {
/// Path to the project to serve. Defaults to the current directory.
#[structopt(default_value = &working_dir())]
pub project: PathBuf,
/// The port to listen on. Defaults to the project's preference, or 34872 if
/// it has none.
#[structopt(long)]
pub port: Option<u16>,
}
/// Build a Rojo project into a file.
#[derive(Debug, StructOpt)]
pub struct BuildCommand {
/// Path to the project to serve. Defaults to the current directory.
#[structopt(default_value = &working_dir())]
pub project: PathBuf,
/// Where to output the result.
#[structopt(long, short)]
pub output: PathBuf,
}
/// Build and upload a Rojo project to Roblox.com.
#[derive(Debug, StructOpt)]
pub struct UploadCommand {
/// Path to the project to upload. Defaults to the current directory.
#[structopt(default_value = &working_dir())]
pub project: PathBuf,
/// The kind of asset to generate, 'place', or 'model'. Defaults to place.
#[structopt(long, default_value = "place")]
pub kind: UploadKind,
/// Authenication cookie to use. If not specified, Rojo will attempt to find one from the system automatically.
#[structopt(long)]
pub cookie: Option<String>,
/// Asset ID to upload to.
#[structopt(long = "asset_id")]
pub asset_id: u64,
}
/// The kind of asset to upload to the website. Affects what endpoints Rojo uses
/// and changes how the asset is built.
#[derive(Debug, Clone, Copy)]
pub enum UploadKind {
/// Upload to a place.
Place,
/// Upload to a model-like asset, like a Model, Plugin, or Package.
Model,
}
impl FromStr for UploadKind {
type Err = UploadKindParseError;
fn from_str(source: &str) -> Result<Self, Self::Err> {
match source {
"place" => Ok(UploadKind::Place),
"model" => Ok(UploadKind::Model),
_ => Err(UploadKindParseError {
attempted: source.to_owned(),
}),
}
}
}
/// Error type for failing to parse an `UploadKind`.
#[derive(Debug)]
pub struct UploadKindParseError {
attempted: String,
}
impl Error for UploadKindParseError {}
impl fmt::Display for UploadKindParseError {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(
formatter,
"Invalid upload kind '{}'. Valid kinds are: place, model",
self.attempted
)
}
}

76
src/cli/serve.rs Normal file
View File

@@ -0,0 +1,76 @@
use std::{
io::{self, Write},
sync::Arc,
};
use failure::Fail;
use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor};
use crate::{
cli::ServeCommand,
project::ProjectError,
serve_session::ServeSession,
vfs::{RealFetcher, Vfs, WatchMode},
web::LiveServer,
};
const DEFAULT_PORT: u16 = 34872;
#[derive(Debug, Fail)]
pub enum ServeError {
#[fail(display = "Couldn't load project: {}", _0)]
ProjectError(#[fail(cause)] ProjectError),
}
impl_from!(ServeError {
ProjectError => ProjectError,
});
pub fn serve(options: ServeCommand) -> Result<(), ServeError> {
let vfs = Vfs::new(RealFetcher::new(WatchMode::Enabled));
let session = Arc::new(ServeSession::new(vfs, &options.project));
let port = options
.port
.or_else(|| session.project_port())
.unwrap_or(DEFAULT_PORT);
let server = LiveServer::new(session);
let _ = show_start_message(port);
server.start(port);
Ok(())
}
fn show_start_message(port: u16) -> io::Result<()> {
let writer = BufferWriter::stdout(ColorChoice::Auto);
let mut buffer = writer.buffer();
writeln!(&mut buffer, "Rojo server listening:")?;
write!(&mut buffer, " Address: ")?;
buffer.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true))?;
writeln!(&mut buffer, "localhost")?;
buffer.set_color(&ColorSpec::new())?;
write!(&mut buffer, " Port: ")?;
buffer.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true))?;
writeln!(&mut buffer, "{}", port)?;
writeln!(&mut buffer)?;
buffer.set_color(&ColorSpec::new())?;
write!(&mut buffer, "Visit ")?;
buffer.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true))?;
write!(&mut buffer, "http://localhost:{}/", port)?;
buffer.set_color(&ColorSpec::new())?;
writeln!(&mut buffer, " in your browser for more information.")?;
writer.print(&buffer)?;
Ok(())
}

72
src/cli/upload.rs Normal file
View File

@@ -0,0 +1,72 @@
use failure::Fail;
use reqwest::header::{ACCEPT, CONTENT_TYPE, COOKIE, USER_AGENT};
use crate::{
auth_cookie::get_auth_cookie,
cli::UploadCommand,
common_setup,
vfs::{RealFetcher, Vfs, WatchMode},
};
#[derive(Debug, Fail)]
pub enum UploadError {
#[fail(display = "Rojo could not find your Roblox auth cookie. Please pass one via --cookie.")]
NeedAuthCookie,
#[fail(display = "XML model file encode error: {}", _0)]
XmlModelEncode(#[fail(cause)] rbx_xml::EncodeError),
#[fail(display = "HTTP error: {}", _0)]
Http(#[fail(cause)] reqwest::Error),
#[fail(display = "Roblox API error: {}", _0)]
RobloxApi(String),
}
impl_from!(UploadError {
rbx_xml::EncodeError => XmlModelEncode,
reqwest::Error => Http,
});
pub fn upload(options: UploadCommand) -> Result<(), UploadError> {
let cookie = options
.cookie
.or_else(get_auth_cookie)
.ok_or(UploadError::NeedAuthCookie)?;
log::trace!("Constructing in-memory filesystem");
let vfs = Vfs::new(RealFetcher::new(WatchMode::Disabled));
let (_maybe_project, tree) = common_setup::start(&options.project, &vfs);
let root_id = tree.get_root_id();
let mut buffer = Vec::new();
log::trace!("Encoding XML model");
let config = rbx_xml::EncodeOptions::new()
.property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown);
rbx_xml::to_writer(&mut buffer, tree.inner(), &[root_id], config)?;
let url = format!(
"https://data.roblox.com/Data/Upload.ashx?assetid={}",
options.asset_id
);
log::trace!("POSTing to {}", url);
let client = reqwest::Client::new();
let mut response = client
.post(&url)
.header(COOKIE, format!(".ROBLOSECURITY={}", &cookie))
.header(USER_AGENT, "Roblox/WinInet")
.header("Requester", "Client")
.header(CONTENT_TYPE, "application/xml")
.header(ACCEPT, "application/json")
.body(buffer)
.send()?;
if !response.status().is_success() {
return Err(UploadError::RobloxApi(response.text()?));
}
Ok(())
}