Skip to content

Commit

Permalink
feat: Implement CRUD methods
Browse files Browse the repository at this point in the history
  • Loading branch information
jacobsvante committed Oct 28, 2021
1 parent 5e01c54 commit 42d4038
Show file tree
Hide file tree
Showing 12 changed files with 393 additions and 69 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ let api = RestApi::new(&config);
# 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");
# when.method(POST).path("/query/v1/suiteql");
# then.status(200).body(r#"{"links":[{"rel":"next","href":"https://123456.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql?limit=2&offset=2"},{"rel":"last","href":"https://123456.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql?limit=2&offset=1998"},{"rel":"self","href":"https://123456.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql?limit=2"}],"count":2,"hasMore":false,"items":[{"links":[],"currency":"1","internalid":"24","item":"24","pricelevel":"15","quantity":"1","saleunit":"1","unitprice":"95.49"},{"links":[],"currency":"1","internalid":"24","item":"24","pricelevel":"21","quantity":"1","saleunit":"1","unitprice":"19.99"}],"offset":0,"totalResults":2000}"#);
# });
let res = api.suiteql.fetch_all::<Price>("SELECT * FROM pricing");
Expand Down
2 changes: 2 additions & 0 deletions src/cli/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ pub enum CliError {
MissingIniSection,
#[error("Unknown environment variable: {0}")]
UnknownEnvironmentVariable(String),
#[error("Parameter format invalid")]
BadParam,
}
140 changes: 127 additions & 13 deletions src/cli/main.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use std::{
fs,
io::{stdout, Write},
os::unix::prelude::OsStrExt,
};

use clap::Parser;
use log::{debug, LevelFilter};

