Merge pull request #128 from tildeio/consume-self

Allow consuming self in methods
This commit is contained in:
Godfrey Chan 2017-10-09 22:18:30 -07:00 committed by GitHub
commit c485529059
12 changed files with 551 additions and 85 deletions

View File

@ -21,7 +21,7 @@ appveyor = { repository = "tildeio/helix", branch = "master", service = "github"
[workspace]
members = ["examples/calculator", "examples/console", "examples/duration", "examples/membership", "examples/text_transform", "examples/turbo_blank"]
members = ["examples/calculator", "examples/console", "examples/duration", "examples/membership", "examples/text_transform", "examples/turbo_blank", "examples/json_builder"]
[dependencies]
libc = "0.2.0"

View File

@ -0,0 +1,14 @@
[package]
name = "json_builder"
version = "0.1.0"
authors = ["Godfrey Chan <godfrey@tilde.io>"]
[lib]
crate-type = ["cdylib"]
[dependencies.helix]
path = "../.."
[dependencies]
serde_json = "*"

View File

@ -0,0 +1,5 @@
source 'https://rubygems.org'
gem 'helix_runtime', path: '../../ruby'
gem 'rake', '~> 10.0'
gem 'rspec', '~> 3.4'

View File

@ -0,0 +1,39 @@
PATH
remote: ../../ruby
specs:
helix_runtime (0.7.1)
rake (>= 10.0)
thor (~> 0.19.4)
tomlrb (~> 1.2.4)
GEM
remote: https://rubygems.org/
specs:
diff-lcs (1.3)
rake (10.5.0)
rspec (3.5.0)
rspec-core (~> 3.5.0)
rspec-expectations (~> 3.5.0)
rspec-mocks (~> 3.5.0)
rspec-core (3.5.4)
rspec-support (~> 3.5.0)
rspec-expectations (3.5.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.5.0)
rspec-mocks (3.5.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.5.0)
rspec-support (3.5.0)
thor (0.19.4)
tomlrb (1.2.5)
PLATFORMS
ruby
DEPENDENCIES
helix_runtime!
rake (~> 10.0)
rspec (~> 3.4)
BUNDLED WITH
1.15.3

20
examples/json_builder/Rakefile Executable file
View File

@ -0,0 +1,20 @@
require 'bundler/setup'
require 'helix_runtime/build_task'
require 'rspec/core/rake_task'
require_relative '../shared.rb'
# For Windows
$stdout.sync = true
HelixRuntime::BuildTask.new do |t|
t.build_root = File.expand_path("../..", __dir__)
t.helix_lib_dir = File.expand_path("../../ruby/windows_build", __dir__)
t.pre_build = HelixRuntime::Tests.pre_build
end
RSpec::Core::RakeTask.new(:spec) do |t|
t.verbose = false
end
task :spec => :build
task :default => :spec

View File

@ -0,0 +1,2 @@
require 'helix_runtime'
require 'json_builder/native'

View File

@ -0,0 +1,88 @@
require "spec_helper"
require "json"
describe "JsonBuilder" do
let(:builder) { JsonBuilder.new }
let(:json) { JSON.parse(builder.to_json) }
it "can add null" do
builder["foo"] = nil
expect(json).to eq({ "foo" => nil })
end
it "can add booleans" do
builder["foo"] = true
builder["bar"] = false
expect(json).to eq({ "foo" => true, "bar" => false })
end
it "can add integers" do
builder["foo"] = 12345
builder["bar"] = -1_000_000
expect(json).to eq({ "foo" => 12345, "bar" => -1_000_000 })
end
it "can add floats" do
builder["foo"] = 1.2345
builder["bar"] = -1.0
expect(->{ builder["baz"] = Float::NAN }).to raise_error(TypeError)
expect(->{ builder["baz"] = Float::INFINITY }).to raise_error(TypeError)
expect(json).to eq({ "foo" => 1.2345, "bar" => -1.0 })
end
it "can add string" do
builder["foo"] = "FOO"
builder["bar"] = "BAR"
expect(json).to eq({ "foo" => "FOO", "bar" => "BAR" })
end
it "can add array" do
foo = builder["foo"] = [nil, true, 12345, 1.2345, "FOO"]
bar = builder["bar"] = [nil, false, -1_000_000, -1.0, "BAR"]
expect(json).to eq({ "foo" => foo, "bar" => bar })
end
it "can add hash" do
foo = builder["foo"] = { "nil" => nil, "true" => true, "12345" => 12345, "1.2345" => 1.2345, "FOO" => "FOO" }
bar = builder["bar"] = { "nil" => nil, "false" => false, "-1_000_000" => -1_000_000, "-1.0" => -1.0, "BAR" => "BAR" }
expect(json).to eq({ "foo" => foo, "bar" => bar })
end
it "can add nested builder" do
builder["foo"] = JsonBuilder.new.tap { |inner| inner["foo"] = "FOO" }
builder["bar"] = JsonBuilder.new.tap { |inner| inner["bar"] = "BAR" }
expect(json).to eq({ "foo" => { "foo" => "FOO" }, "bar" => { "bar" => "BAR" } })
end
it "can convert into a hash" do
builder["foo"] = "FOO"
builder["bar"] = nil
expect(builder.to_h).to eq({ "foo" => "FOO", "bar" => nil })
end
it "cannot be used once to_json is called" do
expect(builder.to_json).to eq("{}")
expect(->{ builder["foo"] = nil }).to raise_error(RuntimeError, "Uninitialized JsonBuilder")
expect(->{ builder.to_json }).to raise_error(RuntimeError, "Uninitialized JsonBuilder")
expect(->{ builder.to_h }).to raise_error(RuntimeError, "Uninitialized JsonBuilder")
end
it "cannot be used once to_h is called" do
expect(builder.to_h).to eq({})
expect(->{ builder["foo"] = nil }).to raise_error(RuntimeError, "Uninitialized JsonBuilder")
expect(->{ builder.to_json }).to raise_error(RuntimeError, "Uninitialized JsonBuilder")
expect(->{ builder.to_h }).to raise_error(RuntimeError, "Uninitialized JsonBuilder")
end
it "cannot be used once added to another builder" do
JsonBuilder.new["foo"] = builder
expect(->{ builder["foo"] = nil }).to raise_error(RuntimeError, "Uninitialized JsonBuilder")
expect(->{ builder.to_json }).to raise_error(RuntimeError, "Uninitialized JsonBuilder")
expect(->{ builder.to_h }).to raise_error(RuntimeError, "Uninitialized JsonBuilder")
end
end

View File

@ -0,0 +1,2 @@
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
require 'json_builder'

View File

@ -0,0 +1,108 @@
extern crate serde_json;
use super::{JsonValue, JsonBuilder};
use helix::{FromRuby, CheckResult, ToRuby, ToRubyResult};
use helix::sys::VALUE;
use std::collections::HashMap;
pub enum CheckedJsonValue {
Null,
Boolean(<bool as FromRuby>::Checked),
Integer(<i64 as FromRuby>::Checked),
Float(f64),
String(<String as FromRuby>::Checked),
Array(<Vec<JsonValue> as FromRuby>::Checked),
Object(<HashMap<String, JsonValue> as FromRuby>::Checked),
Nested(<JsonBuilder as FromRuby>::Checked)
}
impl FromRuby for JsonValue {
type Checked = CheckedJsonValue;
fn from_ruby(value: VALUE) -> CheckResult<CheckedJsonValue> {
if let Ok(_) = <()>::from_ruby(value) {
Ok(CheckedJsonValue::Null)
} else if let Ok(checked) = bool::from_ruby(value) {
Ok(CheckedJsonValue::Boolean(checked))
} else if let Ok(checked) = i64::from_ruby(value) {
Ok(CheckedJsonValue::Integer(checked))
} else if let Ok(checked) = f64::from_ruby(value) {
let float = f64::from_checked(checked);
if float.is_normal() {
Ok(CheckedJsonValue::Float(float))
} else {
type_error!(format!("Cannot convert {} into a JSON number", float))
}
} else if let Ok(checked) = String::from_ruby(value) {
Ok(CheckedJsonValue::String(checked))
} else if let Ok(checked) = Vec::<JsonValue>::from_ruby(value) {
Ok(CheckedJsonValue::Array(checked))
} else if let Ok(checked) = HashMap::<String, JsonValue>::from_ruby(value) {
Ok(CheckedJsonValue::Object(checked))
} else if let Ok(checked) = JsonBuilder::from_ruby(value) {
Ok(CheckedJsonValue::Nested(checked))
} else {
type_error!(value, "a JSON value")
}
}
fn from_checked(checked: CheckedJsonValue) -> JsonValue {
match checked {
CheckedJsonValue::Null => JsonValue::Null,
CheckedJsonValue::Boolean(c) => JsonValue::Boolean(FromRuby::from_checked(c)),
CheckedJsonValue::Integer(c) => JsonValue::Integer(FromRuby::from_checked(c)),
CheckedJsonValue::Float(c) => JsonValue::Float(c),
CheckedJsonValue::String(c) => JsonValue::String(FromRuby::from_checked(c)),
CheckedJsonValue::Array(c) => JsonValue::Array(FromRuby::from_checked(c)),
CheckedJsonValue::Object(c) => JsonValue::Object(FromRuby::from_checked(c)),
CheckedJsonValue::Nested(c) => JsonValue::Object(JsonBuilder::from_checked(c).to_hash_map())
}
}
}
impl ToRuby for JsonValue {
fn to_ruby(self) -> ToRubyResult {
match self {
JsonValue::Null => ().to_ruby(),
JsonValue::Boolean(v) => v.to_ruby(),
JsonValue::Integer(v) => v.to_ruby(),
JsonValue::Float(v) => v.to_ruby(),
JsonValue::String(v) => v.to_ruby(),
JsonValue::Array(v) => v.to_ruby(),
JsonValue::Object(v) => v.to_ruby(),
}
}
}
use serde_json::{Value, Number};
pub trait ToSerde {
fn to_serde(self) -> Value;
}
impl ToSerde for JsonValue {
fn to_serde(self) -> Value {
match self {
JsonValue::Null => Value::Null,
JsonValue::Boolean(v) => Value::Bool(v),
JsonValue::Integer(v) => Value::Number(Number::from(v)),
JsonValue::Float(v) => Value::Number(Number::from_f64(v).unwrap()),
JsonValue::String(v) => Value::String(v),
JsonValue::Array(v) => v.to_serde(),
JsonValue::Object(v) => v.to_serde(),
}
}
}
impl ToSerde for Vec<JsonValue> {
fn to_serde(self) -> Value {
Value::Array(self.into_iter().map(|v| v.to_serde()).collect())
}
}
impl ToSerde for HashMap<String, JsonValue> {
fn to_serde(self) -> Value {
Value::Object(self.into_iter().map(|(k,v)| (k, v.to_serde())).collect())
}
}

View File

@ -0,0 +1,49 @@
#![recursion_limit="1024"]
#[macro_use]
extern crate helix;
extern crate serde_json;
mod coercion;
use coercion::ToSerde;
use std::collections::HashMap;
use std::error::Error;
#[derive(Clone,Debug)]
pub enum JsonValue {
Null,
Boolean(bool),
Integer(i64),
Float(f64),
String(String),
Array(Vec<JsonValue>),
Object(HashMap<String, JsonValue>)
}
ruby! {
pub class JsonBuilder {
struct {
entries: HashMap<String, JsonValue>
}
def initialize(helix) {
JsonBuilder { helix, entries: HashMap::new() }
}
#[ruby_name="[]="]
def put(&mut self, key: String, value: JsonValue) {
self.entries.insert(key, value);
}
def to_json(self) -> Result<String, String> {
serde_json::to_string(&self.entries.to_serde())
.map_err(|e| e.description().to_string())
}
#[ruby_name="to_h"]
def to_hash_map(self) -> HashMap<String, JsonValue> {
self.entries
}
}
}

View File

@ -40,6 +40,32 @@ macro_rules! codegen_coercions {
struct: $struct:tt,
methods: $methods:tt
}) => (
impl $crate::FromRuby for $rust_name {
type Checked = Box<$rust_name>;
fn from_ruby(value: $crate::sys::VALUE) -> $crate::CheckResult<Box<$rust_name>> {
use $crate::{ToError, sys};
if unsafe { $rust_name != $crate::as_usize(sys::rb_obj_class(value)) } {
type_error!(value, stringify!($rust_name));
}
let ptr = unsafe { sys::Data_Get_Struct_Value(value) };
if ptr != ::std::ptr::null_mut() {
Ok(unsafe { ::std::mem::transmute(ptr) })
} else {
Err(format!("Uninitialized {}", stringify!($rust_name)).to_error())
}
}
fn from_checked(mut checked: Box<$rust_name>) -> $rust_name {
unsafe { $crate::sys::Data_Set_Struct_Value(checked.helix, ::std::ptr::null_mut()) };
checked.helix = unsafe { $crate::sys::Qnil };
*checked
}
}
impl_struct_to_rust!(&'a $rust_name, $rust_name);
impl_struct_to_rust!(&'a mut $rust_name, $rust_name);
@ -54,6 +80,34 @@ macro_rules! codegen_coercions {
);
}
#[macro_export]
macro_rules! impl_struct_to_rust {
($rust_name:ty, $helix_id:tt) => {
impl<'a> $crate::FromRuby for $rust_name {
type Checked = $rust_name;
fn from_ruby(value: $crate::sys::VALUE) -> $crate::CheckResult<$rust_name> {
use $crate::{ToError, sys};
if unsafe { $helix_id != $crate::as_usize(sys::rb_obj_class(value)) } {
type_error!(value, stringify!($helix_id));
}
let ptr = unsafe { sys::Data_Get_Struct_Value(value) };
if ptr != ::std::ptr::null_mut() {
Ok(unsafe { ::std::mem::transmute(ptr) })
} else {
Err(format!("Uninitialized {}", stringify!($helix_id)).to_error())
}
}
fn from_checked(checked: $rust_name) -> $rust_name {
checked
}
}
}
}
#[doc(hidden)]
#[macro_export]
@ -68,32 +122,3 @@ macro_rules! impl_to_ruby {
}
}
}
#[macro_export]
macro_rules! impl_struct_to_rust {
($rust_name:ty, $helix_id:tt) => {
impl<'a> $crate::FromRuby for $rust_name {
type Checked = $crate::CheckedValue<$rust_name>;
fn from_ruby(value: $crate::sys::VALUE) -> $crate::CheckResult<$crate::CheckedValue<$rust_name>> {
use $crate::{CheckedValue, sys};
use ::std::ffi::{CStr};
if unsafe { $helix_id == $crate::as_usize(sys::rb_obj_class(value)) } {
if unsafe { $crate::sys::Data_Get_Struct_Value(value) == ::std::ptr::null_mut() } {
type_error!(format!("Uninitialized {}", $crate::inspect(unsafe { sys::rb_obj_class(value) })))
} else {
Ok(unsafe { CheckedValue::new(value) })
}
} else {
let val = unsafe { CStr::from_ptr(sys::rb_obj_classname(value)).to_string_lossy() };
type_error!(format!("No implicit conversion of {} into {}", val, $crate::inspect(unsafe { sys::rb_obj_class(value) })))
}
}
fn from_checked(checked: $crate::CheckedValue<$rust_name>) -> $rust_name {
unsafe { ::std::mem::transmute($crate::sys::Data_Get_Struct_Value(checked.to_value())) }
}
}
}
}

