Skip to content

Commit

Permalink
feat!: Support INI configs
Browse files Browse the repository at this point in the history
This is a breaking change because authentication arguments had to be moved to subcommand.
  • Loading branch information
jacobsvante committed Oct 27, 2021
1 parent aea4244 commit 1ee0494
Show file tree
Hide file tree
Showing 8 changed files with 245 additions and 77 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ percent-encoding = "2.1.0"
base64 = "0.13.0"
serde = { version = "1.0.130", features = ["derive"] }
serde_json = { version = "1.0.68", features = ["preserve_order"] }
configparser = "3.0.0"
dirs = "4.0.0"

[dev-dependencies]
httpmock = "0.6.2"
64 changes: 48 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,54 @@ Supports both programmatic and CLI usage.

Currently using ureq for HTTP requests, which means this library is not useful for async environments currently. Async will probably be released as a feature flag in the future.

## Programmatic examples
## CLI

This is the easiest way to get started. To find out what you can do with the CLI, just append `--help` or `-h` to the installed `netsuite` command.

### Config file
It's recommended to create an INI config file before using the CLI, to avoid having to provide all OAuth 1.0 details with every command execution.

You can use `netsuite default-ini-path` to find the default INI config location.
```bash
netsuite default-ini-path
```

Write your settings to the file, in this example using [heredoc syntax](https://en.wikipedia.org/wiki/Here_document):
```bash
cat <<EOF >"$(netsuite default-ini-path)"
[sandbox]
account = <account id>_SB1
consumer_key = <64 chars>
consumer_secret = <64 chars>
token_id = <64 chars>
token_secret = <64 chars>
EOF
```

After this you just have to provide the section name to start using the CLI:
```bash
netsuite -s sandbox suiteql 'SELECT * FROM pricing'
```

### Environment variables

As an alternative you can provide environment variables directly. For example:
```bash
export ACCOUNT=<account id>
export CONSUMER_KEY=<64 chars>
export CONSUMER_SECRET=<64 chars>
export TOKEN_ID=<64 chars>
export TOKEN_SECRET=<64 chars>
netsuite suiteql 'SELECT * FROM pricing'
```

## Programmatic access

See example below on how to integrate into your code.

(You can ignore lines prepended with # if you see them, they are there to ensure that provided rust code is correct.)

```rust
# use httpmock::{MockServer, Method::POST};
use netsuite::{Config, RestApi};

#[derive(Debug, PartialEq, serde::Deserialize)]
Expand All @@ -18,9 +62,10 @@ struct Price {
unitprice: String,
}

# let server = MockServer::start();
let config = Config::new("123456", "2", "3", "4", "5");
let api = RestApi::new(&config);
# use httpmock::{MockServer, Method::POST};
# let server = MockServer::start();
# let api = RestApi::with_base_url(&config, server.base_url());;
# let mock = server.mock(|when, then| {
# when.method(POST).path("/suiteql");
Expand All @@ -30,16 +75,3 @@ let res = api.suiteql.fetch_all::<Price>("SELECT * FROM pricing");
# mock.assert();
assert_eq!(res.unwrap(), [Price { internalid: "24".into(), unitprice: "95.49".into() }, Price { internalid: "24".into(), unitprice: "19.99".into() }]);
```

## CLI examples

Get all prices via SuiteQL.

```bash
export ACCOUNT=<6 chars>;
export CONSUMER_KEY=<64 chars>;
export CONSUMER_SECRET=<64 chars>;
export TOKEN_ID=<64 chars>;
export TOKEN_SECRET=<64 chars>;
netsuite suiteql 'SELECT * FROM pricing'
```
61 changes: 0 additions & 61 deletions src/cli.rs

This file was deleted.

89 changes: 89 additions & 0 deletions src/cli/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use std::{
io::{stdout, Write},
os::unix::prelude::OsStrExt,
path::PathBuf,
};

use crate::config::Config;
use crate::error::Error;
use crate::rest_api::RestApi;
use clap::Parser;
use log::{LevelFilter, debug};
use super::ini;
use super::env::EnvVar;

#[derive(Debug, Parser)]
#[clap(name = "netsuite", version = "abc123")]
pub(crate) struct Opts {
#[clap(short = 's', long, env, default_value = "netsuite")]
ini_section: String,
/// Where to load INI from, defaults to your OS's config directory.
#[clap(short = 'p', long, env)]
ini_path: Option<PathBuf>,
#[clap(subcommand)]
subcmd: SubCommand,
}

#[derive(Debug, Parser)]
enum SubCommand {
#[clap(name = "suiteql")]
SuiteQl {
/// The query to execute. If `-` is provided, query will be read from standard input.
query: String,
#[clap(short, long, env = EnvVar::Account.into())]
account: String,
#[clap(short = 'c', long, env = EnvVar::ConsumerKey.into())]
consumer_key: String,
#[clap(short = 'C', long, env = EnvVar::ConsumerSecret.into())]
consumer_secret: String,
#[clap(short = 't', long, env = EnvVar::TokenId.into())]
token_id: String,
#[clap(short = 'T', long, env = EnvVar::TokenSecret.into())]
token_secret: String,
#[clap(short, long, default_value = "1000")]
limit: usize,
#[clap(short, long, default_value = "0")]
offset: usize,
},
#[clap(name = "default-ini-path")]
DefaultIniPath,
}

pub fn run() -> Result<(), Error> {

if let Err(err) = ini::to_env() {
debug!("Couldn't load INI: {}", err);
};

let cli_opts = Opts::parse();

match &cli_opts.subcmd {
SubCommand::SuiteQl {
query,
account,
consumer_key,
consumer_secret,
token_id,
token_secret,
limit,
offset,
} => {
let config = Config::new(
&account,
&consumer_key,
&consumer_secret,
&token_id,
&token_secret,
);
let api = RestApi::new(&config);
let result = api.suiteql.raw(query, *limit, *offset)?;
println!("{}", result);
}
SubCommand::DefaultIniPath => {
ini::default_location().map(|p| stdout().write(p.as_os_str().as_bytes()));
}
}

Ok(())
}

38 changes: 38 additions & 0 deletions src/cli/env.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
pub enum EnvVar {
Account,
ConsumerKey,
ConsumerSecret,
TokenId,
TokenSecret,
}

impl EnvVar {
pub fn exists(key: &str) -> bool {
match key {
"ACCOUNT" => true,
"CONSUMER_KEY" => true,
"CONSUMER_SECRET" => true,
"TOKEN_ID" => true,
"TOKEN_SECRET" => true,
_ => false,
}
}

pub fn set(key: &str, val: &str) {
if EnvVar::exists(key) {
std::env::set_var(key, val);
}
}
}

impl From<EnvVar> for &'static str {
fn from(var: EnvVar) -> Self {
match var {
EnvVar::Account => "ACCOUNT",
EnvVar::ConsumerKey => "CONSUMER_KEY",
EnvVar::ConsumerSecret => "CONSUMER_SECRET",
EnvVar::TokenId => "TOKEN_ID",
EnvVar::TokenSecret => "TOKEN_SECRET",
}
}
}
5 changes: 5 additions & 0 deletions src/cli/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#[derive(thiserror::Error, Debug)]
pub enum CliError {
#[error("INI path could not be found")]
MissingIniPath,
}
56 changes: 56 additions & 0 deletions src/cli/ini.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use std::path::PathBuf;
use clap::{IntoApp, AppSettings};
use configparser::ini::Ini;
use log::debug;
use super::CliError;
use super::cli::Opts;
use super::env::EnvVar;

pub(crate) fn default_location() -> Option<PathBuf> {
dirs::config_dir().map(|p| p.join("netsuite.ini"))
}

/// Ensure that configured ini values are exported as environment variables, so
/// that they can later be loaded by Opts.
// TODO: This is not very pretty. Submit issue/PR to clap for INI support.
pub(crate) fn to_env() -> Result<(), CliError> {
let maybe_ini_path = &default_location().map(|p| p.into_os_string());
let (ini_path, ini_section) = {
let app = Opts::into_app()
.global_setting(AppSettings::IgnoreErrors)
.mut_arg("ini-path", |arg| match maybe_ini_path {
Some(p) => arg.default_value_os(p),
None => arg,
});
let matches = app.get_matches();
let ini_section: String = matches.value_of_t_or_exit("ini-section");
let ini_path: PathBuf = match matches.value_of_t("ini-path") {
Ok(p) => p,
Err(_) => return Err(CliError::MissingIniPath),
};
(ini_path, ini_section)
};

let mut ini = Ini::new();
if ini_path.exists() {
debug!("Loaded INI {:?}", &ini_path);
ini.load(&ini_path).unwrap_or_default();
} else {
debug!("INI {:?} doesn't exist. Nothing loaded.", &ini_path);
}

let section = ini.remove_section(&ini_section);
if section.is_some() {
debug!("Loaded INI section {}.", &ini_section);
} else {
debug!("No such INI section: {}.", &ini_section);
}

section.unwrap_or_default().into_iter().for_each(|(k, v)| {
let k = k.to_ascii_uppercase();
if let Some(v) = v {
EnvVar::set(&k, &v);
}
});
Ok(())
}
7 changes: 7 additions & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
mod ini;
mod cli;
mod env;
mod error;

pub use error::*;
pub use cli::*;

0 comments on commit 1ee0494

Please sign in to comment.