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