master
Ondřej Hruška 5 years ago
commit afb7befbfa
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 2
      .gitignore
  2. 6
      .idea/$CACHE_FILE$
  3. 2
      .idea/.gitignore
  4. 14
      .idea/manabu.iml
  5. 6
      .idea/misc.xml
  6. 8
      .idea/modules.xml
  7. 6
      .idea/vcs.xml
  8. 1946
      Cargo.lock
  9. 21
      Cargo.toml
  10. 12
      build.rs
  11. 5
      manabu.toml
  12. 1
      manabu_store.json
  13. 125
      src/bootstrap.rs
  14. 55
      src/main.rs
  15. 246
      src/store.rs

2
.gitignore vendored

@ -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>

2
.idea/.gitignore vendored

@ -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>

1946
Cargo.lock generated

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…
Cancel
Save