c0data 0.3.1
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/LICENSE +21 -0
- data/README.md +143 -0
- data/c0-c/c0.h +3926 -0
- data/ext/c0/c0_ext.c +1061 -0
- data/ext/c0/extconf.rb +7 -0
- data/lib/c0/body.rb +314 -0
- data/lib/c0/builder.rb +64 -0
- data/lib/c0/document.rb +243 -0
- data/lib/c0/pretty.rb +28 -0
- data/lib/c0/stream.rb +76 -0
- data/lib/c0.rb +123 -0
- metadata +53 -0
data/lib/c0/stream.rb
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module C0
|
|
4
|
+
# Stream mode: ETB-committed append logs (DESIGN.md "Stream Mode").
|
|
5
|
+
#
|
|
6
|
+
# r = C0::Stream::Reader.new(File.binread("events.c0"))
|
|
7
|
+
# r.torn? # => data trails the last commit
|
|
8
|
+
# r.each_record { |rec| ... }
|
|
9
|
+
module Stream
|
|
10
|
+
# Scans a buffer for commits. An unescaped ETB is always a commit, even
|
|
11
|
+
# inside open text or a level; such a block is damaged and yields nothing.
|
|
12
|
+
# The tail after the last commit is torn unless it holds only layout,
|
|
13
|
+
# notes, EOT or EM. Enumerates the committed blocks' bytes.
|
|
14
|
+
class Reader
|
|
15
|
+
include Enumerable
|
|
16
|
+
|
|
17
|
+
attr_reader :buf, :committed_end
|
|
18
|
+
|
|
19
|
+
def initialize(buf)
|
|
20
|
+
@buf = C0.to_bin(buf)
|
|
21
|
+
@committed_end, @torn, @blocks = Ext.stream(@buf)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.read(path) = new(File.binread(path))
|
|
25
|
+
|
|
26
|
+
def torn? = @torn
|
|
27
|
+
|
|
28
|
+
# The bytes up to and including the last commit.
|
|
29
|
+
def committed = @buf.byteslice(0, @committed_end)
|
|
30
|
+
|
|
31
|
+
# The bytes after the last commit.
|
|
32
|
+
def tail = @buf.byteslice(@committed_end, @buf.bytesize - @committed_end)
|
|
33
|
+
|
|
34
|
+
def block_count = @blocks.size
|
|
35
|
+
|
|
36
|
+
alias size block_count
|
|
37
|
+
|
|
38
|
+
# Block i's bytes (previous commit to ETB, marker and payload excluded).
|
|
39
|
+
def block(index)
|
|
40
|
+
s, e, = @blocks[index]
|
|
41
|
+
s && @buf.byteslice(s, e - s)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def blocks = @blocks.map { |s, e, _| @buf.byteslice(s, e - s) }
|
|
45
|
+
|
|
46
|
+
def each_block(&) = blocks.each(&)
|
|
47
|
+
|
|
48
|
+
alias each each_block
|
|
49
|
+
|
|
50
|
+
# Indexes of the damaged blocks (cut by an ETB inside open text or a level).
|
|
51
|
+
def damaged = @blocks.each_index.select { |i| @blocks[i][2] }
|
|
52
|
+
|
|
53
|
+
def damaged?(index) = @blocks[index] ? @blocks[index][2] : false
|
|
54
|
+
|
|
55
|
+
# The committed region as a document, undamaged blocks only.
|
|
56
|
+
def document = Document.new(Ext.stream_undamaged(@buf))
|
|
57
|
+
|
|
58
|
+
def body = document.body
|
|
59
|
+
|
|
60
|
+
# The committed records as a group (the first group, or an empty one).
|
|
61
|
+
def table
|
|
62
|
+
b = body
|
|
63
|
+
b.groups.first || Group.new("".b, 0, 0, 0, 0, 0)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Every committed record, across all groups.
|
|
67
|
+
def records = body.groups.flat_map(&:records)
|
|
68
|
+
|
|
69
|
+
def each_record(&) = records.each(&)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Position just past the last commit, found by a backward scan: a
|
|
73
|
+
# shortcut for well-formed input (Reader is authoritative).
|
|
74
|
+
def self.last_commit_end(buf) = Ext.last_commit_end(C0.to_bin(buf))
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/c0.rb
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# C0DATA 1.0 — structured data using ASCII C0 control codes.
|
|
4
|
+
#
|
|
5
|
+
# A thin Ruby layer over the c0 C core (the C0::Ext extension). The read path
|
|
6
|
+
# is zero-copy: accessors return shared byteslices into the input buffer.
|
|
7
|
+
#
|
|
8
|
+
# buf = C0.build do |b|
|
|
9
|
+
# b.syn(shape: "tabular")
|
|
10
|
+
# b.group("users", %w[name amount]) do
|
|
11
|
+
# b.record("Alice", "1502.30")
|
|
12
|
+
# b.record("Bob", "340.00")
|
|
13
|
+
# end
|
|
14
|
+
# b.eot
|
|
15
|
+
# end
|
|
16
|
+
# doc = C0::Document.new(buf)
|
|
17
|
+
# doc["users"].record(0).value(0) # => "Alice"
|
|
18
|
+
#
|
|
19
|
+
# Reading is a two-step contract, as in the C core: `C0.validate` checks a
|
|
20
|
+
# buffer against the grammar once; the navigators (Document, Body, Group,
|
|
21
|
+
# Record) then assume well-formed input and raise only on the malformations
|
|
22
|
+
# they happen to meet.
|
|
23
|
+
|
|
24
|
+
begin
|
|
25
|
+
require "c0/c0_ext"
|
|
26
|
+
rescue LoadError
|
|
27
|
+
require_relative "../ext/c0/c0_ext"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
module C0
|
|
31
|
+
# Assigned C0 control codes (DESIGN.md "Assigned Control Codes").
|
|
32
|
+
SOH = 0x01 # Heading: a key for the value after it, or SOH RS a header row
|
|
33
|
+
STX = 0x02 # Start of text (a literal value)
|
|
34
|
+
ETX = 0x03 # End of text
|
|
35
|
+
EOT = 0x04 # End of document
|
|
36
|
+
ENQ = 0x05 # Reference
|
|
37
|
+
BEL = 0x07 # Comment: a note, or marks the next element dead
|
|
38
|
+
DLE = 0x10 # Recurse: opens a nested level (outside text); escape (inside text)
|
|
39
|
+
SYN = 0x16 # Start of document, then the heading
|
|
40
|
+
ETB = 0x17 # Commit marker (stream mode)
|
|
41
|
+
CAN = 0x18 # Close a nested level
|
|
42
|
+
EM = 0x19 # End of medium: a raw blob follows
|
|
43
|
+
SUB = 0x1a # Substitution (C0-DIFF only)
|
|
44
|
+
FS = 0x1c # File separator
|
|
45
|
+
GS = 0x1d # Group separator
|
|
46
|
+
RS = 0x1e # Record separator
|
|
47
|
+
US = 0x1f # Unit separator
|
|
48
|
+
|
|
49
|
+
# Layout codes outside text.
|
|
50
|
+
HT = 0x09
|
|
51
|
+
LF = 0x0a
|
|
52
|
+
CR = 0x0d
|
|
53
|
+
SP = 0x20
|
|
54
|
+
|
|
55
|
+
# What a raw unit holds, by its first non-layout byte (matches the C enum).
|
|
56
|
+
module UnitType
|
|
57
|
+
PLAIN = 0
|
|
58
|
+
TEXT = 1
|
|
59
|
+
LEVEL = 2
|
|
60
|
+
REF = 3
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
module_function
|
|
64
|
+
|
|
65
|
+
# Coerce to a binary (ASCII-8BIT) String without copying when already binary.
|
|
66
|
+
def to_bin(str)
|
|
67
|
+
str = str.to_s unless str.is_a?(String)
|
|
68
|
+
str.encoding == Encoding::BINARY ? str : str.b
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Whether a byte is one of the sixteen assigned codes.
|
|
72
|
+
def assigned?(byte) = Ext.is_assigned(byte)
|
|
73
|
+
|
|
74
|
+
# Whether a byte is layout outside text (SP, HT, LF, CR).
|
|
75
|
+
def layout?(byte) = Ext.is_layout(byte)
|
|
76
|
+
|
|
77
|
+
# Whether a byte is a control code (below 0x20).
|
|
78
|
+
def control?(byte) = byte < 0x20
|
|
79
|
+
|
|
80
|
+
# Whether a value can be written plain: no byte below 0x20, no edge whitespace.
|
|
81
|
+
def plain?(value) = Ext.is_plain(to_bin(value))
|
|
82
|
+
|
|
83
|
+
# Whether bytes are a legal name (label, key, header name): the same test as plain.
|
|
84
|
+
def name?(value) = Ext.is_plain(to_bin(value))
|
|
85
|
+
|
|
86
|
+
# Layout trimmed from both ends of a raw slice.
|
|
87
|
+
def trim(raw) = Ext.trim(to_bin(raw))
|
|
88
|
+
|
|
89
|
+
# Check a whole buffer against the grammar. Raises a C0::SyntaxError (or a
|
|
90
|
+
# subclass) on the first error; returns nil when well-formed.
|
|
91
|
+
def validate(buf)
|
|
92
|
+
Ext.validate(to_bin(buf))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Whether bytes are a canonical unit: well-formed, no layout, one spelling
|
|
96
|
+
# per value, unique keys in each keyed record, no framing, no comments.
|
|
97
|
+
def canonical?(buf) = Ext.canonical(to_bin(buf))
|
|
98
|
+
|
|
99
|
+
# The logical value of a raw unit: a plain value trimmed, or the inside of a
|
|
100
|
+
# text with its escapes decoded. Raises C0::Error on a level or reference.
|
|
101
|
+
def value(raw)
|
|
102
|
+
b = to_bin(raw)
|
|
103
|
+
Ext.value(b, 0, b.bytesize)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Decode the inside of a text (the bytes between STX and ETX).
|
|
107
|
+
def decode_text(inside) = Ext.decode_text(to_bin(inside))
|
|
108
|
+
|
|
109
|
+
# Build a buffer by driving a fresh Builder, returning its bytes.
|
|
110
|
+
def build(&) = Builder.build(&)
|
|
111
|
+
|
|
112
|
+
# Pretty form (Unicode Control Pictures) of compact bytes.
|
|
113
|
+
def pretty_format(buf, indent: " ", mode: :compact) = Pretty.format(buf, indent: indent, mode: mode)
|
|
114
|
+
|
|
115
|
+
# Compact bytes from pretty form.
|
|
116
|
+
def pretty_parse(text) = Pretty.parse(text)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
require_relative "c0/body"
|
|
120
|
+
require_relative "c0/document"
|
|
121
|
+
require_relative "c0/builder"
|
|
122
|
+
require_relative "c0/stream"
|
|
123
|
+
require_relative "c0/pretty"
|
metadata
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: c0data
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.3.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Thomas Sawyer
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: A thin, fast Ruby binding over the c0 C core. The read path is zero-copy;
|
|
13
|
+
the scan-heavy work runs as native C.
|
|
14
|
+
email:
|
|
15
|
+
- transfire@gmail.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions:
|
|
18
|
+
- ext/c0/extconf.rb
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- LICENSE
|
|
22
|
+
- README.md
|
|
23
|
+
- c0-c/c0.h
|
|
24
|
+
- ext/c0/c0_ext.c
|
|
25
|
+
- ext/c0/extconf.rb
|
|
26
|
+
- lib/c0.rb
|
|
27
|
+
- lib/c0/body.rb
|
|
28
|
+
- lib/c0/builder.rb
|
|
29
|
+
- lib/c0/document.rb
|
|
30
|
+
- lib/c0/pretty.rb
|
|
31
|
+
- lib/c0/stream.rb
|
|
32
|
+
homepage: https://github.com/c0data/c0-rb
|
|
33
|
+
licenses:
|
|
34
|
+
- MIT
|
|
35
|
+
metadata: {}
|
|
36
|
+
rdoc_options: []
|
|
37
|
+
require_paths:
|
|
38
|
+
- lib
|
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '3.0'
|
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '0'
|
|
49
|
+
requirements: []
|
|
50
|
+
rubygems_version: 3.6.9
|
|
51
|
+
specification_version: 4
|
|
52
|
+
summary: C0DATA — structured data using ASCII C0 control codes
|
|
53
|
+
test_files: []
|