commit
afb7befbfa
@ -0,0 +1,2 @@ |
||||
/target |
||||
**/*.rs.bk |
@ -0,0 +1,6 @@ |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project version="4"> |
||||
<component name="NodePackageJsonFileManager"> |
||||
<packageJsonPaths /> |
||||
</component> |
||||
</project> |
@ -0,0 +1,2 @@ |
||||
# Default ignored files |
||||
/workspace.xml |
@ -0,0 +1,14 @@ |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<module type="CPP_MODULE" version="4"> |
||||
<component name="NewModuleRootManager"> |
||||
<content url="file://$MODULE_DIR$"> |
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> |
||||
<sourceFolder url="file://$MODULE_DIR$/examples" isTestSource="false" /> |
||||
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" /> |
||||
<sourceFolder url="file://$MODULE_DIR$/benches" isTestSource="true" /> |
||||
<excludeFolder url="file://$MODULE_DIR$/target" /> |
||||
</content> |
||||
<orderEntry type="inheritedJdk" /> |
||||
<orderEntry type="sourceFolder" forTests="false" /> |
||||
</component> |
||||
</module> |
@ -0,0 +1,6 @@ |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project version="4"> |
||||
<component name="JavaScriptSettings"> |
||||
<option name="languageLevel" value="ES6" /> |
||||
</component> |
||||
</project> |
@ -0,0 +1,8 @@ |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project version="4"> |
||||
<component name="ProjectModuleManager"> |
||||
<modules> |
||||
<module fileurl="file://$PROJECT_DIR$/.idea/manabu.iml" filepath="$PROJECT_DIR$/.idea/manabu.iml" /> |
||||
</modules> |
||||
</component> |
||||
</project> |
@ -0,0 +1,6 @@ |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project version="4"> |
||||
<component name="VcsDirectoryMappings"> |
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" /> |
||||
</component> |
||||
</project> |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,21 @@ |
||||
[package] |
||||
name = "manabu" |
||||
version = "0.1.0" |
||||
authors = ["Ondřej Hruška <ondra@ondrovo.com>"] |
||||
edition = "2018" |
||||
publish = false |
||||
build = "build.rs" |
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
||||
|
||||
[dependencies] |
||||
elefren = "0.20.1" |
||||
log = "0.4.8" |
||||
env_logger = "0.7.0" |
||||
serde = "1.0.101" |
||||
serde_json = "1.0.40" |
||||
smart-default = "0.5.2" |
||||
failure = "0.1.5" |
||||
clap = "2.33.0" |
||||
toml = "0.5.3" |
||||
|
@ -0,0 +1,12 @@ |
||||
use std::process::Command; |
||||
use std::str; |
||||
|
||||
fn main() { |
||||
let desc_c = Command::new("git").args(&["describe", "--all", "--long"]).output().unwrap(); |
||||
|
||||
let desc = unsafe { |
||||
str::from_utf8_unchecked( &desc_c.stdout ) |
||||
}; |
||||
|
||||
println!("cargo:rustc-env=GIT_REV={}", desc); |
||||
} |
@ -0,0 +1,5 @@ |
||||
# Logging level: trace, debug, info, warn, error |
||||
logging="debug" |
||||
|
||||
# Mastodon-compatible instance URL |
||||
instance="https://piggo.space" |
@ -0,0 +1 @@ |
||||
{"foo":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]} |
@ -0,0 +1,125 @@ |
||||
use std::fs::File; |
||||
use std::io::Read; |
||||
use failure::Fallible; |
||||
use serde::Deserialize; |
||||
use serde::Serialize; |
||||
|
||||
const CONFIG_FILE: &str = "manabu.toml"; |
||||
|
||||
const SOFTWARE_NAME: &str = env!("CARGO_PKG_NAME"); |
||||
|
||||
/// 3rd-party libraries that produce log spam - we set these to a fixed higher level
|
||||
/// to allow using e.g. TRACE without drowing our custom messages
|
||||
const SPAMMY_LIBS: [&str; 5] = ["tokio_reactor", "hyper", "reqwest", "mio", "want"]; |
||||
|
||||
#[derive(SmartDefault,Serialize,Deserialize,Debug)] |
||||
#[serde(default)] |
||||
pub struct Config { |
||||
#[default="info"] |
||||
logging: String, |
||||
pub instance: String, |
||||
#[default="manabu_store.json"] |
||||
pub store: String, |
||||
} |
||||
|
||||
const LOG_LEVELS: [&str; 5] = ["error", "warn", "info", "debug", "trace"]; |
||||
|
||||
/// Load the shared config file
|
||||
fn load_config(file: &str) -> Fallible<Config> { |
||||
let mut file = File::open(file)?; |
||||
|
||||
let mut buf = String::new(); |
||||
file.read_to_string(&mut buf)?; |
||||
|
||||
let config: Config = toml::from_str(&buf)?; |
||||
|
||||
// Validations
|
||||
if !LOG_LEVELS.contains(&config.logging.as_str()) { |
||||
bail!("Invalid value for \"logging\""); |
||||
} |
||||
|
||||
Ok(config) |
||||
} |
||||
|
||||
pub(crate) fn init() -> Fallible<Config> { |
||||
let version = format!("{}, built from {}", env!("CARGO_PKG_VERSION"), env!("GIT_REV")); |
||||
let argv = |
||||
clap::App::new(SOFTWARE_NAME) |
||||
.version(version.as_str()) |
||||
.arg( |
||||
clap::Arg::with_name("config") |
||||
.short("c") |
||||
.long("config") |
||||
.value_name("FILE") |
||||
.help("Sets a custom config file (JSON)") |
||||
.takes_value(true), |
||||
) |
||||
.arg(clap::Arg::with_name("v").short("v").multiple(true).help( |
||||
"Sets the level of verbosity (adds to the level configured in the config file)", |
||||
)) |
||||
.arg( |
||||
clap::Arg::with_name("log") |
||||
.short("l") |
||||
.long("log") |
||||
.value_name("LEVEL") |
||||
.help("Set custom logging level (error,warning,info,debug,trace)") |
||||
.takes_value(true), |
||||
) |
||||
.get_matches(); |
||||
|
||||
let confile = argv.value_of("config").unwrap_or(CONFIG_FILE); |
||||
|
||||
println!("{}\nrun with -h for help", SOFTWARE_NAME); |
||||
println!("config file: {}", confile); |
||||
|
||||
let mut config = load_config(confile).unwrap_or_else(|e| { |
||||
println!("Error loading config file: {}", e); |
||||
Default::default() |
||||
}); |
||||
|
||||
if let Some(l) = argv.value_of("log") { |
||||
if !LOG_LEVELS.contains(&config.logging.as_str()) { |
||||
bail!("Invalid value for \"logging\""); |
||||
} |
||||
config.logging = l.to_owned(); |
||||
} |
||||
|
||||
if argv.is_present("v") { |
||||
// bump verbosity if -v's are present
|
||||
let pos = LOG_LEVELS |
||||
.iter() |
||||
.position(|x| x == &config.logging) |
||||
.unwrap(); |
||||
|
||||
config.logging = match LOG_LEVELS |
||||
.iter() |
||||
.nth(pos + argv.occurrences_of("v") as usize) |
||||
{ |
||||
Some(new_level) => new_level.to_string(), |
||||
None => "trace".to_owned(), |
||||
}; |
||||
} |
||||
|
||||
println!("log level: {}", config.logging); |
||||
|
||||
let env = env_logger::Env::default().default_filter_or(&config.logging); |
||||
let mut builder = env_logger::Builder::from_env(env); |
||||
builder.format_timestamp_millis(); |
||||
|
||||
// set logging level for spammy libs. Ensure the configured log level is not exceeded
|
||||
let mut lib_level = log::LevelFilter::Info; |
||||
if config.logging == "warn" { |
||||
lib_level = log::LevelFilter::Warn; |
||||
} |
||||
else if config.logging == "error" { |
||||
lib_level = log::LevelFilter::Error; |
||||
} |
||||
|
||||
for lib in &SPAMMY_LIBS { |
||||
builder.filter_module(lib, lib_level); |
||||
} |
||||
|
||||
builder.init(); |
||||
|
||||
Ok(config) |
||||
} |
@ -0,0 +1,55 @@ |
||||
#[macro_use] |
||||
extern crate log; |
||||
#[macro_use] |
||||
extern crate failure; |
||||
#[macro_use] |
||||
extern crate smart_default; |
||||
#[macro_use] |
||||
extern crate serde; |
||||
#[macro_use] |
||||
use elefren::{helpers::cli, prelude::*}; |
||||
|
||||
use failure::Fallible; |
||||
use crate::bootstrap::Config; |
||||
use crate::store::Store; |
||||
|
||||
mod bootstrap; |
||||
mod store; |
||||
|
||||
fn main() { |
||||
let config : Config = bootstrap::init().expect("error init config"); |
||||
|
||||
debug!("Loaded config: {:#?}", config); |
||||
|
||||
let mut store = Store::from_file(&config.store); |
||||
|
||||
debug!("Store: {:?}", store); |
||||
|
||||
//store.put("foo", vec![1,2,3,4]);
|
||||
|
||||
let mut v : Vec<u32> = store.get("foo").unwrap(); |
||||
v.push(v.last().unwrap()+1); |
||||
store.put("foo", v); |
||||
|
||||
store.save(); |
||||
|
||||
|
||||
|
||||
/* |
||||
|
||||
let registration = Registration::new(config.instance) |
||||
.client_name("elefren_test") |
||||
.build().expect("error register"); |
||||
|
||||
let mastodon = cli::authenticate(registration).expect("error auth"); |
||||
|
||||
println!( |
||||
"{:?}", |
||||
mastodon |
||||
.get_home_timeline().expect("error get TL") |
||||
.items_iter() |
||||
.take(100) |
||||
.collect::<Vec<_>>() |
||||
); |
||||
*/ |
||||
} |
@ -0,0 +1,246 @@ |
||||
use std::borrow::Cow; |
||||
use serde::{Serialize, Deserialize}; |
||||
use serde::de::DeserializeOwned; |
||||
use std::path::{Path, PathBuf}; |
||||
use std::fs::File; |
||||
use std::io::{self, Read, Write}; |
||||
use serde_json::{Map, Value, Error as SerdeError}; |
||||
use std::fmt::{self, Display, Formatter}; |
||||
|
||||
/// Polymorphic value store, optionally backed by a file
|
||||
///
|
||||
/// Anything serializable and unserializable can be put into the store.
|
||||
///
|
||||
/// Please be mindful of the fact that values are serialized and unserialized to/from serde_json::Value
|
||||
/// when they move between the store and the outside world. This incurs some performance penalty,
|
||||
/// but makes it possible to store any Serializable type at all.
|
||||
#[derive(Debug, Serialize, Deserialize)] |
||||
pub struct Store { |
||||
path: Option<PathBuf>, |
||||
autosave: bool, |
||||
items: Map<String, serde_json::Value>, |
||||
} |
||||
|
||||
impl Default for Store { |
||||
fn default() -> Self { |
||||
Self { |
||||
path: None, |
||||
autosave: false, |
||||
items: Map::new() |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Errors occuring in the store's methods
|
||||
#[derive(Debug)] |
||||
pub enum StoreError { |
||||
IOError(io::Error), |
||||
SerdeError(SerdeError), |
||||
Other(Cow<'static, str>), |
||||
} |
||||
|
||||
pub type Result<T> = std::result::Result<T, StoreError>; |
||||
|
||||
impl From<io::Error> for StoreError { |
||||
fn from(e: io::Error) -> Self { |
||||
Self::IOError(e) |
||||
} |
||||
} |
||||
|
||||
impl From<SerdeError> for StoreError { |
||||
fn from(e: SerdeError) -> Self { |
||||
Self::SerdeError(e) |
||||
} |
||||
} |
||||
|
||||
impl Display for StoreError { |
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { |
||||
match self { |
||||
StoreError::IOError(e) => { |
||||
write!(f, "IOError: {}", e) |
||||
} |
||||
StoreError::SerdeError(e) => { |
||||
write!(f, "SerdeError: {}", e) |
||||
} |
||||
StoreError::Other(msg) => { |
||||
write!(f, "{}", msg) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl Store { |
||||
/// Create a new instance without initial load or a file path assigned.
|
||||
/// Path can always be set using `set_path()`
|
||||
pub fn new() -> Self { |
||||
Default::default() |
||||
} |
||||
|
||||
/// Create a new instance of the store.
|
||||
/// If a path is given, it will try to load the content from a file.
|
||||
pub fn from_file<P: AsRef<Path>>(path: P) -> Self { |
||||
let mut store = Store { |
||||
path: Some(path.as_ref().into()), |
||||
autosave: false, |
||||
items: Map::new(), |
||||
}; |
||||
|
||||
let _ = store.load().map_err(|e| { |
||||
error!("Error loading store: {}", e); |
||||
}); |
||||
|
||||
store |
||||
} |
||||
|
||||
/// Set auto-save option - save on each mutation.
|
||||
pub fn set_autosave(&mut self, autosave: bool) { |
||||
self.autosave = autosave; |
||||
} |
||||
|
||||
/// Assign file path for save and load
|
||||
pub fn set_path<P: AsRef<Path>>(&mut self, path: P) { |
||||
self.path = Some(path.as_ref().into()); |
||||
} |
||||
|
||||
/// Delete the assigned file path
|
||||
pub fn unset_path(&mut self) { |
||||
self.path = None; |
||||
} |
||||
|
||||
/// Load from a user-specified file (this ignores the path set with `set_path()`
|
||||
/// or `from_file()`
|
||||
pub fn load_from<P: AsRef<Path>>(&mut self, path: P) -> Result<()> { |
||||
self.items = Self::load_map_from(path)?; |
||||
Ok(()) |
||||
} |
||||
|
||||
/// Load a map from a file - returns the raw inner map.
|
||||
fn load_map_from<P: AsRef<Path>>(path: P) -> Result<Map<String, Value>> { |
||||
let mut file = File::open(path)?; |
||||
let mut buf = String::new(); |
||||
file.read_to_string(&mut buf)?; |
||||
|
||||
if let serde_json::Value::Object(v) = serde_json::from_str(&buf)? { |
||||
Ok(v) |
||||
} else { |
||||
Err(StoreError::Other("Invalid data file format".into())) |
||||
} |
||||
} |
||||
|
||||
/// Load the store's content from the file selected using `set_path()` or `from_file()`
|
||||
pub fn load(&mut self) -> Result<()> { |
||||
match &self.path { |
||||
Some(path) => { |
||||
self.items = Self::load_map_from(path)?; |
||||
Ok(()) |
||||
} |
||||
None => { |
||||
Err(StoreError::Other("No path set".into())) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Save the map to a custom file path.
|
||||
pub fn save_to<P: AsRef<Path>>(&self, path: P) -> Result<()> { |
||||
let as_str = serde_json::to_string(&self.items).unwrap(); |
||||
|
||||
let mut file = File::create(path)?; |
||||
file.write(as_str.as_bytes())?; |
||||
Ok(()) |
||||
} |
||||
|
||||
/// Save the map to a file selected using `set_path()` or `from_file()`.
|
||||
///
|
||||
/// Saving can be automated using `set_autosave()`.
|
||||
pub fn save(&self) -> Result<()> { |
||||
self.save_to(self.path.as_ref().unwrap()) |
||||
} |
||||
|
||||
/// Get an item using a string key.
|
||||
/// If no item was stored with this key, then None is returned.
|
||||
pub fn get<T: DeserializeOwned>(&self, key: &str) -> Option<T> |
||||
{ |
||||
self.items.get(key) |
||||
.map(ToOwned::to_owned) |
||||
.map(serde_json::from_value) |
||||
.transpose() |
||||
.unwrap_or(None) |
||||
} |
||||
|
||||
/// Get an item, or a default value.
|
||||
pub fn get_or<T: DeserializeOwned>(&self, key: &str, def : T) -> T { |
||||
self.get(key).unwrap_or(def) |
||||
} |
||||
|
||||
/// Get an item, or a default value using the Default trait
|
||||
pub fn get_or_default<T: DeserializeOwned + Default>(&self, key: &str) -> T { |
||||
self.get(key).unwrap_or_default() |
||||
} |
||||
|
||||
/// Get an item, or a default value computed from a callback.
|
||||
///
|
||||
/// Use this if it's expensive to create the default value.
|
||||
pub fn get_or_else<T: DeserializeOwned, O : FnOnce() -> T>(&self, key: &str, f : O) -> T { |
||||
self.get(key).unwrap_or_else(f) |
||||
} |
||||
|
||||
/// Get an item using a string key, removing it from the store.
|
||||
/// If no item was stored with this key, then None is returned.
|
||||
pub fn take<T: DeserializeOwned>(&mut self, key: &str) -> Option<T> |
||||
{ |
||||
let value = self.get(&key); |
||||
self.items.remove(key); |
||||
|
||||
if self.autosave { |
||||
let _ = self.save(); |
||||
} |
||||
|
||||
value |
||||
} |
||||
|
||||
/// Remove an item matching a key.
|
||||
/// Returns true if any item was removed.
|
||||
///
|
||||
/// This is similar to take(), but the old item is not unpacked and it's not an error
|
||||
/// if the old item is e.g. malformed.
|
||||
pub fn remove(&mut self, key: &str) -> bool { |
||||
let old = self.items.remove(key); |
||||
|
||||
if self.autosave { |
||||
let _ = self.save(); |
||||
} |
||||
|
||||
old.is_some() |
||||
} |
||||
|
||||
/// Assign an item to a string key.
|
||||
/// If the key was previously occupied, the old value is dropped.
|
||||
pub fn put<ID, T>(&mut self, key: ID, value: T) |
||||
where ID: Into<String>, T: Serialize + DeserializeOwned |
||||
{ |
||||
self.items.insert(key.into(), serde_json::to_value(value).unwrap()); |
||||
|
||||
if self.autosave { |
||||
let _ = self.save(); |
||||
} |
||||
} |
||||
|
||||
/// Assign an item to a string key.
|
||||
/// If the key was previously occupied, the old value is returned.
|
||||
pub fn replace<ID, T>(&mut self, key: ID, value: T) -> Option<T> |
||||
where ID: Into<String>, T: Serialize + DeserializeOwned |
||||
{ |
||||
let rv = self.items |
||||
.insert(key.into(), serde_json::to_value(value).unwrap()) |
||||
.map(serde_json::from_value) |
||||
.transpose() |
||||
.unwrap_or(None); |
||||
|
||||
if self.autosave { |
||||
let _ = self.save(); |
||||
} |
||||
|
||||
rv |
||||
} |
||||
} |
||||
|
Loading…
Reference in new issue