use super::opts::{Opts, SubCommand};
use super::opts::{Opts, RestApiSubCommand, SubCommand};
use super::{helpers::safe_extract_arg, ini};
use crate::config::Config;
use crate::error::Error;
Expand All @@ -23,27 +24,24 @@ pub fn run() -> Result<(), Error> {

let cli_opts = Opts::parse();

match &cli_opts.subcmd {
SubCommand::SuiteQl {
query,
match cli_opts.subcmd {
SubCommand::RestApiOpts {
account,
consumer_key,
consumer_secret,
token_id,
token_secret,
limit,
offset,
subcmd,
} => {
let config = Config::new(
account,
consumer_key,
consumer_secret,
token_id,
token_secret,
&account,
&consumer_key,
&consumer_secret,
&token_id,
&token_secret,
);
let api = RestApi::new(&config);
let result = api.suiteql.raw(query, *limit, *offset)?;
println!("{}", result);
rest_api_sub_command(subcmd, api)?
}
SubCommand::DefaultIniPath => {
ini::default_location().map(|p| stdout().write(p.as_os_str().as_bytes()));
Expand All @@ -52,3 +50,119 @@ pub fn run() -> Result<(), Error> {

Ok(())
}

fn rest_api_sub_command(subcmd: RestApiSubCommand, api: RestApi) -> Result<(), Error> {
use RestApiSubCommand::*;
let response = match subcmd {
RestApiSubCommand::SuiteQl {
query,
limit,
offset,
} => api.suiteql.raw(&query, limit, offset)?,
Get {
endpoint,
params,
headers,
} => {
let params = if params.is_empty() {
None
} else {
Some(params.into())
};
let headers = if headers.is_empty() {
None
} else {
Some(headers.into())
};
api.get_raw(&endpoint, params, headers)?
}
Post {
endpoint,
file,
params,
headers,
} => {
let params = if params.is_empty() {
None
} else {
Some(params.into())
};
let headers = if headers.is_empty() {
None
} else {
Some(headers.into())
};
let payload = if let Some(file) = file {
Some(fs::read_to_string(file)?)
} else {
None
};
api.post_raw(&endpoint, params, headers, payload.as_deref())?
}
Put {
endpoint,
file,
params,
headers,
} => {
let params = if params.is_empty() {
None
} else {
Some(params.into())
};
let headers = if headers.is_empty() {
None
} else {
Some(headers.into())
};
let payload = if let Some(file) = file {
Some(fs::read_to_string(file)?)
} else {
None
};
api.put_raw(&endpoint, params, headers, payload.as_deref())?
}
Patch {
endpoint,
file,
params,
headers,
} => {
let params = if params.is_empty() {
None
} else {
Some(params.into())
};
let headers = if headers.is_empty() {
None
} else {
Some(headers.into())
};
let payload = if let Some(file) = file {
Some(fs::read_to_string(file)?)
} else {
None
};
api.patch_raw(&endpoint, params, headers, payload.as_deref())?
}
Delete {
endpoint,
params,
headers,
} => {
let params = if params.is_empty() {
None
} else {
Some(params.into())
};
let headers = if headers.is_empty() {
None
} else {
Some(headers.into())
};
api.delete_raw(&endpoint, params, headers)?
}
};
println!("{}", response.body());
Ok(())
}
87 changes: 79 additions & 8 deletions src/cli/opts.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
use std::path::PathBuf;
use std::{path::PathBuf, str::FromStr};

use clap::Parser;
use log::LevelFilter;

use super::env::EnvVar;
use super::{env::EnvVar, CliError};
use crate::metadata::VERSION;
use crate::params::ParamStr;

impl FromStr for ParamStr {
type Err = CliError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parsed = s
.split_once('=')
.map(|(k, v)| ParamStr::new(k.trim().to_owned(), v.trim().to_owned()));
parsed.ok_or(CliError::BadParam)
}
}

#[derive(Debug, Parser)]
#[clap(name = "netsuite", version = VERSION)]
Expand All @@ -28,10 +39,8 @@ pub(crate) struct Opts {

#[derive(Debug, Parser)]
pub(crate) enum SubCommand {
#[clap(name = "suiteql")]
SuiteQl {
/// The query to execute. If `-` is provided, query will be read from standard input.
query: String,
#[clap(name = "rest-api")]
RestApiOpts {
#[clap(short, long, env = EnvVar::Account.into())]
account: String,
#[clap(short = 'c', long, env = EnvVar::ConsumerKey.into())]
Expand All @@ -42,11 +51,73 @@ pub(crate) enum SubCommand {
token_id: String,
#[clap(short = 'T', long, env = EnvVar::TokenSecret.into())]
token_secret: String,
#[clap(subcommand)]
subcmd: RestApiSubCommand,
},
#[clap(name = "default-ini-path")]
DefaultIniPath,
}

#[derive(Debug, Parser)]
pub(crate) enum RestApiSubCommand {
#[clap(name = "get")]
Get {
/// The endpoint to get data for
endpoint: String,
#[clap(short = 'p', long = "param")]
params: Vec<ParamStr>,
#[clap(short = 'H', long = "header")]
headers: Vec<ParamStr>,
},
#[clap(name = "post")]
Post {
/// The endpoint to submit data to
endpoint: String,
/// A file containing data to submit
file: Option<PathBuf>,
#[clap(short = 'p', long = "param")]
params: Vec<ParamStr>,
#[clap(short = 'H', long = "header")]
headers: Vec<ParamStr>,
},
#[clap(name = "put")]
Put {
/// The endpoint to submit data to
endpoint: String,
/// A file containing data to submit
file: Option<PathBuf>,
#[clap(short = 'p', long = "param")]
params: Vec<ParamStr>,
#[clap(short = 'H', long = "header")]
headers: Vec<ParamStr>,
},
#[clap(name = "patch")]
Patch {
/// The resource to update
endpoint: String,
/// A file containing the update data
file: Option<PathBuf>,
#[clap(short = 'p', long = "param")]
params: Vec<ParamStr>,
#[clap(short = 'H', long = "header")]
headers: Vec<ParamStr>,
},
#[clap(name = "delete")]
Delete {
/// The resource to delete
endpoint: String,
#[clap(short = 'p', long = "param")]
params: Vec<ParamStr>,
#[clap(short = 'H', long = "header")]
headers: Vec<ParamStr>,
},
#[clap(name = "suiteql")]
SuiteQl {
/// The query to execute. If `-` is provided, query will be read from standard input.
query: String,
#[clap(short, long, default_value = "1000")]
limit: usize,
#[clap(short, long, default_value = "0")]
offset: usize,
},
#[clap(name = "default-ini-path")]
DefaultIniPath,
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod metadata;
pub mod oauth1;
mod params;
mod requester;
mod response;
mod rest_api;
mod suiteql;

Expand Down
2 changes: 1 addition & 1 deletion src/oauth1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ fn encode(s: &str) -> String {
percent_encoding::percent_encode(s.as_bytes(), &STRICT_ENCODE_SET).collect()
}

fn to_query(params: &Vec<(String, String)>) -> String {
fn to_query(params: &[(String, String)]) -> String {
let mut pairs: Vec<_> = params
.iter()
.map(|(k, v)| format!("{}={}", encode(k), encode(v)))
Expand Down
41 changes: 27 additions & 14 deletions src/params.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,47 @@
use std::collections::HashMap;

#[derive(Debug, Clone)]
pub struct Params<'a>(Vec<(&'a str, &'a str)>);
pub struct Params(Vec<(String, String)>);

impl<'a> Params<'a> {
impl Params {
pub fn new() -> Self {
Self(Vec::new())
}

pub fn push(&mut self, k: &'a str, v: &'a str) {
pub fn push(&mut self, k: String, v: String) {
self.0.push((k, v))
}

pub fn get(&self) -> &Vec<(&'a str, &'a str)> {
pub fn get(&self) -> &Vec<(String, String)> {
&self.0
}
}

impl<'a> From<Params<'a>> for HashMap<&'a str, &'a str> {
fn from(params: Params<'a>) -> Self {
let mut map = Self::with_capacity(params.0.len());
for (k, v) in params.0 {
map.insert(k, v);
}
map
impl From<Params> for Vec<(String, String)> {
fn from(params: Params) -> Self {
params.0
}
}

impl<'a> Default for Params<'a> {
impl Default for Params {
fn default() -> Self {
Self::new()
}
}

#[derive(Debug)]
pub(crate) struct ParamStr(String, String);

impl ParamStr {
pub(crate) fn new(k: String, v: String) -> Self {
Self(k, v)
}
}

impl From<Vec<ParamStr>> for Params {
fn from(param_strings: Vec<ParamStr>) -> Self {
let mut p = Params::new();
for ps in param_strings {
p.push(ps.0, ps.1);
}
p
}
}
Loading

0 comments on commit 42d4038

Please sign in to comment.