diff options
author | Matthias Beyer <mail@beyermatthias.de> | 2017-01-25 11:23:21 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-01-25 11:23:21 +0100 |
commit | 1e3193ebb2028478aa26efd1b69697cddf00914f (patch) | |
tree | d1504a620dc7065f387467162d75276b311ab56c | |
parent | 636bfbb768f23c92d581ab660fcaa88927c859b1 (diff) | |
parent | 4804cf36ce9a45a0300d5e78850cf55f6b8b2c2c (diff) | |
download | imag-1e3193ebb2028478aa26efd1b69697cddf00914f.zip imag-1e3193ebb2028478aa26efd1b69697cddf00914f.tar.gz |
Merge pull request #847 from matthiasbeyer/imag-ruby
Imag ruby
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | Makefile | 3 | ||||
-rw-r--r-- | libimagruby/.gitignore | 3 | ||||
-rw-r--r-- | libimagruby/Cargo.toml | 41 | ||||
-rw-r--r-- | libimagruby/Gemfile | 4 | ||||
-rw-r--r-- | libimagruby/Makefile | 20 | ||||
-rw-r--r-- | libimagruby/README.md | 106 | ||||
-rw-r--r-- | libimagruby/Rakefile | 2 | ||||
-rw-r--r-- | libimagruby/imag.gemspec | 26 | ||||
-rw-r--r-- | libimagruby/lib/imag.rb | 96 | ||||
-rw-r--r-- | libimagruby/lib/imag/version.rb | 3 | ||||
-rw-r--r-- | libimagruby/src/cache.rs | 42 | ||||
-rw-r--r-- | libimagruby/src/entry.rs | 282 | ||||
-rw-r--r-- | libimagruby/src/imag.rs | 148 | ||||
-rw-r--r-- | libimagruby/src/lib.rs | 53 | ||||
-rw-r--r-- | libimagruby/src/ruby_utils.rs | 210 | ||||
-rw-r--r-- | libimagruby/src/store.rs | 559 | ||||
-rw-r--r-- | libimagruby/src/storeid.rs | 154 | ||||
-rw-r--r-- | libimagruby/src/toml_utils.rs | 65 | ||||
-rw-r--r-- | libimagruby/src/util.rs | 102 | ||||
-rw-r--r-- | libimagruby/test/test_entries.rb | 27 | ||||
-rw-r--r-- | libimagruby/test/test_ruby.rb | 58 | ||||
-rw-r--r-- | libimagruby/test/test_store.rb | 16 |
23 files changed, 2021 insertions, 0 deletions
@@ -36,6 +36,7 @@ members = [ "libimagnotes", "libimagref", "libimagrt", + "libimagruby", "libimagstore", "libimagstorestdhook", "libimagtimeui", @@ -67,6 +67,9 @@ lib: $(LIB_TARGETS) lib-test: $(LIB_TARGETS_TEST) +lib-imag-ruby-test: + @$(MAKE) -C libimagruby + test: bin-test lib-test install: $(INSTALL_TARGETS) imag-bin-install diff --git a/libimagruby/.gitignore b/libimagruby/.gitignore new file mode 100644 index 0000000..2f7c731 --- /dev/null +++ b/libimagruby/.gitignore @@ -0,0 +1,3 @@ +.bundle +Gemfile.lock +vendor/bundle diff --git a/libimagruby/Cargo.toml b/libimagruby/Cargo.toml new file mode 100644 index 0000000..2d98a08 --- /dev/null +++ b/libimagruby/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "libimagruby" +version = "0.1.0" +authors = ["Matthias Beyer <mail@beyermatthias.de>"] + +description = "Library for the imag core distribution" + +keywords = ["imag", "PIM", "personal", "information", "management"] +readme = "../README.md" +license = "LGPL-2.1" + +documentation = "https://matthiasbeyer.github.io/imag/imag_documentation/index.html" +repository = "https://github.com/matthiasbeyer/imag" +homepage = "http://imag-pim.org" + +[lib] +crate-type = ["dylib"] + +[dependencies] +ruru = "0.9" +lazy_static = "0.2" +log = "0.3" +env_logger = "0.3" +toml = "0.2" +uuid = { version = "0.3", features = ["v4"] } + +[dependencies.libimagerror] +path = "../libimagerror" + +[dependencies.libimagrt] +path = "../libimagrt" + +[dependencies.libimagstore] +path = "../libimagstore" + +[dependencies.libimagstorestdhook] +path = "../libimagstorestdhook" + +[dependencies.libimagutil] +path = "../libimagutil" + diff --git a/libimagruby/Gemfile b/libimagruby/Gemfile new file mode 100644 index 0000000..aadb347 --- /dev/null +++ b/libimagruby/Gemfile @@ -0,0 +1,4 @@ +# frozen_string_literal: true +source "https://rubygems.org" + +gemspec diff --git a/libimagruby/Makefile b/libimagruby/Makefile new file mode 100644 index 0000000..f7ed82d --- /dev/null +++ b/libimagruby/Makefile @@ -0,0 +1,20 @@ +ECHO=$(shell which echo) -e +RUBY=$(shell which ruby) +RUBY_TESTS=$(shell find ./test -maxdepth 1 -name "*.rb" -type f) +RUBY_TEST_TARGETS=$(foreach x,$(subst ,,$(RUBY_TESTS)),$(x)) + +all: lib + +lib: + $(MAKE) -C .. libimagruby + +lib-release: + $(MAKE) -C .. libimagruby-release + +test: lib $(RUBY_TEST_TARGETS) + +$(RUBY_TEST_TARGETS): %: lib .FORCE + @$(ECHO) "\t[TEST ]:\t$@" + @$(RUBY) $(subst -test,,$@) + +.FORCE: diff --git a/libimagruby/README.md b/libimagruby/README.md new file mode 100644 index 0000000..2f761fb --- /dev/null +++ b/libimagruby/README.md @@ -0,0 +1,106 @@ +# imag-ruby + +A Ruby gem for scripting imag modules. + +## How does this work? + +Well, as we have some problems with lifetimes here, we have a fairly complex +codebase in this crate. + +### The Problem + +The Problem is, that `libimagstore::store::FileLockEntry<'a>` has a lifetime. If +we would wrap this object into a ruru wrapper and pass to the Ruby code, we +couldn't guarantee anymore that the lifetime holds. + +The problem is simple, you see... + +### The solution? + +Never pass anything to the Ruby code. + +Yes, exactly. The Ruby code only sees 'handles'. It never actually gets the +`Store` object either. +We move the `Store` Object into a `Cache` object (actually, the Ruby code could +have multiple `Store` objects to work with this way) and return a `StoreHandle` +to the Ruby code (which is a UUID underneath). + +Also, the Ruby code never actually touches a `FileLockEntry` - it only gets a +Handle for each `FileLockEntry` - which is a tuple of the `StoreHandle` and the +`libimagstore::storeid::StoreId` for the Entry. + +Each operation on a `FileLockEntry` is then wrapped by this very library. Each +time `FileLockEntry` is touched, this library fetches the appropriate `Store` +object from the static `Cache`, then fetches the `FileLockEntry` object from it, +does the operation and then drops the object (which implies that the actual +`FileLockEntry` is `update()`d!). + +### The Hell? + +Yes, I know this is a lot of overhead. But what are we talking about here? This +is Ruby code we're talking about here, so speed is not our concern. + +You could argue this is a hell of complexity introduced in this library and yes +it is. +If there are bugs (and I bet there are) they would be complex as hell. +But that's it... if you have a better approach, please file a PR. + +## Tests? + +We have tests Ruby scripts in `./test`, they are not executed by travis-ci, as +we need Ruby `2.3.0` for this and travis has `2.2.0` as latest version. +But I hope we get it in travis soonish. + +## Ruby gem? + +This crate will contain both the Rust bindings for imag using `ruru` and a bunch +of wrapper code for the actual `imag` gem. + +We are not there yet, though. + +### Why another layer of indirection? + +As "ruru" does not yet support modules (which is sad btw) we would end up with +functions for all the things. + +E.G.: `imag_runtime_setup()` instead of `Imag::Runtime::setup()` + +I want to add a Ruby gem to wrap these things. + +So basically a piece of Ruby which uses `imag.rb` (the Rust gem) to build +`imag` as a gem which then exports a fine module system. + +### Ideas for module system: + +```text +Imag (Module) + Runtime (Class) + Store (Class) + Entry (Class) + EntryHeader (Class) + EntryContent (Class (inherits from String)) + StoreId (Class) +``` + +I would name the types the same as in the Rust codebase, to avoid confusion. +Only exception would be the `Entry` class, which would be a `FileLockEntry` +underneath. + +If we adapt `libimagentrytag` and the other `libimagentry*` +libraries, we would extend this type. + +## More plans + +I want to pull these libraries into the Ruby bindings: + +* libimagentryedit +* libimagentryfilter +* libimagentrylink +* libimagentrylist +* libimagentrymarkdown +* libimagentrytag +* libimagentryview + +Which all provide functions on top of `libimagstore::store::{FileLock,}Entry`, +so we will implement them on `Imag::Entry`. + diff --git a/libimagruby/Rakefile b/libimagruby/Rakefile new file mode 100644 index 0000000..43022f7 --- /dev/null +++ b/libimagruby/Rakefile @@ -0,0 +1,2 @@ +require "bundler/gem_tasks" +task :default => :spec diff --git a/libimagruby/imag.gemspec b/libimagruby/imag.gemspec new file mode 100644 index 0000000..c606691 --- /dev/null +++ b/libimagruby/imag.gemspec @@ -0,0 +1,26 @@ +# coding: utf-8 +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'imag/version' + +Gem::Specification.new do |spec| + spec.name = "imag" + spec.version = Imag::VERSION + spec.authors = ["Matthias Beyer"] + spec.email = ["mail@beyermatthias.de"] + + spec.summary = %q{A Ruby gem to script imag.} + spec.description = %q{A Ruby gem to script imag, the personal information management suite for the commandline} + spec.homepage = "http://imag-pim.org" + + spec.files = `git ls-files -z`.split("\x0").reject do |f| + f.match(%r{^(test|spec|features)/}) + end + + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + + spec.add_development_dependency "bundler", "~> 1.13" + spec.add_development_dependency "rake", "~> 10.0" +end diff --git a/libimagruby/lib/imag.rb b/libimagruby/lib/imag.rb new file mode 100644 index 0000000..b8c9111 --- /dev/null +++ b/libimagruby/lib/imag.rb @@ -0,0 +1,96 @@ +#!/usr/bin/env ruby + +module Imag + + IMAG_INIT_FN_NAME = 'imag_ruby_initialize' + + def self.setup binary_path + require binary_path + + self.core_setup + self.classes_setup + end + + module Logger + + def self.init debug, verbose, color + RImag.init_logger debug, verbose, color + end + + def self.trace msg + RImag.trace msg + end + + def self.dbg msg + RImag.dbg msg + end + + def self.debug msg + RImag.debug msg + end + + def self.info msg + RImag.info msg + end + + def self.warn msg + RImag.warn msg + end + + def self.error msg + RImag.error msg + end + + end + + private + + def self.class_names + [ + :StoreId , + :StoreHandle , + :FileLockEntryHandle , + :EntryHeader , + :EntryContent , + ] + end + + def self.core_setup + self.class_names.map {|n| [n, "R#{n}".to_sym ] }.each do |elem| + Imag.const_set elem.first, Kernel.const_get(elem.last) + end + end + + def self.classes_setup + self.class_storeid_setup + end + + def self.class_storeid_setup + Imag::StoreId.class_exec do + def to_s + self.to_str + end + end + end + +end + +if __FILE__ == $0 + puts "Running some tests..." + puts "I hope you passed the library object as first argument..." + begin + Imag.setup ARGV.first + rescue Exception => e + puts "Seems not to be the case... or something else went wrong..." + puts e + exit 1 + end + + Imag::Logger.init true, true, true + Imag::Logger.info "The Logger should work now" + + Imag::Logger.info "Lets see whether we have properly setup StoreId" + Imag::Logger.info Imag::StoreId::new_baseless("baselessId").to_s + Imag::Logger.info "Seems good." +end + diff --git a/libimagruby/lib/imag/version.rb b/libimagruby/lib/imag/version.rb new file mode 100644 index 0000000..c2a2473 --- /dev/null +++ b/libimagruby/lib/imag/version.rb @@ -0,0 +1,3 @@ +module Imag + VERSION = "0.1.0" +end diff --git a/libimagruby/src/cache.rs b/libimagruby/src/cache.rs new file mode 100644 index 0000000..94c2b86 --- /dev/null +++ b/libimagruby/src/cache.rs @@ -0,0 +1,42 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015, 2016 Matthias Beyer <mail@beyermatthias.de> and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +use std::collections::BTreeMap; +use std::sync::Arc; +use std::sync::Mutex; + +use uuid::Uuid; + +use libimagstore::store::Store; + +#[derive(Clone, Debug, Ord, Hash, Eq, PartialOrd, PartialEq)] +pub struct StoreHandle(Uuid); + +impl StoreHandle { + pub fn new() -> StoreHandle { + StoreHandle(Uuid::new_v4()) + } +} + +lazy_static! { + pub static ref RUBY_STORE_CACHE: Arc<Mutex<BTreeMap<StoreHandle, Store>>> = { + Arc::new(Mutex::new(BTreeMap::new())) + }; +} + diff --git a/libimagruby/src/entry.rs b/libimagruby/src/entry.rs new file mode 100644 index 0000000..4614fea --- /dev/null +++ b/libimagruby/src/entry.rs @@ -0,0 +1,282 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015, 2016 Matthias Beyer <mail@beyermatthias.de> and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +use std::collections::BTreeMap; +use std::error::Error; +use std::ops::Deref; +use std::ops::DerefMut; + +use ruru::{Class, Object, AnyObject, Boolean, RString, VM, Hash, NilClass, VerifiedObject}; +use uuid::Uuid; + +use libimagstore::store::FileLockEntry as FLE; +use libimagstore::store::EntryHeader; +use libimagstore::store::EntryContent; +use libimagstore::store::Entry; +use libimagstore::storeid::StoreId; + +use ruby_utils::IntoToml; +use toml_utils::IntoRuby; +use util::Wrap; +use util::Unwrap; +use cache::RUBY_STORE_CACHE; +use cache::StoreHandle; + +pub struct FileLockEntryHandle(StoreHandle, StoreId); + +impl FileLockEntryHandle { + pub fn new(sh: StoreHandle, id: StoreId) -> FileLockEntryHandle { + FileLockEntryHandle(sh, id) + } + + pub fn store_handle(&self) -> &StoreHandle { + &self.0 + } + + pub fn fle_handle(&self) -> &StoreId { + &self.1 + } +} + +wrappable_struct!(FileLockEntryHandle, FileLockEntryWrapper, FLE_WRAPPER); +class!(RFileLockEntryHandle); +impl_wrap!(FileLockEntryHandle => FLE_WRAPPER); +impl_unwrap!(RFileLockEntryHandle => FileLockEntryHandle => FLE_WRAPPER); +impl_verified_object!(RFileLockEntryHandle); + +/// Helper macro for operating on RUBY_STORE_CACHE object +/// +/// This helps us calling operations on FileLockEntry objects. +/// +/// What I do here: Fetch the Store object from the cache, fetch the appropriate FileLockEntry and +/// call the operation on it. +/// +/// This could be improved with another cache, so not the store is cached but the FileLockEntry +/// only, but then we run into lifetime problems with the Store and its FileLockEntry objects. +/// Feel free to fix this, but for now, this is a workable solution. +/// +#[macro_export] +macro_rules! call_on_fle_from_store { + ($itself:ident ($wrapper:ident) -> $name:ident -> $operation:block) => {{ + let handle = $itself.get_data(&*$wrapper); + let store_handle = handle.store_handle(); + call_on_store_by_handle! { + store_handle named store inside { + match store.get(handle.fle_handle().clone()) { + Ok(Some(mut $name)) => { + $operation + }, + Ok(None) => { + VM::raise(Class::from_existing("RuntimeError"), "Obj does not exist"); + NilClass::new().to_any_object() + }, + Err(e) => { + VM::raise(Class::from_existing("RuntimeError"), e.description()); + NilClass::new().to_any_object() + }, + } + } + } + }}; + ($itself:ident ($wrapper:ident) -> $name:ident -> $operation: block on fail return $ex:expr) => {{ + let handle = $itself.get_data(&*$wrapper); + let store_handle = handle.store_handle(); + call_on_store_by_handle! { + store_handle named store inside { + match store.get(handle.fle_handle().clone()) { + Ok(Some(mut $name)) => { + $operation + }, + Ok(None) => { + VM::raise(Class::from_existing("RuntimeError"), "Obj does not exist"); + $ex + }, + Err(e) => { + VM::raise(Class::from_existing("RuntimeError"), e.description()); + $ex + }, + } + } + } + }}; +} + + +methods!( + RFileLockEntryHandle, + itself, + + fn r_get_location() -> AnyObject { + call_on_fle_from_store!(itself (FLE_WRAPPER) -> fle -> { fle.get_location().clone().wrap() }) + } + + fn r_get_header() -> AnyObject { + call_on_fle_from_store!(itself (FLE_WRAPPER) -> fle -> { fle.get_header().clone().wrap() }) + } + + fn r_set_header(hdr: Hash) -> NilClass { + use ruby_utils::IntoToml; + use toml_utils::IntoRuby; + use toml::Value; + + let entryheader = match typecheck!(hdr or return NilClass::new()).into_toml() { + Value::Table(t) => EntryHeader::from(t), + _ => { + let ec = Class::from_existing("RuntimeError"); + VM::raise(ec, "Something weird happened. Hash seems to be not a Hash"); + return NilClass::new(); + }, + }; + + call_on_fle_from_store!(itself (FLE_WRAPPER) -> fle -> { + *fle.get_header_mut() = entryheader; + NilClass::new().to_any_object() + }); + + NilClass::new() + } + + fn r_get_content() -> AnyObject { + call_on_fle_from_store!(itself (FLE_WRAPPER) -> fle -> { + fle.get_content().clone().wrap() + } on fail return NilClass::new().to_any_object()) + } + + fn r_set_content(ctt: RString) -> NilClass { + use ruby_utils::IntoToml; + use toml_utils::IntoRuby; + use toml::Value; + + let content = match typecheck!(ctt).into_toml() { + Value::String(s) => s, + _ => { + let ec = Class::from_existing("RuntimeError"); + VM::raise(ec, "Something weird happened. String seems to be not a String"); + return NilClass::new(); + }, + }; + + call_on_fle_from_store!(itself (FLE_WRAPPER) -> fle -> { + *fle.get_content_mut() = content; + NilClass::new().to_any_object() + }); + + NilClass::new() + } + +); + +wrappable_struct!(EntryHeader, EntryHeaderWrapper, ENTRY_HEADER_WRAPPER); +class!(REntryHeader); +impl_wrap!(EntryHeader => ENTRY_HEADER_WRAPPER); +impl_unwrap!(REntryHeader => EntryHeader => ENTRY_HEADER_WRAPPER); +impl_verified_object!(REntryHeader); + +methods!( + REntryHeader, + itself, + + fn r_entry_header_new() -> AnyObject { + EntryHeader::new().wrap() + } + + fn r_entry_header_insert(spec: RString, obj: AnyObject) -> Boolean { + let spec = typecheck!(spec or return Boolean::new(false)).to_string(); + let obj = obj.unwrap(); // possibly not safe... TODO + + match itself.get_data(&*ENTRY_HEADER_WRAPPER).insert(&spec, obj.into_toml()) { + Ok(b) => Boolean::new(b), + Err(e) => { + VM::raise(Class::from_existing("RuntimeError"), e.description()); + Boolean::new(false) + } + } + } + + fn r_entry_header_set(spec: RString, obj: AnyObject) -> AnyObject { + use ruru::NilClass; + + let spec = typecheck!(spec or return any Boolean::new(false)).to_string(); + let obj = obj.unwrap(); // possibly not safe... TODO + + match itself.get_data(&*ENTRY_HEADER_WRAPPER).set(&spec, obj.into_toml()) { + Ok(Some(v)) => v.into_ruby(), + Ok(None) => NilClass::new().to_any_object(), + Err(e) => { + VM::raise(Class::from_existing("RuntimeError"), e.description()); + return Boolean::new(false).to_any_object(); + } + } + } + + fn r_entry_header_get(spec: RString) -> AnyObject { + use ruru::NilClass; + + let spec = typecheck!(spec or return any Boolean::new(false)).to_string(); + + match itself.get_data(&*ENTRY_HEADER_WRAPPER).read(&spec) { + Ok(Some(v)) => v.into_ruby(), + Ok(None) => NilClass::new().to_any_object(), + Err(e) => { + VM::raise(Class::from_existing("RuntimeError"), e.description()); + return Boolean::new(false).to_any_object(); + } + } + } + +); + +wrappable_struct!(EntryContent, EntryContentWrapper, ENTRY_CONTENT_WRAPPER); +class!(REntryContent); +impl_wrap!(EntryContent => ENTRY_CONTENT_WRAPPER); +impl_unwrap!(REntryContent => EntryContent => ENTRY_CONTENT_WRAPPER); + +wrappable_struct!(Entry, EntryWrapper, ENTRY_WRAPPER); +class!(REntry); +impl_unwrap!(REntry => Entry => ENTRY_WRAPPER); + +pub fn setup_filelockentry() -> Class { + let mut class = Class::new("RFileLockEntryHandle", None); + class.define(|itself| { + itself.def("location", r_get_location); + itself.def("header" , r_get_header); + itself.def("header=" , r_set_header); + itself.def("content" , r_get_content); + itself.def("content=", r_set_content); + }); + class +} + +pub fn setup_entryheader() -> Class { + let mut class = Class::new("REntryHeader", None); + class.define(|itself| { + itself.def("insert", r_entry_header_insert); + itself.def("set" , r_entry_header_set); + itself.def("[]=" , r_entry_header_set); + itself.def("read" , r_entry_header_get); + itself.def("[]" , r_entry_header_get); + }); + class +} + +pub fn setup_entrycontent() -> Class { + let string = Class::from_existing("String"); + let mut class = Class::new("REntryContent", Some(&string)); + class +} diff --git a/libimagruby/src/imag.rs b/libimagruby/src/imag.rs new file mode 100644 index 0000000..a59610b --- /dev/null +++ b/libimagruby/src/imag.rs @@ -0,0 +1,148 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015, 2016 Matthias Beyer <mail@beyermatthias.de> and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + + +use std::error::Error; + +use ruru::{Class, Boolean, RString, NilClass, VM, Object}; + +use libimagrt::logger::ImagLogger; + +class!(RImag); + +methods!( + RImag, + itself, + + fn r_initialize_logger(debug: Boolean, verbose: Boolean, colored: Boolean) -> NilClass { + use std::env::var as env_var; + use env_logger; + use log; + use log::{Log, LogLevel, LogRecord, LogMetadata}; + use log::LogLevelFilter; + + let debug = match debug { + Ok(d) => d.to_bool(), + Err(ref e) => { + VM::raise(e.to_exception(), e.description()); + return NilClass::new(); + }, + }; + + let verbose = match verbose { + Ok(v) => v.to_bool(), + Err(ref e) => { + VM::raise(e.to_exception(), e.description()); + return NilClass::new(); + }, + }; + + let colored = match colored { + Ok(c) => c.to_bool(), + Err(ref e) => { + VM::raise(e.to_exception(), e.description()); + return NilClass::new(); + }, + }; + + if env_var("IMAG_LOG_ENV").is_ok() { + env_logger::init().unwrap(); + } else { + let lvl = if debug { + LogLevelFilter::Debug + } else if verbose { + LogLevelFilter::Info + } else { + LogLevelFilter::Warn + }; + + log::set_logger(|max_log_lvl| { + max_log_lvl.set(lvl); + debug!("Init logger with {}", lvl); + let lgr = ImagLogger::new(lvl.to_log_level().unwrap()) + .with_color(colored) + .with_prefix("[imag][ruby]".to_owned()) + .with_dbg_file_and_line(false); + Box::new(lgr) + }) + .map_err(|_| { + panic!("Could not setup logger"); + }) + .ok(); + } + + NilClass::new() + } + + fn r_log_trace(l: RString) -> NilClass { + match l { + Err(ref e) => VM::raise(e.to_exception(), e.description()), + Ok(s) => trace!("{}", s.to_string()), + } + NilClass::new() + } + + fn r_log_debug(l: RString) -> NilClass { + match l { + Err(ref e) => VM::raise(e.to_exception(), e.description()), + Ok(s) => debug!("{}", s.to_string()), + } + NilClass::new() + } + + fn r_log_info(l: RString) -> NilClass { + match l { + Err(ref e) => VM::raise(e.to_exception(), e.description()), + Ok(s) => info!("{}", s.to_string()), + } + NilClass::new() + } + + fn r_log_warn(l: RString) -> NilClass { + match l { + Err(ref e) => VM::raise(e.to_exception(), e.description()), + Ok(s) => warn!("{}", s.to_string()), + } + NilClass::new() + } + + fn r_log_error(l: RString) -> NilClass { + match l { + Err(ref e) => VM::raise(e.to_exception(), e.description()), + Ok(s) => error!("{}", s.to_string()), + } + NilClass::new() + } + +); + +pub fn setup() -> Class { + let mut class = Class::new("RImag", None); + class.define(|itself| { + itself.def_self("init_logger", r_initialize_logger); + itself.def_self("trace", r_log_trace); + itself.def_self("dbg", r_log_debug); + itself.def_self("debug", r_log_debug); + itself.def_self("info", r_log_info); + itself.def_self("warn", r_log_warn); + itself.def_self("error", r_log_error); + }); + class +} + diff --git a/libimagruby/src/lib.rs b/libimagruby/src/lib.rs new file mode 100644 index 0000000..13d933b --- /dev/null +++ b/libimagruby/src/lib.rs @@ -0,0 +1,53 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015, 2016 Matthias Beyer <mail@beyermatthias.de> and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +#[macro_use] extern crate ruru; +#[macro_use] extern crate lazy_static; +#[macro_use] extern crate log; +extern crate env_logger; +extern crate toml; +extern crate uuid; + +#[macro_use] extern crate libimagerror; +extern crate libimagstore; +extern crate libimagstorestdhook; +extern crate libimagrt; +#[macro_use] extern crate libimagutil; + +#[macro_use] mod util; +#[macro_use] pub mod store; +mod cache; + +pub mod entry; +pub mod imag; +pub mod ruby_utils; +pub mod storeid; +pub mod toml_utils; + +#[no_mangle] +#[allow(non_snake_case)] +pub extern fn Init_liblibimagruby() { + self::store::setup(); + self::storeid::setup(); + self::entry::setup_filelockentry(); + self::entry::setup_entryheader(); + self::entry::setup_entrycontent(); + self::imag::setup(); +} + diff --git a/libimagruby/src/ruby_utils.rs b/libimagruby/src/ruby_utils.rs new file mode 100644 index 0000000..b40f684 --- /dev/null +++ b/libimagruby/src/ruby_utils.rs @@ -0,0 +1,210 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015, 2016 Matthias Beyer <mail@beyermatthias.de> and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +// Ruby -> Toml translation primitives + +use std::collections::BTreeMap; + +use ruru::{Object, AnyObject, Class, RString, Fixnum, Float, Symbol, Hash, Array, VM}; +use ruru::types::ValueType; +use toml::Value; + + +pub trait AsToml : Sized { + fn as_toml(&self) -> Value; +} + +pub trait IntoToml : AsToml { + fn into_toml(self) -> Value { + self.as_toml() + } +} +impl<T: AsToml> IntoToml for T { } + +impl AsToml for AnyObject { + + fn as_toml(&self) -> Value { + match self.value().ty() { + ValueType::None => { + Value::Boolean(false) + }, + ValueType::Object => { + let rte = Class::from_existing("TypeError"); + VM::raise(rte, "Cannot translate type '' to fit into TOML"); + Value::Boolean(false) + }, + ValueType::Class => { + let rte = Class::from_existing("TypeError"); + VM::raise(rte, "Cannot translate type '' to fit into TOML"); + Value::Boolean(false) + }, + ValueType::Module => { + let rte = Class::from_existing("TypeError"); + VM::raise(rte, "Cannot translate type '' to fit into TOML"); + Value::Boolean(false) + }, + ValueType::Float => self.try_convert_to::<Float>().unwrap().as_toml(), + ValueType::RString => self.try_convert_to::<RString>().unwrap().as_toml(), + ValueType::Regexp => { + let rte = Class::from_existing("TypeError"); + VM::raise(rte, "Cannot translate type '' to fit into TOML"); + Value::Boolean(false) + }, + ValueType::Array => self.try_convert_to::<Array>().unwrap().as_toml(), + ValueType::Hash => self.try_convert_to::<Hash>().unwrap().as_toml(), + ValueType::Struct => { + let rte = Class::from_existing("TypeError"); + VM::raise(rte, "Cannot translate type '' to fit into TOML"); + Value::Boolean(false) + }, + ValueType::Bignum => { + let rte = Class::from_existing("TypeError"); + VM::raise(rte, "Cannot translate type '' to fit into TOML"); + Value::Boolean(false) + }, + ValueType::File => { + let rte = Class::from_existing("TypeError"); + VM::raise(rte, "Cannot translate type '' to fit into TOML"); + Value::Boolean(false) + }, + ValueType::Data => { + let rte = Class::from_existing("TypeError"); + VM::raise(rte, "Cannot translate type '' to fit into TOML"); + Value::Boolean(false) + }, + ValueType::Match => { + let rte = Class::from_existing("TypeError"); + VM::raise(rte, "Cannot translate type '' to fit into TOML"); + Value::Boolean(false) + }, + ValueType::Complex => { + let rte = Class::from_existing("TypeError"); + VM::raise(rte, "Cannot translate type '' to fit into TOML"); + Value::Boolean(false) + }, + ValueType::Rational => { + let rte = Class::from_existing("TypeError"); + VM::raise(rte, "Cannot translate type '' to fit into TOML"); + Value::Boolean(false) + }, + ValueType::Nil => Value::Boolean(false), + ValueType::True => Value::Boolean(true), + ValueType::False => Value::Boolean(false), + ValueType::Symbol => self.try_convert_to::<Symbol>().unwrap().as_toml(), + ValueType::Fixnum => self.try_convert_to::<Fixnum>().unwrap().as_toml(), + ValueType::Undef => { + let rte = Class::from_existing("TypeError"); + VM::raise(rte, "Cannot translate type '' to fit into TOML"); + Value::Boolean(false) + }, + ValueType::Node => { + let rte = Class::from_existing("TypeError"); + VM::raise(rte, "Cannot translate type '' to fit into TOML"); + Value::Boolean(false) + }, + ValueType::IClass => { + let rte = Class::from_existing("TypeError"); + VM::raise(rte, "Cannot translate type '' to fit into TOML"); + Value::Boolean(false) + }, + ValueType::Zombie => { + let rte = Class::from_existing("TypeError"); + VM::raise(rte, "Cannot translate type '' to fit into TOML"); + Value::Boolean(false) + }, + ValueType::Mask => { + let rte = Class::from_existing("TypeError"); + VM::raise(rte, "Cannot translate type '' to fit into TOML"); + Value::Boolean(false) + }, + } + } + +} + +impl AsToml for Hash { + + fn as_toml(&self) -> Value { + let mut btm = BTreeMap::new(); + self.try_convert_to::<Hash>() + .unwrap() + .each(|key, value| { + let key = match key.as_toml() { + Value::String(s) => s, + _ => { + let rte = Class::from_existing("TypeError"); + VM::raise(rte, "Can only have String or Symbol as Key for TOML maps"); + String::new() + } + }; + let value = value.as_toml(); + btm.insert(key, value); + }); + Value::Table(btm) + } + +} + +impl AsToml for Array { + + fn as_toml(&self) -> Value { + let vals = self + .try_convert_to::<Array>() + .unwrap() + .into_iter() + .map(|v| v.as_toml()) + .collect::<Vec<Value>>(); + + Value::Array(vals) + } + +} + +impl AsToml for RString { + + fn as_toml(&self) -> Value { + Value::String(self.try_convert_to::<RString>().unwrap().to_string()) + } + +} + +impl AsToml for Float { + + fn as_toml(&self) -> Value { + Value::Float(self.try_convert_to::<Float>().unwrap().to_f64()) + } + +} + +impl AsToml for Symbol { + + fn as_toml(&self) -> Value { + Value::String(self.try_convert_to::<Symbol>().unwrap().to_string()) + } + +} + +impl AsToml for Fixnum { + + fn as_toml(&self) -> Value { + Value::Integer(self.try_convert_to::<Fixnum>().unwrap().to_i64()) + } + +} + diff --git a/libimagruby/src/store.rs b/libimagruby/src/store.rs new file mode 100644 index 0000000..4c3a49a --- /dev/null +++ b/libimagruby/src/store.rs @@ -0,0 +1,559 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015, 2016 Matthias Beyer <mail@beyermatthias.de> and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +use libimagstore::store::Store; +use libimagerror::trace::trace_error; +use std::error::Error; +use std::ops::Deref; +use std::ops::DerefMut; + +use ruru::{Class, Object, AnyObject, Boolean, RString, VM, Hash, NilClass, VerifiedObject}; + +use ruby_utils::IntoToml; +use toml_utils::IntoRuby; +use util::Wrap; +use util::Unwrap; + +use storeid::RStoreId; +use entry::RFileLockEntryHandle; +use cache::StoreHandle; + +wrappable_struct!(StoreHandle, StoreWrapper, STORE_WRAPPER); +class!(RStoreHandle); +impl_wrap!(StoreHandle => STORE_WRAPPER); +impl_unwrap!(RStoreHandle => StoreHandle => STORE_WRAPPER); +impl_verified_object!(RStoreHandle); + +macro_rules! call_on_store_by_handle { + { + $store_handle:ident named $name:ident inside $operation:block + }=> {{ + call_on_store_by_handle! { + $store_handle + named $name + inside $operation + on fail return NilClass::new().to_any_object() + } + }}; + + { + $store_handle:ident named $name:ident inside $operation:block on fail return $ex:expr + } => {{ + use cache::RUBY_STORE_CACHE; + + let arc = RUBY_STORE_CACHE.clone(); + { + let lock = arc.lock(); + match lock { + Ok(mut hm) => { + match hm.get($store_handle) { + Some($name) => { $operation }, + None => { + VM::raise(Class::from_existing("RuntimeError"), + "Tried to operate on non-existing object"); + $ex + } + } + }, + Err(e) => { + VM::raise(Class::from_existing("RuntimeError"), e.description()); + $ex + } + } + } + }}; +} + +macro_rules! call_on_store { + { + $store_name:ident <- $itself:ident wrapped inside $wrapper:ident, + $fle_name:ident <- fetch $fle_handle_name:ident + operation $operation:block + } => { + call_on_store! { + $store_name <- $itself wrapped inside $wrapper, + $fle_name <- fetch $fle_handle_name, + operation $operation, + on fail return NilClass::new() + } + }; + + { + $store_name:ident <- $itself:ident wrapped inside $wrapper:ident, + $fle_name:ident <- fetch $fle_handle_name:ident, + operation $operation:block, + on fail return $fail_expr:expr + } => { + let handle = $itself.get_data(&*$wrapper); + call_on_store_by_handle! { + handle named $store_name inside { + let $fle_name = match $store_name.get($fle_handle_name) { + Ok(Some(fle)) => fle, + Ok(None) => { + VM::raise(Class::from_existing("RuntimeError"), "Obj does not exist"); + return $fail_expr + }, + Err(e) => { + VM::raise(Class::from_existing("RuntimeError"), e.description()); + return $fail_expr + }, + }; + $operation + } + on fail return $fail_expr + } + }; + + { + $store_name:ident <- $itself:ident wrapped inside $wrapper:ident, + operation $operation:block, + on fail return $fail_expr:expr + } => { + let handle = $itself.get_data(&*$wrapper); + call_on_store_by_handle! { + handle named $store_name inside $operation on fail return $fail_expr + } + }; + + { + $store_name:ident <- $itself:ident wrapped inside $wrapper:ident, + operation $block + } => { + let handle = $itself.get_data(&*$wrapper); + call_on_store_by_handle! { handle named $name inside $operation } + }; +} + +methods!( + RStoreHandle, + itself, + + // Build a new Store object, return a handle to it. + // + // This function takes a boolean whether the store should include debugging functionality + // (namingly the debug hooks) and a runtimepath, where the store lifes. + // It then builds a Store object (raising errors on failure and returning Nil) and a handle for + // it. + // It puts the store object and the handle in the cache and returns the handle as object to the + // Ruby code. + // + // # Returns + // + // Nil on failure (including raising an error) + // StoreHandle on success + // + fn new(store_debugging: Boolean, rtp: RString) -> AnyObject { + use std::path::PathBuf; + use libimagerror::into::IntoError; + use libimagerror::trace::trace_error; + use libimagerror::trace::trace_error_dbg; + use libimagerror::trace::trace_error_exit; + use libimagrt::configuration::ConfigErrorKind; + use libimagrt::configuration::Configuration; + use libimagrt::error::RuntimeErrorKind; + use libimagstore::error::StoreErrorKind; + use libimagstore::hook::Hook; + use libimagstore::hook::position::HookPosition as HP; + use libimagstorestdhook::debug::DebugHook; + use libimagstorestdhook::vcs::git::delete::DeleteHook as GitDeleteHook; + use libimagstorestdhook::vcs::git::store_unload::StoreUnloadHook as GitStoreUnloadHook; + use libimagstorestdhook::vcs::git::update::UpdateHook as GitUpdateHook; + + use cache::RUBY_STORE_CACHE; + + let store_debugging = typecheck!(store_debugging or return any NilClass::new()).to_bool(); + let rtp = PathBuf::from(typecheck!(rtp or return any NilClass::new()).to_string()); + + if !rtp.exists() || !rtp.is_dir() { + VM::raise(Class::from_existing("RuntimeError"), "Runtimepath not a directory"); + return NilClass::new().to_any_object(); + } + + let store_config = match Configuration::new(&rtp) { + Ok(mut cfg) => cfg.store_config().cloned(), + Err(e) => if e.err_type() != ConfigErrorKind::NoConfigFileFound { + VM::raise(Class::from_existing("RuntimeError"), e.description()); + return NilClass::new().to_any_object(); + } else { + warn!("No config file found."); + warn!("Continuing without configuration file"); + None + }, + }; + + let storepath = { + let mut spath = rtp.clone(); + spath.push("store"); + spath + }; + + let store = Store::new(storepath.clone(), store_config).map(|mut store| { + // If we are debugging, generate hooks for all positions + if store_debugging { + let hooks : Vec<(Box<Hook>, &str, HP)> = vec![ + (Box::new(DebugHook::new(HP::PreCreate)) , "debug", HP::PreCreate), + (Box::new(DebugHook::new(HP::PostCreate)) , "debug", HP::PostCreate), + (Box::new(DebugHook::new(HP::PreRetrieve)) , "debug", HP::PreRetrieve), + (Box::new(DebugHook::new(HP::PostRetrieve)) , "debug", HP::PostRetrieve), + (Box::new(DebugHook::new(HP::PreUpdate)) , "debug", HP::PreUpdate), + (Box::new(DebugHook::new(HP::PostUpdate)) , "debug", HP::PostUpdate), + (Box::new(DebugHook::new(HP::PreDelete)) , "debug", HP::PreDelete), + (Box::new(DebugHook::new(HP::PostDelete)) , "debug", HP::PostDelete), + ]; + + // If hook registration fails, trace the error and warn, but continue. + for (hook, aspectname, position) in hooks { + if let Err(e) = store.register_hook(position, &String::from(aspectname), hook) { + if e.err_type() == StoreErrorKind::HookRegisterError { + trace_error_dbg(&e); + warn!("Registering debug hook with store failed"); + } else { + trace_error(&e); + }; + } + } + } + + let sp = storepath; + + let hooks : Vec<(Box<Hook>, &str, HP)> = vec![ + (Box::new(GitDeleteHook::new(sp.clone(), HP::PostDelete)), "vcs", HP::PostDelete), + (Box::new(GitUpdateHook::new(sp.clone(), HP::PostUpdate)), "vcs", HP::PostUpdate), + (Box::new(GitStoreUnloadHook::new(sp)), "vcs", HP::StoreUnload), + ]; + + for (hook, aspectname, position) in hooks { + if let Err(e) = store.register_hook(position, &String::from(aspectname), hook) { + if e.err_type() == StoreErrorKind::HookRegisterError { + trace_error_dbg(&e); + warn!("Registering git hook with store failed"); + } else { + trace_error(&e); + }; + } + } + + store + }); + + let store = match store { + Ok(s) => s, + Err(e) => { + VM::raise(Class::from_existing("RuntimeError"), e.description()); + return NilClass::new().to_any_object(); + }, + }; + + let store_handle = StoreHandle::new(); + + let arc = RUBY_STORE_CACHE.clone(); + { + let lock = arc.lock(); + match lock { + Ok(mut hm) => { + hm.insert(store_handle.clone(), store); + return store_handle.wrap().to_any_object(); + }, + Err(e) => { + VM::raise(Class::from_existing("RuntimeError"), e.description()); + return NilClass::new().to_any_object(); + } + } + } + + } + + + // Create an FileLockEntry in the store + // + // # Returns: + // + // On success: A RFileLockEntry + // On failure: Nil + // On error: Nil + Exception + // + fn create(id: RStoreId) -> AnyObject { + use entry::FileLockEntryHandle; + let sid = typecheck!(id or return any NilClass::new()).unwrap().clone(); + + call_on_store! { + store <- itself wrapped inside STORE_WRAPPER, + operation { + match store.create(sid.clone()) { + Err(e) => { + trace_error(&e); + VM::raise(Class::from_existing("RuntimeError"), e.description()); + NilClass::new().to_any_object() + }, + Ok(entry) => { + // Take the location (StoreId) of the entry (we know it exists... so this + // is fine) and wrap it into a RFileLockEntry which is then returned to the + // user (as handle) + let sid = entry.get_location().clone(); + let store_handle = itself.get_data(&*STORE_WRAPPER).clone(); + FileLockEntryHandle::new(store_handle, sid).wrap() + }, + } + }, + on fail return NilClass::new().to_any_object() + } + } + + // Retrieve an FileLockEntry from the store + // + // # Returns: + // + // On success: A RFileLockEntry + // On error: Nil + Exception + // + fn retrieve(id: RStoreId) -> AnyObject { + use entry::FileLockEntryHandle; + let sid = typecheck!(id or return any NilClass::new()).unwrap().clone(); + + call_on_store! { + store <- itself wrapped inside STORE_WRAPPER, + operation { + match store.retrieve(sid.clone()) { + Err(e) => { + trace_error(&e); + VM::raise(Class::from_existing("RuntimeError"), e.description()); + NilClass::new().to_any_object() + }, + Ok(entry) => { + // Take the location (StoreId) of the entry (we know it exists... so this + // is fine) and wrap it into a RFileLockEntry which is then returned to the + // user (as handle) + let sid = entry.get_location().clone(); + let store_handle = itself.get_data(&*STORE_WRAPPER).clone(); + FileLockEntryHandle::new(store_handle, sid).wrap() + }, + } + }, + on fail return NilClass::new().to_any_object() + } + } + + // Get an FileLockEntry from the store + // + // # Returns: + // + // On success, if there is some: A RFileLockEntry + // On success, if there is none: Nil + // On error: Nil + Exception + // + fn get(sid: RStoreId) -> AnyObject { + use entry::FileLockEntryHandle; + let sid = typecheck!(sid or return any NilClass::new()).unwrap().clone(); + + call_on_store! { + store <- itself wrapped inside STORE_WRAPPER, + operation { + match store.get(sid.clone()) { + Err(e) => { + trace_error(&e); + VM::raise(Class::from_existing("RuntimeError"), e.description()); + NilClass::new().to_any_object() + }, + Ok(None) => NilClass::new().to_any_object(), + Ok(Some(entry)) => { + // Take the location (StoreId) of the entry (we know it exists... so this + // is fine) and wrap it into a RFileLockEntry which is then returned to the + // user (as handle) + let sid = entry.get_location().clone(); + let store_handle = itself.get_data(&*STORE_WRAPPER).clone(); + FileLockEntryHandle::new(store_handle, sid).wrap() + }, + } + }, + on fail return NilClass::new().to_any_object() + } + } + + // Get all FileLockEntry of a module from the store + // + // # Returns: + // + // On success: A Array[RFileLockEntry] + // On error: Nil + Exception + // + fn retrieve_for_module(name: RString) -> AnyObject { + unimplemented!() + } + + // Update a FileLockEntry in the store + // + // # Returns: + // + // On success: Nil + // On error: Nil + Exception + // + fn update(fle: RFileLockEntryHandle) -> NilClass { + let fle = typecheck!(fle).unwrap().fle_handle().clone(); + + call_on_store! { + store <- itself wrapped inside STORE_WRAPPER, + real_fle <- fetch fle, + operation { + if let Err(e) = store.update(real_fle) { + trace_error(&e); + VM::raise(Class::from_existing("RuntimeError"), e.description()); + } + NilClass::new() + }, + on fail return NilClass::new() + } + } + + // Delete a FileLockEntry from the store + // + // # Returns: + // + // On success: Nil + // On error: Nil + Exception + // + fn delete(sid: RStoreId) -> NilClass { + let sid = typecheck!(sid).unwrap().clone(); + + call_on_store! { + store <- itself wrapped inside STORE_WRAPPER, + operation { + if let Err(e) = store.delete(sid) { + trace_error(&e); + VM::raise(Class::from_existing("RuntimeError"), e.description()); + } + NilClass::new() + }, + on fail return NilClass::new() + } + } + + // Save a FileLockEntry in a new path inside the store, keep the RFileLockEntry + // + // # Returns: + // + // On success: Nil + // On error: Nil + Exception + // + fn save_to(fle: RFileLockEntryHandle, sid: RStoreId) -> NilClass { + let fle = typecheck!(fle).unwrap().fle_handle().clone(); + let sid = typecheck!(sid).unwrap().clone(); + + call_on_store! { + store <- itself wrapped inside STORE_WRAPPER, + real_fle <- fetch fle, + operation { + if let Err(e) = store.save_to(&real_fle, sid) { + trace_error(&e); + VM::raise(Class::from_existing("RuntimeError"), e.description()); + } + NilClass::new() + }, + on fail return NilClass::new() + } + } + + // Save a FileLockEntry in a new path inside the store, move the RFileLockEntry + // + // # Returns: + // + // On success: Nil + // On error: Nil + Exception + // + fn save_as(fle: RFileLockEntryHandle, sid: RStoreId) -> NilClass { + let fle = typecheck!(fle).unwrap().fle_handle().clone(); + let sid = typecheck!(sid).unwrap().clone(); + + call_on_store! { + store <- itself wrapped inside STORE_WRAPPER, + real_fle <- fetch fle, + operation { + if let Err(e) = store.save_as(real_fle, sid) { + trace_error(&e); + VM::raise(Class::from_existing("RuntimeError"), e.description()); + } + NilClass::new() + }, + on fail return NilClass::new() + } + } + + // Move one entry in the store to another place, by its ID + // + // # Returns: + // + // On success: Nil + // On error: Nil + Exception + // + fn move_by_id(old: RStoreId, nw: RStoreId) -> NilClass { + let old = typecheck!(old).unwrap().clone(); + let nw = typecheck!(nw).unwrap().clone(); + + call_on_store! { + store <- itself wrapped inside STORE_WRAPPER, + operation { + if let Err(e) = store.move_by_id(old, nw) { + trace_error(&e); + VM::raise(Class::from_existing("RuntimeError"), e.description()); + } + NilClass::new() + }, + on fail return NilClass::new() + } + } + + // Get the path of the store object + // + // # Returns: + // + // A RString + // + fn path() -> RString { + call_on_store! { + store <- itself wrapped inside STORE_WRAPPER, + operation { + store.path() + .clone() + .to_str() + .map(RString::new) + .unwrap_or(RString::new("")) + }, + on fail return RString::new("") + } + } + +); + +pub fn setup() -> Class { + let mut class = Class::new("RStoreHandle", None); + class.define(|itself| { + itself.def_self("new" , new); + itself.def("create" , create); + itself.def("retrieve" , retrieve); + itself.def("get" , get); + itself.def("retrieve_for_module" , retrieve_for_module); + itself.def("update" , update); + itself.def("delete" , delete); + itself.def("save_to" , save_to); + itself.def("save_as" , save_as); + itself.def("move_by_id" , move_by_id); + itself.def("path" , path); + }); + class +} + diff --git a/libimagruby/src/storeid.rs b/libimagruby/src/storeid.rs new file mode 100644 index 0000000..a820064 --- /dev/null +++ b/libimagruby/src/storeid.rs @@ -0,0 +1,154 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015, 2016 Matthias Beyer <mail@beyermatthias.de> and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +use std::path::PathBuf; + +use ruru::{Class, Object, AnyObject, Boolean, RString, NilClass, VerifiedObject}; + +use libimagstore::storeid::StoreId; +use util::Unwrap; +use util::Wrap; + +wrappable_struct!(StoreId, StoreIdWrapper, STOREID_WRAPPER); +class!(RStoreId); +impl_wrap!(StoreId => STOREID_WRAPPER); +impl_unwrap!(RStoreId => StoreId => STOREID_WRAPPER); +impl_verified_object!(RStoreId); + + +methods!( + RStoreId, + itself, + + fn r_storeid_new(base: RString, id: RString) -> AnyObject { + let base = match base.map(|b| b.to_string()).map(PathBuf::from) { + Ok(base) => base, + Err(e) => { + // TODO: Exception! + error!("Building StoreId object failed: {:?}", e); + return AnyObject::from(NilClass::new().value()); + }, + }; + + let id = match id.map(|id| id.to_string()).map(PathBuf::from) { + Ok(id) => id, + Err(e) => { + // TODO: Exception! + error!("Building StoreId object failed: {:?}", e); + return AnyObject::from(NilClass::new().value()); + }, + }; + + match StoreId::new(Some(base), id) { + Ok(sid) => Class::from_existing("RStoreId").wrap_data(sid, &*STOREID_WRAPPER), + Err(e) => { + // TODO: Exception! + error!("Building StoreId object failed: {:?}", e); + return AnyObject::from(NilClass::new().value()); + }, + } + } + + fn r_storeid_new_baseless(id: RString) -> AnyObject { + let id = match id.map(|id| id.to_string()).map(PathBuf::from) { + Ok(id) => id, + Err(e) => { + // TODO: Exception! + error!("Building StoreId object failed: {:?}", e); + return AnyObject::from(NilClass::new().value()); + }, + }; + + match StoreId::new(None, id) { + Ok(sid) => Class::from_existing("RStoreId").wrap_data(sid, &*STOREID_WRAPPER), + Err(e) => { + // TODO: Exception! + error!("Building StoreId object failed: {:?}", e); + return AnyObject::from(NilClass::new().value()); + }, + } + } + + fn r_storeid_without_base() -> RStoreId { + let withoutbase : StoreId = itself.get_data(&*STOREID_WRAPPER).clone().without_base(); + Class::from_existing("RStoreId").wrap_data(withoutbase, &*STOREID_WRAPPER) + } + + fn r_storeid_with_base(base: RString) -> AnyObject { + let base : PathBuf = match base.map(|b| b.to_string()).map(PathBuf::from) { + Ok(pb) => pb, + Err(e) => { + // TODO: Exception! + error!("Error: {:?}", e); + return AnyObject::from(NilClass::new().value()); + }, + }; + let withoutbase : StoreId = itself.get_data(&*STOREID_WRAPPER).clone().with_base(base); + Class::from_existing("RStoreId").wrap_data(withoutbase, &*STOREID_WRAPPER) + } + + fn r_storeid_into_pathbuf() -> AnyObject { + itself.get_data(&*STOREID_WRAPPER) + .clone() + .into_pathbuf() + // TODO: No unwraps + .map(|pb| pb.to_str().map(String::from).unwrap()) + .as_ref() + .map(|s| AnyObject::from(RString::new(s).value())) + // TODO: Exception! + .unwrap_or(AnyObject::from(NilClass::new().value())) + } + + fn r_storeid_exists() -> Boolean { + Boolean::new(itself.get_data(&*STOREID_WRAPPER).exists()) + } + + fn r_storeid_to_str() -> AnyObject { + itself.get_data(&*STOREID_WRAPPER) + .to_str() + .as_ref() + .map(|s| AnyObject::from(RString::new(s).value())) + // TODO: Exception! + .unwrap_or(AnyObject::from(NilClass::new().value())) + } + + fn r_storeid_local() -> RString { + let local = itself.get_data(&*STOREID_WRAPPER).local(); + let local = local.to_str().unwrap(); // TODO: No unwraps + RString::new(local) + } + +); + +pub fn setup() -> Class { + let mut class = Class::new("RStoreId", None); + class.define(|itself| { + itself.def_self("new" , r_storeid_new); + itself.def_self("new_baseless" , r_storeid_new_baseless); + + itself.def("without_base" , r_storeid_without_base); + itself.def("with_base" , r_storeid_with_base); + itself.def("into_pathbuf" , r_storeid_into_pathbuf); + itself.def("exists" , r_storeid_exists); + itself.def("to_str" , r_storeid_to_str); + itself.def("local" , r_storeid_local); + }); + class +} + diff --git a/libimagruby/src/toml_utils.rs b/libimagruby/src/toml_utils.rs new file mode 100644 index 0000000..85df9dd --- /dev/null +++ b/libimagruby/src/toml_utils.rs @@ -0,0 +1,65 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015, 2016 Matthias Beyer <mail@beyermatthias.de> and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +// Toml -> Ruby translation primitives + +use ruru::{Object, AnyObject, RString, Fixnum, Float, Boolean, Hash, Array}; +use toml::Value; + +pub trait AsRuby : Sized { + fn as_ruby(&self) -> AnyObject; +} + +pub trait IntoRuby : AsRuby { + fn into_ruby(self) -> AnyObject { + self.as_ruby() + } +} +impl<T: AsRuby> IntoRuby for T { } + +impl AsRuby for Value { + + fn as_ruby(&self) -> AnyObject { + match *self { + Value::String(ref s) => RString::new(&s).to_any_object(), + Value::Integer(i) => Fixnum::new(i).to_any_object(), + Value::Float(f) => Float::new(f).to_any_object(), + Value::Boolean(b) => Boolean::new(b).to_any_object(), + Value::Datetime(ref s) => RString::new(&s).to_any_object(), + Value::Array(ref a) => { + let mut arr = Array::new(); + for obj in a.into_iter().map(AsRuby::as_ruby) { + arr.push(obj); + } + arr.to_any_object() + }, + Value::Table(ref t) => { + let mut h = Hash::new(); + for (k, v) in t.into_iter() { + let key = RString::new(k).to_any_object(); + let v = v.as_ruby(); + h.store(key, v); + } + h.to_any_object() + }, + } + } + +} + diff --git a/libimagruby/src/util.rs b/libimagruby/src/util.rs new file mode 100644 index 0000000..85813f2 --- /dev/null +++ b/libimagruby/src/util.rs @@ -0,0 +1,102 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015, 2016 Matthias Beyer <mail@beyermatthias.de> and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +use ruru::AnyObject; +pub trait Wrap { + fn wrap(self) -> AnyObject; +} + +#[macro_export] +macro_rules! impl_wrap { + ($target:ty => $wrapper:path) => { + impl Wrap for $target { + fn wrap(self) -> AnyObject { + Class::from_existing(concat!("R", stringify!($target))) + .wrap_data(self, &*($wrapper)) + } + } + } +} + +pub trait Unwrap { + type Target; + fn unwrap<'a>(&'a self) -> &'a mut Self::Target; +} + +#[macro_export] +macro_rules! impl_unwrap { + ($from:ty => $to:ty => $wrapper:path) => { + impl Unwrap for $from { + type Target = $to; + fn unwrap<'a>(&'a self) -> &'a mut $to { + self.get_data(&*($wrapper)) + } + } + } +} + +#[macro_export] +macro_rules! impl_verified_object { + ($objname: ty) => { + impl VerifiedObject for $objname { + fn is_correct_type<T: Object>(object: &T) -> bool { + object.class() == Class::from_existing(stringify!($objname)) + } + + fn error_message() -> &'static str { + concat!("Not a ", stringify!($objname), " object") + } + } + }; +} + +/// Helper macro to simplify type checking in the ruby-interfacing functions. +/// +/// # Return +/// +/// If called with only the object to check, this returns NIL after raising an exception. +/// If called with more arguments, the other things will be returned. +/// E.G.: +/// +/// ```ignore +/// let obj1 = typecheck!(obj1); // returns `obj` or raises exception +/// +/// // returns `obj` or raises exception and returns AnyObject (Boolean -> false): +/// let obj2 = typecheck!(obj2 or return any Boolean::new(false)); +/// +/// // returns `obj` or raises excpetion and returns Boolean -> false +/// let obj3 = typecheck!(obj3 or return Boolean::new(false)); +/// ``` +/// +#[macro_export] +macro_rules! typecheck { + ($obj: ident) => { typecheck!($obj or return NilClass::new()) }; + ($obj: ident or return any $els: expr) => { typecheck!($obj or return $els.to_any_object()) }; + ($obj: ident or return $els: expr) => { + match $obj { + Ok(o) => o, + Err(e) => { + VM::raise(e.to_exception(), e.description()); + return $els + }, + } + }; + +} + diff --git a/libimagruby/test/test_entries.rb b/libimagruby/test/test_entries.rb new file mode 100644 index 0000000..f10dc64 --- /dev/null +++ b/libimagruby/test/test_entries.rb @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby + +require "../target/debug/liblibimagruby.so" + +color = true +verbose = true +debug = false + +RImag.init_logger debug, verbose, color + +store_handle = RStoreHandle::new(false, "/tmp/store") +id = RStoreId::new_baseless("test") +test_handle = store_handle.retrieve(id) +puts "Header: #{test_handle.header.to_s}" +puts "Content: '#{test_handle.content}'" + +test_handle.content = "Foo" +test_handle.header = { + "imag" => { + "links" => [], + "version" => "0.2.0" + }, + "example" => { + "test" => "foo" + } +} + diff --git a/libimagruby/test/test_ruby.rb b/libimagruby/test/test_ruby.rb new file mode 100644 index 0000000..9b08859 --- /dev/null +++ b/libimagruby/test/test_ruby.rb @@ -0,0 +1,58 @@ +#!/usr/bin/env ruby + +require "../target/debug/liblibimagruby.so" + +color = true +verbose = true +debug = true + +RImag.init_logger debug, verbose, color + +RImag.trace "Trace-Hello from Ruby" +RImag.dbg "Debug-Hello from Ruby" +RImag.debug "Debug-Hello from Ruby" +RImag.info "Info-Hello from Ruby" +RImag.warn "Warn-Hello from Ruby" +RImag.error "Error-Hello from Ruby" + +def works name, b + if b + RImag.info "Works: #{name}" + else + RImag.error "Fails: #{name}" + end +end + +def has_instance_method klass, meth + works "#{klass}.instance_methods.include? #{meth}", + (klass.instance_methods.include? meth) +end + +puts "---" + +works "RStoreId.new_baseless", (not RStoreId.new_baseless("test").nil?) + +works "RStoreHandle.respond_to? :new", (RStoreHandle.respond_to? :new) + +has_instance_method RStoreHandle, :create +has_instance_method RStoreHandle, :get +has_instance_method RStoreHandle, :retrieve +has_instance_method RStoreHandle, :delete +has_instance_method RStoreHandle, :update +has_instance_method RStoreHandle, :move_by_id +has_instance_method RStoreHandle, :save_as +has_instance_method RStoreHandle, :save_to + +has_instance_method RFileLockEntryHandle, :content +has_instance_method RFileLockEntryHandle, :content= +has_instance_method RFileLockEntryHandle, :header +has_instance_method RFileLockEntryHandle, :header= + +has_instance_method REntryHeader, :read +has_instance_method REntryHeader, :[] +has_instance_method REntryHeader, :set +has_instance_method REntryHeader, :[]= +has_instance_method REntryHeader, :insert + +works "REntryContent.superclass == String", (REntryContent.superclass == String) + diff --git a/libimagruby/test/test_store.rb b/libimagruby/test/test_store.rb new file mode 100644 index 0000000..aed0017 --- /dev/null +++ b/libimagruby/test/test_store.rb @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby + +require "../target/debug/liblibimagruby.so" + +color = true +verbose = true +debug = true + +RImag.init_logger debug, verbose, color + +store_handle = RStoreHandle::new(false, "/tmp/store") +id = RStoreId::new_baseless("test") +test_handle = store_handle.create(id) + +RImag.info "Created #{test_handle.location.to_str} from Ruby" + |