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.
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mechanomeld
4
+ class Error < StandardError; end
5
+ 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mechanomeld
4
+ VERSION = "0.1.0"
5
+ end
@@ -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"
@@ -0,0 +1,4 @@
1
+ module Mechanomeld
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
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: []