View File

@ -328,7 +328,7 @@ macro_rules! parse {
assert_has_struct!($class, "Cannot define `initialize` without a `struct`");
parse! {
state: parse_arguments_helix,
state: parse_arguments_initialize,
buffer: { $($args)* },
stack: {
class_body: { $($rest)* },
@ -348,7 +348,7 @@ macro_rules! parse {
}
} => {
parse! {
state: parse_arguments_self,
state: parse_arguments,
buffer: { $($args)* },
stack: {
rust_name: $rust_name,
@ -359,16 +359,21 @@ macro_rules! parse {
}
};
// STATE: parse_arguments_helix
// STATE: parse_arguments_initialize
{
state: parse_arguments_helix,
buffer: { $helix_arg:tt $($rest:tt)* },
stack: { $($stack:tt)* }
state: parse_arguments_initialize,
buffer: { $helix_arg:tt, $($args:tt)+ },
stack: {
class_body: $class_body:tt,
$($stack:tt)*
}
} => {
assert_valid_helix_arg!($helix_arg);
parse! {
state: parse_arguments_consume_possible_comma,
buffer: { $($rest)* },
state: parse_return_type,
buffer: $class_body,
stack: {
method: {
type: initializer,
@ -378,7 +383,7 @@ macro_rules! parse {
ownership: { },
name: $helix_arg
},
args: uninitialized,
args: [ $($args)* ],
ret: uninitialized,
body: uninitialized
},
@ -387,22 +392,54 @@ macro_rules! parse {
}
};
// STATE: parse_arguments_self
{
state: parse_arguments_initialize,
buffer: { $helix_arg:tt },
stack: {
class_body: $class_body:tt,
$($stack:tt)*
}
} => {
assert_valid_helix_arg!($helix_arg);
parse! {
state: parse_return_type,
buffer: $class_body,
stack: {
method: {
type: initializer,
rust_name: initialize,
ruby_name: { "initialize" },
self: {
ownership: { },
name: $helix_arg
},
args: [ ],
ret: uninitialized,
body: uninitialized
},
$($stack)*
}
}
};
// STATE: parse_arguments
{
state: parse_arguments_self,
buffer: { &mut $self_arg:tt $($rest:tt)* },
state: parse_arguments,
buffer: { &mut $self_arg:tt, $($args:tt)+ },
stack: {
rust_name: $rust_name:tt,
ruby_name: $ruby_name:tt,
class_body: $class_body:tt,
$($stack:tt)*
}
} => {
assert_valid_self_arg!($self_arg);
parse! {
state: parse_arguments_consume_possible_comma,
buffer: { $($rest)* },
state: parse_return_type,
buffer: $class_body,
stack: {
method: {
type: instance_method,
@ -412,7 +449,7 @@ macro_rules! parse {
ownership: { &mut },
name: $self_arg
},
args: uninitialized,
args: [ $($args)* ],
ret: uninitialized,
body: uninitialized
},
@ -422,19 +459,53 @@ macro_rules! parse {
};
{
state: parse_arguments_self,
buffer: { & $self_arg:tt $($rest:tt)* },
state: parse_arguments,
buffer: { &mut $self_arg:tt },
stack: {
rust_name: $rust_name:tt,
ruby_name: $ruby_name:tt,
class_body: $class_body:tt,
$($stack:tt)*
}
} => {
assert_valid_self_arg!($self_arg);
parse! {
state: parse_arguments_consume_possible_comma,
buffer: { $($rest)* },
state: parse_return_type,
buffer: $class_body,
stack: {
method: {
type: instance_method,
rust_name: $rust_name,
ruby_name: $ruby_name,
self: {
ownership: { &mut },
name: $self_arg
},
args: [ ],
ret: uninitialized,
body: uninitialized
},
$($stack)*
}
}
};
{
state: parse_arguments,
buffer: { & $self_arg:tt, $($args:tt)+ },
stack: {
rust_name: $rust_name:tt,
ruby_name: $ruby_name:tt,
class_body: $class_body:tt,
$($stack:tt)*
}
} => {
assert_valid_self_arg!($self_arg);
parse! {
state: parse_return_type,
buffer: $class_body,
stack: {
method: {
type: instance_method,
@ -444,7 +515,7 @@ macro_rules! parse {
ownership: { & },
name: $self_arg
},
args: uninitialized,
args: [ $($args)* ],
ret: uninitialized,
body: uninitialized
},
@ -454,24 +525,30 @@ macro_rules! parse {
};
{
state: parse_arguments_self,
buffer: $buffer:tt,
state: parse_arguments,
buffer: { & $self_arg:tt },
stack: {
rust_name: $rust_name:tt,
ruby_name: $ruby_name:tt,
class_body: $class_body:tt,
$($stack:tt)*
}
} => {
assert_valid_self_arg!($self_arg);
parse! {
state: parse_arguments,
buffer: $buffer,
state: parse_return_type,
buffer: $class_body,
stack: {
method: {
type: class_method,
type: instance_method,
rust_name: $rust_name,
ruby_name: $ruby_name,
self: (),
args: uninitialized,
self: {
ownership: { & },
name: $self_arg
},
args: [ ],
ret: uninitialized,
body: uninitialized
},
@ -480,47 +557,78 @@ macro_rules! parse {
}
};
// STATE: parse_arguments_consume_possible_comma
{
state: parse_arguments_consume_possible_comma,
buffer: { , $($rest:tt)+ },
stack: $stack:tt
{
state: parse_arguments,
buffer: { $self_arg:tt, $($args:tt)+ },
stack: {
rust_name: $rust_name:tt,
ruby_name: $ruby_name:tt,
class_body: $class_body:tt,
$($stack:tt)*
}
} => {
assert_valid_self_arg!($self_arg);
parse! {
state: parse_arguments,
buffer: { $($rest)+ },
stack: $stack
state: parse_return_type,
buffer: $class_body,
stack: {
method: {
type: instance_method,
rust_name: $rust_name,
ruby_name: $ruby_name,
self: {
ownership: { },
name: $self_arg
},
args: [ $($args)* ],
ret: uninitialized,
body: uninitialized
},
$($stack)*
}
}
};
{
state: parse_arguments_consume_possible_comma,
buffer: { },
stack: $stack:tt
state: parse_arguments,
buffer: { $self_arg:tt },
stack: {
rust_name: $rust_name:tt,
ruby_name: $ruby_name:tt,
class_body: $class_body:tt,
$($stack:tt)*
}
} => {
assert_valid_self_arg!($self_arg);
parse! {
state: parse_arguments,
buffer: { },
stack: $stack
state: parse_return_type,
buffer: $class_body,
stack: {
method: {
type: instance_method,
rust_name: $rust_name,
ruby_name: $ruby_name,
self: {
ownership: { },
name: $self_arg
},
args: [ ],
ret: uninitialized,
body: uninitialized
},
$($stack)*
}
}
};
// STATE: parse_arguments
{
state: parse_arguments,
buffer: { $($args:tt)* },
stack: {
method: {
type: $type:tt,
rust_name: $rust_name:tt,
ruby_name: $ruby_name:tt,
self: $self:tt,
args: uninitialized,
ret: uninitialized,
body: uninitialized
},
rust_name: $rust_name:tt,
ruby_name: $ruby_name:tt,
class_body: $class_body:tt,
$($stack:tt)*
}
@ -530,10 +638,10 @@ macro_rules! parse {
buffer: $class_body,
stack: {
method: {
type: $type,
type: class_method,
rust_name: $rust_name,
ruby_name: $ruby_name,
self: $self,
self: (),
args: [ $($args)* ],
ret: uninitialized,
body: uninitialized
@ -774,6 +882,12 @@ macro_rules! assert_has_struct {
{ { struct: $struct:tt }, $($message:expr),* } => {};
}
#[doc(hidden)]
#[macro_export]
macro_rules! assert_valid_helix_arg {
(helix) => {};
}
#[doc(hidden)]
#[macro_export]
macro_rules! assert_valid_self_arg {