mirror of
https://github.com/lune-org/lune.git
synced 2024-12-12 13:00:37 +00:00
Bundle type definition files in executable
This commit is contained in:
parent
b1e08bf813
commit
79f6d6df8a
14 changed files with 287 additions and 244 deletions
|
@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- Type definitions are now bundled as part of the Lune executable, meaning they do not need to be downloaded, and are instead generated.
|
||||||
|
- `lune --generate-selene-types` will generate the Selene type definitions file, replacing `lune --download-selene-types`
|
||||||
|
- `lune --generate-luau-types` will generate the Luau type definitions file, replacing `lune --download-luau-types`
|
||||||
- Improve error handling and messages for `net.serve`
|
- Improve error handling and messages for `net.serve`
|
||||||
- Improve error handling and messages for `stdio.prompt`
|
- Improve error handling and messages for `stdio.prompt`
|
||||||
|
|
||||||
|
|
20
Cargo.lock
generated
20
Cargo.lock
generated
|
@ -814,6 +814,7 @@ dependencies = [
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_yaml",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1289,6 +1290,19 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_yaml"
|
||||||
|
version = "0.9.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fb06d4b6cdaef0e0c51fa881acb721bed3c924cfaa71d9c94a3b771dfdf6567"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
"unsafe-libyaml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.5"
|
version = "0.10.5"
|
||||||
|
@ -1582,6 +1596,12 @@ version = "0.1.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unsafe-libyaml"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc7ed8ba44ca06be78ea1ad2c3682a43349126c8818054231ee6f4748012aed2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
|
|
|
@ -27,6 +27,7 @@ reqwest.workspace = true
|
||||||
|
|
||||||
anyhow = "1.0.68"
|
anyhow = "1.0.68"
|
||||||
regex = "1.7.1"
|
regex = "1.7.1"
|
||||||
|
serde_yaml = "0.9.17"
|
||||||
|
|
||||||
clap = { version = "4.1.1", features = ["derive"] }
|
clap = { version = "4.1.1", features = ["derive"] }
|
||||||
full_moon = { version = "0.17.0", features = ["roblox"] }
|
full_moon = { version = "0.17.0", features = ["roblox"] }
|
||||||
|
|
|
@ -10,88 +10,83 @@ use crate::{
|
||||||
gen::generate_docs_json_from_definitions,
|
gen::generate_docs_json_from_definitions,
|
||||||
utils::{
|
utils::{
|
||||||
files::find_parse_file_path,
|
files::find_parse_file_path,
|
||||||
github::Client as GithubClient,
|
|
||||||
listing::{find_lune_scripts, print_lune_scripts, sort_lune_scripts},
|
listing::{find_lune_scripts, print_lune_scripts, sort_lune_scripts},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) const LUNE_SELENE_FILE_NAME: &str = "lune.yml";
|
pub(crate) const FILE_NAME_SELENE_TYPES: &str = "lune.yml";
|
||||||
pub(crate) const LUNE_LUAU_FILE_NAME: &str = "luneTypes.d.luau";
|
pub(crate) const FILE_NAME_LUAU_TYPES: &str = "luneTypes.d.luau";
|
||||||
pub(crate) const LUNE_DOCS_FILE_NAME: &str = "luneDocs.json";
|
pub(crate) const FILE_NAME_DOCS: &str = "luneDocs.json";
|
||||||
|
|
||||||
/// Lune CLI
|
pub(crate) const FILE_CONTENTS_SELENE_TYPES: &str = include_str!("../../../lune.yml");
|
||||||
#[derive(Parser, Debug, Default)]
|
pub(crate) const FILE_CONTENTS_LUAU_TYPES: &str = include_str!("../../../luneTypes.d.luau");
|
||||||
#[command(author, version, about, long_about = None)]
|
|
||||||
|
/// A Luau script runner
|
||||||
|
#[derive(Parser, Debug, Default, Clone)]
|
||||||
|
#[command(version, long_about = None)]
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
/// Path to the file to run, or the name
|
/// Script name or full path to the file to run
|
||||||
/// of a luau file in a lune directory
|
|
||||||
///
|
|
||||||
/// Can be omitted when downloading type definitions
|
|
||||||
script_path: Option<String>,
|
script_path: Option<String>,
|
||||||
/// Arguments to pass to the file as vararg (...)
|
/// Arguments to pass to the script, stored in process.args
|
||||||
script_args: Vec<String>,
|
script_args: Vec<String>,
|
||||||
/// Pass this flag to list scripts inside of
|
/// List scripts found inside of a nearby `lune` directory
|
||||||
/// nearby `lune` and / or `.lune` directories
|
|
||||||
#[clap(long, short = 'l')]
|
#[clap(long, short = 'l')]
|
||||||
list: bool,
|
list: bool,
|
||||||
/// Pass this flag to download the Selene type
|
/// Generate a Selene type definitions file in the current dir
|
||||||
/// definitions file to the current directory
|
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
download_selene_types: bool,
|
generate_selene_types: bool,
|
||||||
/// Pass this flag to download the Luau type
|
/// Generate a Luau type definitions file in the current dir
|
||||||
/// definitions file to the current directory
|
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
download_luau_types: bool,
|
generate_luau_types: bool,
|
||||||
/// Pass this flag to generate the Lune documentation file
|
/// Generate a Lune documentation file for Luau LSP
|
||||||
/// from a luau type definitions file in the current directory
|
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
generate_docs_file: bool,
|
generate_docs_file: bool,
|
||||||
|
/// Generate the full Lune wiki directory
|
||||||
|
#[clap(long, hide = true)]
|
||||||
|
generate_wiki_dir: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
impl Cli {
|
impl Cli {
|
||||||
pub fn from_path<S>(path: S) -> Self
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_path<S>(mut self, path: S) -> Self
|
||||||
where
|
where
|
||||||
S: Into<String>,
|
S: Into<String>,
|
||||||
{
|
{
|
||||||
Self {
|
self.script_path = Some(path.into());
|
||||||
script_path: Some(path.into()),
|
self
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_path_with_args<S, A>(path: S, args: A) -> Self
|
pub fn with_args<A>(mut self, args: A) -> Self
|
||||||
where
|
where
|
||||||
S: Into<String>,
|
|
||||||
A: Into<Vec<String>>,
|
A: Into<Vec<String>>,
|
||||||
{
|
{
|
||||||
Self {
|
self.script_args = args.into();
|
||||||
script_path: Some(path.into()),
|
self
|
||||||
script_args: args.into(),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn download_selene_types() -> Self {
|
pub fn generate_selene_types(mut self) -> Self {
|
||||||
Self {
|
self.generate_selene_types = true;
|
||||||
download_selene_types: true,
|
self
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn download_luau_types() -> Self {
|
pub fn generate_luau_types(mut self) -> Self {
|
||||||
Self {
|
self.generate_luau_types = true;
|
||||||
download_luau_types: true,
|
self
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list() -> Self {
|
pub fn generate_docs_file(mut self) -> Self {
|
||||||
Self {
|
self.generate_docs_file = true;
|
||||||
list: true,
|
self
|
||||||
..Default::default()
|
}
|
||||||
}
|
|
||||||
|
pub fn list(mut self) -> Self {
|
||||||
|
self.list = true;
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(self) -> Result<ExitCode> {
|
pub async fn run(self) -> Result<ExitCode> {
|
||||||
|
@ -115,36 +110,37 @@ impl Cli {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Download definition files, if wanted
|
// Generate (save) definition files, if wanted
|
||||||
let download_types_requested = self.download_selene_types || self.download_luau_types;
|
let generate_file_requested =
|
||||||
if download_types_requested {
|
self.generate_selene_types || self.generate_luau_types || self.generate_docs_file;
|
||||||
let client = GithubClient::new();
|
if generate_file_requested {
|
||||||
let release = client.fetch_release_for_this_version().await?;
|
if self.generate_selene_types {
|
||||||
if self.download_selene_types {
|
generate_and_save_file(FILE_NAME_SELENE_TYPES, "Selene type definitions", || {
|
||||||
println!("Downloading Selene type definitions...");
|
Ok(FILE_CONTENTS_SELENE_TYPES.to_string())
|
||||||
client
|
})
|
||||||
.fetch_release_asset(&release, LUNE_SELENE_FILE_NAME)
|
.await?;
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
if self.download_luau_types {
|
if self.generate_luau_types {
|
||||||
println!("Downloading Luau type definitions...");
|
generate_and_save_file(FILE_NAME_LUAU_TYPES, "Luau type definitions", || {
|
||||||
client
|
Ok(FILE_CONTENTS_LUAU_TYPES.to_string())
|
||||||
.fetch_release_asset(&release, LUNE_LUAU_FILE_NAME)
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
}
|
||||||
|
if self.generate_docs_file {
|
||||||
|
generate_and_save_file(FILE_NAME_DOCS, "Luau LSP documentation", || {
|
||||||
|
let docs = &generate_docs_json_from_definitions(
|
||||||
|
FILE_CONTENTS_LUAU_TYPES,
|
||||||
|
"roblox/global",
|
||||||
|
)?;
|
||||||
|
Ok(serde_json::to_string_pretty(docs)?)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Generate docs file, if wanted
|
|
||||||
if self.generate_docs_file {
|
|
||||||
let defs_contents = read_to_string(LUNE_LUAU_FILE_NAME).await?;
|
|
||||||
let docs_root = generate_docs_json_from_definitions(&defs_contents, "roblox/global")?;
|
|
||||||
let docs_contents = serde_json::to_string_pretty(&docs_root)?;
|
|
||||||
write(LUNE_DOCS_FILE_NAME, &docs_contents).await?;
|
|
||||||
}
|
}
|
||||||
if self.script_path.is_none() {
|
if self.script_path.is_none() {
|
||||||
// Only downloading types without running a script is completely
|
// Only generating typedefs without running a script is completely
|
||||||
// fine, and we should just exit the program normally afterwards
|
// fine, and we should just exit the program normally afterwards
|
||||||
// Same thing goes for generating the docs file
|
if generate_file_requested {
|
||||||
if download_types_requested || self.generate_docs_file {
|
|
||||||
return Ok(ExitCode::SUCCESS);
|
return Ok(ExitCode::SUCCESS);
|
||||||
}
|
}
|
||||||
// HACK: We know that we didn't get any arguments here but since
|
// HACK: We know that we didn't get any arguments here but since
|
||||||
|
@ -171,3 +167,34 @@ impl Cli {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn generate_and_save_file(
|
||||||
|
file_path: &str,
|
||||||
|
display_name: &str,
|
||||||
|
f: impl Fn() -> Result<String>,
|
||||||
|
) -> Result<()> {
|
||||||
|
#[cfg(test)]
|
||||||
|
use crate::tests::fmt_path_relative_to_workspace_root;
|
||||||
|
match f() {
|
||||||
|
Ok(file_contents) => {
|
||||||
|
write(file_path, file_contents).await?;
|
||||||
|
#[cfg(not(test))]
|
||||||
|
println!("Generated {display_name} file at '{file_path}'");
|
||||||
|
#[cfg(test)]
|
||||||
|
println!(
|
||||||
|
"Generated {display_name} file at '{}'",
|
||||||
|
fmt_path_relative_to_workspace_root(file_path)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
#[cfg(not(test))]
|
||||||
|
println!("Failed to generate {display_name} file at '{file_path}'\n{e}");
|
||||||
|
#[cfg(test)]
|
||||||
|
println!(
|
||||||
|
"Failed to generate {display_name} file at '{}'\n{e}",
|
||||||
|
fmt_path_relative_to_workspace_root(file_path)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ mod visitor;
|
||||||
|
|
||||||
use self::{doc::DocsFunctionParamLink, visitor::DocumentationVisitor};
|
use self::{doc::DocsFunctionParamLink, visitor::DocumentationVisitor};
|
||||||
|
|
||||||
fn parse_definitions(contents: &str) -> Result<DocumentationVisitor> {
|
pub fn parse_definitions(contents: &str) -> Result<DocumentationVisitor> {
|
||||||
// TODO: Properly handle the "declare class" syntax, for now we just skip it
|
// TODO: Properly handle the "declare class" syntax, for now we just skip it
|
||||||
let mut no_declares = contents.to_string();
|
let mut no_declares = contents.to_string();
|
||||||
while let Some(dec) = no_declares.find("\ndeclare class") {
|
while let Some(dec) = no_declares.find("\ndeclare class") {
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
use std::env::{current_dir, set_current_dir};
|
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
|
||||||
use serde_json::Value;
|
|
||||||
use tokio::fs::{create_dir_all, read_to_string, remove_file};
|
|
||||||
|
|
||||||
use crate::cli::{Cli, LUNE_LUAU_FILE_NAME, LUNE_SELENE_FILE_NAME};
|
|
||||||
|
|
||||||
async fn run_cli(cli: Cli) -> Result<()> {
|
|
||||||
let path = current_dir()
|
|
||||||
.context("Failed to get current dir")?
|
|
||||||
.join("bin");
|
|
||||||
create_dir_all(&path)
|
|
||||||
.await
|
|
||||||
.context("Failed to create bin dir")?;
|
|
||||||
set_current_dir(&path).context("Failed to set current dir")?;
|
|
||||||
cli.run().await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn ensure_file_exists_and_is_not_json(file_name: &str) -> Result<()> {
|
|
||||||
match read_to_string(file_name)
|
|
||||||
.await
|
|
||||||
.context("Failed to read definitions file")
|
|
||||||
{
|
|
||||||
Ok(file_contents) => match serde_json::from_str::<Value>(&file_contents) {
|
|
||||||
Err(_) => {
|
|
||||||
remove_file(file_name)
|
|
||||||
.await
|
|
||||||
.context("Failed to remove definitions file")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Ok(_) => bail!("Downloading selene definitions returned json, expected luau"),
|
|
||||||
},
|
|
||||||
Err(e) => bail!("Failed to download selene definitions!\n{e}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
|
||||||
async fn list() -> Result<()> {
|
|
||||||
Cli::list().run().await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
|
||||||
async fn download_selene_types() -> Result<()> {
|
|
||||||
run_cli(Cli::download_selene_types()).await?;
|
|
||||||
ensure_file_exists_and_is_not_json(LUNE_SELENE_FILE_NAME).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
|
||||||
async fn download_luau_types() -> Result<()> {
|
|
||||||
run_cli(Cli::download_luau_types()).await?;
|
|
||||||
ensure_file_exists_and_is_not_json(LUNE_LUAU_FILE_NAME).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
20
packages/cli/src/tests/bin_dir.rs
Normal file
20
packages/cli/src/tests/bin_dir.rs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
use std::{env::set_current_dir, path::PathBuf};
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use tokio::fs::create_dir_all;
|
||||||
|
|
||||||
|
pub async fn enter_bin_dir() -> Result<()> {
|
||||||
|
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../bin");
|
||||||
|
if !path.exists() {
|
||||||
|
create_dir_all(&path)
|
||||||
|
.await
|
||||||
|
.context("Failed to enter bin dir")?;
|
||||||
|
set_current_dir(&path).context("Failed to set current dir")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn leave_bin_dir() -> Result<()> {
|
||||||
|
set_current_dir(env!("CARGO_MANIFEST_DIR")).context("Failed to leave bin dir")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
57
packages/cli/src/tests/file_checks.rs
Normal file
57
packages/cli/src/tests/file_checks.rs
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
use tokio::fs::{read_to_string, remove_file};
|
||||||
|
|
||||||
|
use super::bin_dir::{enter_bin_dir, leave_bin_dir};
|
||||||
|
use super::file_type::FileType;
|
||||||
|
|
||||||
|
pub fn fmt_path_relative_to_workspace_root(value: &str) -> String {
|
||||||
|
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("../../")
|
||||||
|
.canonicalize()
|
||||||
|
.unwrap();
|
||||||
|
match PathBuf::from(value).strip_prefix(root) {
|
||||||
|
Err(_) => format!("{:#?}", PathBuf::from(value).display()),
|
||||||
|
Ok(inner) => format!("{:#?}", inner.display()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn inner(file_name: &str, desired_type: FileType) -> Result<()> {
|
||||||
|
match read_to_string(file_name).await.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to read definitions file at '{}'",
|
||||||
|
fmt_path_relative_to_workspace_root(file_name)
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
Ok(file_contents) => {
|
||||||
|
remove_file(file_name).await.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to remove definitions file at '{}'",
|
||||||
|
fmt_path_relative_to_workspace_root(file_name)
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let parsed_type = FileType::from_contents(&file_contents);
|
||||||
|
if parsed_type != Some(desired_type) {
|
||||||
|
bail!(
|
||||||
|
"Generating definitions file at '{}' created '{}', expected '{}'",
|
||||||
|
fmt_path_relative_to_workspace_root(file_name),
|
||||||
|
parsed_type.map_or("unknown", |t| t.name()),
|
||||||
|
desired_type.name()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => bail!(
|
||||||
|
"Failed to generate definitions file at '{}'\n{e}",
|
||||||
|
fmt_path_relative_to_workspace_root(file_name)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn ensure_file_exists_and_is(file_name: &str, desired_type: FileType) -> Result<()> {
|
||||||
|
enter_bin_dir().await?;
|
||||||
|
let res = inner(file_name, desired_type).await;
|
||||||
|
leave_bin_dir()?;
|
||||||
|
res
|
||||||
|
}
|
32
packages/cli/src/tests/file_type.rs
Normal file
32
packages/cli/src/tests/file_type.rs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::gen::parse_definitions;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum FileType {
|
||||||
|
Json,
|
||||||
|
Yaml,
|
||||||
|
Luau,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileType {
|
||||||
|
pub fn from_contents(contents: &str) -> Option<Self> {
|
||||||
|
if serde_json::from_str::<Value>(contents).is_ok() {
|
||||||
|
Some(Self::Json)
|
||||||
|
} else if serde_yaml::from_str::<Value>(contents).is_ok() {
|
||||||
|
Some(Self::Yaml)
|
||||||
|
} else if parse_definitions(contents).is_ok() {
|
||||||
|
Some(Self::Luau)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
FileType::Json => "json",
|
||||||
|
FileType::Yaml => "yaml",
|
||||||
|
FileType::Luau => "luau",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
40
packages/cli/src/tests/mod.rs
Normal file
40
packages/cli/src/tests/mod.rs
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate::cli::{Cli, FILE_NAME_DOCS, FILE_NAME_LUAU_TYPES, FILE_NAME_SELENE_TYPES};
|
||||||
|
|
||||||
|
mod bin_dir;
|
||||||
|
mod file_checks;
|
||||||
|
mod file_type;
|
||||||
|
mod run_cli;
|
||||||
|
|
||||||
|
pub(crate) use file_checks::*;
|
||||||
|
pub(crate) use file_type::*;
|
||||||
|
pub(crate) use run_cli::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn list() -> Result<()> {
|
||||||
|
Cli::new().list().run().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn generate_selene_types() -> Result<()> {
|
||||||
|
run_cli(Cli::new().generate_selene_types()).await?;
|
||||||
|
ensure_file_exists_and_is(FILE_NAME_SELENE_TYPES, FileType::Yaml).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn generate_luau_types() -> Result<()> {
|
||||||
|
run_cli(Cli::new().generate_luau_types()).await?;
|
||||||
|
ensure_file_exists_and_is(FILE_NAME_LUAU_TYPES, FileType::Luau).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn generate_docs_file() -> Result<()> {
|
||||||
|
run_cli(Cli::new().generate_luau_types()).await?;
|
||||||
|
run_cli(Cli::new().generate_docs_file()).await?;
|
||||||
|
ensure_file_exists_and_is(FILE_NAME_DOCS, FileType::Json).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
12
packages/cli/src/tests/run_cli.rs
Normal file
12
packages/cli/src/tests/run_cli.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate::cli::Cli;
|
||||||
|
|
||||||
|
use super::bin_dir::{enter_bin_dir, leave_bin_dir};
|
||||||
|
|
||||||
|
pub async fn run_cli(cli: Cli) -> Result<()> {
|
||||||
|
enter_bin_dir().await?;
|
||||||
|
cli.run().await?;
|
||||||
|
leave_bin_dir()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -1,97 +0,0 @@
|
||||||
use std::env::current_dir;
|
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use super::net::{get_github_owner_and_repo, get_request_user_agent_header};
|
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Serialize)]
|
|
||||||
pub struct ReleaseAsset {
|
|
||||||
id: u64,
|
|
||||||
url: String,
|
|
||||||
name: Option<String>,
|
|
||||||
label: Option<String>,
|
|
||||||
content_type: String,
|
|
||||||
size: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Serialize)]
|
|
||||||
pub struct Release {
|
|
||||||
id: u64,
|
|
||||||
url: String,
|
|
||||||
tag_name: String,
|
|
||||||
name: Option<String>,
|
|
||||||
body: Option<String>,
|
|
||||||
draft: bool,
|
|
||||||
prerelease: bool,
|
|
||||||
assets: Vec<ReleaseAsset>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Client {
|
|
||||||
github_owner: String,
|
|
||||||
github_repo: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Client {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let (github_owner, github_repo) = get_github_owner_and_repo();
|
|
||||||
Self {
|
|
||||||
github_owner,
|
|
||||||
github_repo,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get(&self, url: &str, accept: Option<&str>) -> Result<Vec<u8>> {
|
|
||||||
let request = reqwest::ClientBuilder::new()
|
|
||||||
.build()?
|
|
||||||
.request(reqwest::Method::GET, url)
|
|
||||||
.header("User-Agent", &get_request_user_agent_header())
|
|
||||||
.header("Accept", accept.unwrap_or("application/vnd.github+json"))
|
|
||||||
.header("X-GitHub-Api-Version", "2022-11-28");
|
|
||||||
Ok(request.send().await?.bytes().await?.to_vec())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_releases(&self) -> Result<Vec<Release>> {
|
|
||||||
let release_api_url = format!(
|
|
||||||
"https://api.github.com/repos/{}/{}/releases",
|
|
||||||
&self.github_owner, &self.github_repo
|
|
||||||
);
|
|
||||||
let response_bytes = self.get(&release_api_url, None).await?;
|
|
||||||
Ok(serde_json::from_slice(&response_bytes)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_release_for_this_version(&self) -> Result<Release> {
|
|
||||||
let release_version_tag = format!("v{}", env!("CARGO_PKG_VERSION"));
|
|
||||||
let all_releases = self.fetch_releases().await?;
|
|
||||||
all_releases
|
|
||||||
.iter()
|
|
||||||
.find(|release| release.tag_name == release_version_tag)
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.with_context(|| format!("Failed to find release for version {release_version_tag}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_release_asset(&self, release: &Release, asset_name: &str) -> Result<()> {
|
|
||||||
if let Some(asset) = release
|
|
||||||
.assets
|
|
||||||
.iter()
|
|
||||||
.find(|asset| matches!(&asset.name, Some(name) if name == asset_name))
|
|
||||||
{
|
|
||||||
let file_path = current_dir()?.join(asset_name);
|
|
||||||
let file_bytes = self
|
|
||||||
.get(&asset.url, Some("application/octet-stream"))
|
|
||||||
.await?;
|
|
||||||
tokio::fs::write(&file_path, &file_bytes)
|
|
||||||
.await
|
|
||||||
.with_context(|| {
|
|
||||||
format!("Failed to write file at path '{}'", &file_path.display())
|
|
||||||
})?;
|
|
||||||
} else {
|
|
||||||
bail!(
|
|
||||||
"Failed to find release asset '{}' for release '{}'",
|
|
||||||
asset_name,
|
|
||||||
&release.tag_name
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,2 @@
|
||||||
pub mod files;
|
pub mod files;
|
||||||
pub mod github;
|
|
||||||
pub mod listing;
|
pub mod listing;
|
||||||
pub mod net;
|
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
pub fn get_github_owner_and_repo() -> (String, String) {
|
|
||||||
let (github_owner, github_repo) = env!("CARGO_PKG_REPOSITORY")
|
|
||||||
.strip_prefix("https://github.com/")
|
|
||||||
.unwrap()
|
|
||||||
.split_once('/')
|
|
||||||
.unwrap();
|
|
||||||
(github_owner.to_owned(), github_repo.to_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_request_user_agent_header() -> String {
|
|
||||||
let (github_owner, github_repo) = get_github_owner_and_repo();
|
|
||||||
format!("{github_owner}-{github_repo}-cli")
|
|
||||||
}
|
|
Loading…
Reference in a new issue