mechanomeld 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +21 -0
- data/Cargo.lock +617 -0
- data/Cargo.toml +3 -0
- data/README.md +68 -0
- data/Rakefile +26 -0
- data/checksums/mechanomeld-0.1.0.gem.sha512 +1 -0
- data/ext/mechanomeld/Cargo.toml +13 -0
- data/ext/mechanomeld/extconf.rb +6 -0
- data/ext/mechanomeld/src/classes.rs +24 -0
- data/ext/mechanomeld/src/document.rs +176 -0
- data/ext/mechanomeld/src/errors.rs +24 -0
- data/ext/mechanomeld/src/lib.rs +27 -0
- data/ext/mechanomeld/src/path.rs +117 -0
- data/ext/mechanomeld/src/read.rs +80 -0
- data/ext/mechanomeld/src/write.rs +142 -0
- data/lib/mechanomeld/document.rb +38 -0
- data/lib/mechanomeld/error.rb +5 -0
- data/lib/mechanomeld/scalars.rb +49 -0
- data/lib/mechanomeld/version.rb +5 -0
- data/lib/mechanomeld.rb +15 -0
- data/sig/mechanomeld.rbs +4 -0
- metadata +80 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
//! Converting Ruby values into automerge operations.
|
|
2
|
+
|
|
3
|
+
use automerge::{transaction::Transactable, AutoCommit, ObjId, ObjType, Prop, ScalarValue};
|
|
4
|
+
use magnus::{
|
|
5
|
+
prelude::*,
|
|
6
|
+
r_hash::ForEach,
|
|
7
|
+
value::{Qfalse, Qtrue},
|
|
8
|
+
Error, Float, Integer, RArray, RHash, RObject, RString, Ruby, TryConvert, Value,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
use crate::classes::{BYTES, COUNTER, TEXT, TIMESTAMP, UINT};
|
|
12
|
+
use crate::errors::{arg_error, automerge_error, type_error};
|
|
13
|
+
use crate::path;
|
|
14
|
+
|
|
15
|
+
/// Where a value goes: over an existing prop, or inserted into a list.
|
|
16
|
+
pub enum Slot {
|
|
17
|
+
Put(Prop),
|
|
18
|
+
Insert(usize),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
enum Written {
|
|
22
|
+
Scalar(ScalarValue),
|
|
23
|
+
Map(RHash),
|
|
24
|
+
List(RArray),
|
|
25
|
+
Text(String),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
pub fn write(
|
|
29
|
+
ruby: &Ruby,
|
|
30
|
+
doc: &mut AutoCommit,
|
|
31
|
+
obj: &ObjId,
|
|
32
|
+
slot: Slot,
|
|
33
|
+
value: Value,
|
|
34
|
+
) -> Result<(), Error> {
|
|
35
|
+
match classify(ruby, value)? {
|
|
36
|
+
Written::Scalar(scalar) => match slot {
|
|
37
|
+
Slot::Put(prop) => doc.put(obj, prop, scalar),
|
|
38
|
+
Slot::Insert(index) => doc.insert(obj, index, scalar),
|
|
39
|
+
}
|
|
40
|
+
.map_err(|e| automerge_error(ruby, e)),
|
|
41
|
+
Written::Map(hash) => {
|
|
42
|
+
let child = create(ruby, doc, obj, slot, ObjType::Map)?;
|
|
43
|
+
hash.foreach(|key: Value, item: Value| {
|
|
44
|
+
let key = path::key(ruby, key)?;
|
|
45
|
+
write(ruby, doc, &child, Slot::Put(Prop::Map(key)), item)?;
|
|
46
|
+
Ok(ForEach::Continue)
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
Written::List(array) => {
|
|
50
|
+
let child = create(ruby, doc, obj, slot, ObjType::List)?;
|
|
51
|
+
for index in 0..array.len() {
|
|
52
|
+
let item = array.entry::<Value>(index as isize)?;
|
|
53
|
+
write(ruby, doc, &child, Slot::Insert(index), item)?;
|
|
54
|
+
}
|
|
55
|
+
Ok(())
|
|
56
|
+
}
|
|
57
|
+
Written::Text(text) => {
|
|
58
|
+
let child = create(ruby, doc, obj, slot, ObjType::Text)?;
|
|
59
|
+
doc.splice_text(&child, 0, 0, &text)
|
|
60
|
+
.map_err(|e| automerge_error(ruby, e))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fn create(
|
|
66
|
+
ruby: &Ruby,
|
|
67
|
+
doc: &mut AutoCommit,
|
|
68
|
+
obj: &ObjId,
|
|
69
|
+
slot: Slot,
|
|
70
|
+
obj_type: ObjType,
|
|
71
|
+
) -> Result<ObjId, Error> {
|
|
72
|
+
match slot {
|
|
73
|
+
Slot::Put(prop) => doc.put_object(obj, prop, obj_type),
|
|
74
|
+
Slot::Insert(index) => doc.insert_object(obj, index, obj_type),
|
|
75
|
+
}
|
|
76
|
+
.map_err(|e| automerge_error(ruby, e))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fn classify(ruby: &Ruby, value: Value) -> Result<Written, Error> {
|
|
80
|
+
if value.is_nil() {
|
|
81
|
+
return Ok(Written::Scalar(ScalarValue::Null));
|
|
82
|
+
}
|
|
83
|
+
if Qtrue::from_value(value).is_some() {
|
|
84
|
+
return Ok(Written::Scalar(ScalarValue::Boolean(true)));
|
|
85
|
+
}
|
|
86
|
+
if Qfalse::from_value(value).is_some() {
|
|
87
|
+
return Ok(Written::Scalar(ScalarValue::Boolean(false)));
|
|
88
|
+
}
|
|
89
|
+
if let Some(integer) = Integer::from_value(value) {
|
|
90
|
+
return Ok(Written::Scalar(ScalarValue::Int(integer.to_i64()?)));
|
|
91
|
+
}
|
|
92
|
+
if let Some(float) = Float::from_value(value) {
|
|
93
|
+
return Ok(Written::Scalar(ScalarValue::F64(float.to_f64())));
|
|
94
|
+
}
|
|
95
|
+
if let Some(string) = RString::from_value(value) {
|
|
96
|
+
return Ok(Written::Scalar(ScalarValue::Str(
|
|
97
|
+
utf8(ruby, string)?.into(),
|
|
98
|
+
)));
|
|
99
|
+
}
|
|
100
|
+
if let Some(hash) = RHash::from_value(value) {
|
|
101
|
+
return Ok(Written::Map(hash));
|
|
102
|
+
}
|
|
103
|
+
if let Some(array) = RArray::from_value(value) {
|
|
104
|
+
return Ok(Written::List(array));
|
|
105
|
+
}
|
|
106
|
+
if value.is_kind_of(ruby.get_inner(&TEXT)) {
|
|
107
|
+
return Ok(Written::Text(utf8(ruby, wrapped(value)?)?));
|
|
108
|
+
}
|
|
109
|
+
if value.is_kind_of(ruby.get_inner(&COUNTER)) {
|
|
110
|
+
let count = wrapped::<Integer>(value)?.to_i64()?;
|
|
111
|
+
return Ok(Written::Scalar(ScalarValue::counter(count)));
|
|
112
|
+
}
|
|
113
|
+
if value.is_kind_of(ruby.get_inner(&TIMESTAMP)) {
|
|
114
|
+
let millis = wrapped::<Integer>(value)?.to_i64()?;
|
|
115
|
+
return Ok(Written::Scalar(ScalarValue::Timestamp(millis)));
|
|
116
|
+
}
|
|
117
|
+
if value.is_kind_of(ruby.get_inner(&UINT)) {
|
|
118
|
+
let uint = wrapped::<Integer>(value)?.to_u64()?;
|
|
119
|
+
return Ok(Written::Scalar(ScalarValue::Uint(uint)));
|
|
120
|
+
}
|
|
121
|
+
if value.is_kind_of(ruby.get_inner(&BYTES)) {
|
|
122
|
+
let bytes = unsafe { wrapped::<RString>(value)?.as_slice() }.to_vec();
|
|
123
|
+
return Ok(Written::Scalar(ScalarValue::Bytes(bytes)));
|
|
124
|
+
}
|
|
125
|
+
Err(type_error(
|
|
126
|
+
ruby,
|
|
127
|
+
format!(
|
|
128
|
+
"unsupported Automerge value type: {}",
|
|
129
|
+
path::class_name(value)
|
|
130
|
+
),
|
|
131
|
+
))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// The `@value` of a scalar wrapper, read without calling Ruby methods.
|
|
135
|
+
fn wrapped<T: TryConvert>(value: Value) -> Result<T, Error> {
|
|
136
|
+
RObject::try_convert(value)?.ivar_get("@value")
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fn utf8(ruby: &Ruby, string: RString) -> Result<String, Error> {
|
|
140
|
+
let bytes = unsafe { string.as_slice() }.to_vec();
|
|
141
|
+
String::from_utf8(bytes).map_err(|_| arg_error(ruby, "string is not valid UTF-8"))
|
|
142
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mechanomeld
|
|
4
|
+
# Native methods (new, load, get, keys, length, put, delete, commit, rollback, save)
|
|
5
|
+
# are defined in ext/mechanomeld/src/document.rs.
|
|
6
|
+
class Document
|
|
7
|
+
def self.from(hash, actor_id: nil)
|
|
8
|
+
raise ArgumentError, "Automerge document root must be a Hash" unless hash.is_a?(Hash)
|
|
9
|
+
|
|
10
|
+
doc = new(actor_id: actor_id)
|
|
11
|
+
hash.each { |key, value| doc.put([key], value) }
|
|
12
|
+
doc.commit
|
|
13
|
+
doc
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def [](path)
|
|
17
|
+
get(path)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def []=(path, value)
|
|
21
|
+
put(path, value)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
get([])
|
|
26
|
+
end
|
|
27
|
+
alias_method :to_hash, :to_h
|
|
28
|
+
|
|
29
|
+
def change(message: nil, timestamp: nil)
|
|
30
|
+
yield self
|
|
31
|
+
commit(message: message, timestamp: timestamp)
|
|
32
|
+
self
|
|
33
|
+
rescue Exception
|
|
34
|
+
rollback
|
|
35
|
+
raise
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mechanomeld
|
|
4
|
+
# A typed Automerge value that has no natural Ruby equivalent.
|
|
5
|
+
class Scalar
|
|
6
|
+
attr_reader :value
|
|
7
|
+
|
|
8
|
+
def initialize(value)
|
|
9
|
+
@value = value
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def ==(other)
|
|
13
|
+
other.class == self.class && other.value == value
|
|
14
|
+
end
|
|
15
|
+
alias_method :eql?, :==
|
|
16
|
+
|
|
17
|
+
def hash
|
|
18
|
+
[self.class, value].hash
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def inspect
|
|
22
|
+
"#<#{self.class.name} #{value.inspect}>"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class Counter < Scalar; end
|
|
27
|
+
|
|
28
|
+
# Milliseconds since the Unix epoch.
|
|
29
|
+
class Timestamp < Scalar; end
|
|
30
|
+
|
|
31
|
+
class Uint < Scalar; end
|
|
32
|
+
|
|
33
|
+
class Bytes < Scalar
|
|
34
|
+
def initialize(value)
|
|
35
|
+
super(String(value).b)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# A collaborative text object, as opposed to a plain String value.
|
|
40
|
+
class Text < Scalar
|
|
41
|
+
def initialize(value = "")
|
|
42
|
+
super(String(value))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_s
|
|
46
|
+
value
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/mechanomeld.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "mechanomeld/version"
|
|
4
|
+
require_relative "mechanomeld/error"
|
|
5
|
+
require_relative "mechanomeld/scalars"
|
|
6
|
+
|
|
7
|
+
# Precompiled gems ship one binary per Ruby minor version; source builds put it in lib/mechanomeld.
|
|
8
|
+
begin
|
|
9
|
+
RUBY_VERSION =~ /(\d+\.\d+)/
|
|
10
|
+
require_relative "mechanomeld/#{Regexp.last_match(1)}/mechanomeld"
|
|
11
|
+
rescue LoadError
|
|
12
|
+
require_relative "mechanomeld/mechanomeld"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
require_relative "mechanomeld/document"
|
data/sig/mechanomeld.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mechanomeld
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Jim Gay
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rb_sys
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.9.130
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.9.130
|
|
26
|
+
description: A native extension wrapping the Rust automerge crate so Ruby can load,
|
|
27
|
+
read, create, and save Automerge documents shared with JavaScript.
|
|
28
|
+
email:
|
|
29
|
+
- jim@saturnflyer.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions:
|
|
32
|
+
- ext/mechanomeld/extconf.rb
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- CHANGELOG.md
|
|
36
|
+
- Cargo.lock
|
|
37
|
+
- Cargo.toml
|
|
38
|
+
- README.md
|
|
39
|
+
- Rakefile
|
|
40
|
+
- checksums/mechanomeld-0.1.0.gem.sha512
|
|
41
|
+
- ext/mechanomeld/Cargo.toml
|
|
42
|
+
- ext/mechanomeld/extconf.rb
|
|
43
|
+
- ext/mechanomeld/src/classes.rs
|
|
44
|
+
- ext/mechanomeld/src/document.rs
|
|
45
|
+
- ext/mechanomeld/src/errors.rs
|
|
46
|
+
- ext/mechanomeld/src/lib.rs
|
|
47
|
+
- ext/mechanomeld/src/path.rs
|
|
48
|
+
- ext/mechanomeld/src/read.rs
|
|
49
|
+
- ext/mechanomeld/src/write.rs
|
|
50
|
+
- lib/mechanomeld.rb
|
|
51
|
+
- lib/mechanomeld/document.rb
|
|
52
|
+
- lib/mechanomeld/error.rb
|
|
53
|
+
- lib/mechanomeld/scalars.rb
|
|
54
|
+
- lib/mechanomeld/version.rb
|
|
55
|
+
- sig/mechanomeld.rbs
|
|
56
|
+
homepage: https://github.com/SOFware/mechanomeld
|
|
57
|
+
licenses: []
|
|
58
|
+
metadata:
|
|
59
|
+
allowed_push_host: https://rubygems.org
|
|
60
|
+
homepage_uri: https://github.com/SOFware/mechanomeld
|
|
61
|
+
source_code_uri: https://github.com/SOFware/mechanomeld
|
|
62
|
+
changelog_uri: https://github.com/SOFware/mechanomeld/blob/main/CHANGELOG.md
|
|
63
|
+
rdoc_options: []
|
|
64
|
+
require_paths:
|
|
65
|
+
- lib
|
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: 3.2.0
|
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
requirements: []
|
|
77
|
+
rubygems_version: 4.0.16
|
|
78
|
+
specification_version: 4
|
|
79
|
+
summary: Read and write Automerge documents from Ruby
|
|
80
|
+
test_files: []
|