Class: SimpleData

Inherits:
Object
  • Object
show all
Defined in:
lib/simple-data.rb,
lib/simple-data/version.rb,
lib/simple-data/compression.rb

Defined Under Namespace

Classes: Error, IOCompressedRead, IOCompressedWrite, ParserError

Constant Summary collapse

FILE_VERSION =

Current file version

1
REGEX_MAGIC =

Various regex

/\A# simple-data:(?<version>\d+)\s*\z/
REGEX_SECTION =
/\A# --<(?<section>[^>:]+)(?::(?<extra>[^>]+))?>--+\s*\z/
REGEX_FIELD =
/\A\#\s*    (?<type>\w+      )       \s*:\s*
(?<name>[\w\-.:]+)
                            \s* (?:\[(?<unit>.*?     )\])?
                            \s* (?:\((?<desc>.*      )\))?   \s*\z
/ix
REGEX_TAG =
/\A@(?<tag>\w+)\s+(?<value>.*?)\s*\z/
REGEX_EMPTY =
/\A#\s*\z/
TAGS =

Supported tags / sections / types

%i(title summary author license url doi keywords)
SECTIONS =
%i(spec description data)
TYPES =
%i(i8 i16 i32 i64 u8 u16 u32 u64 f32 f64
cstr blob char bool)
VERSION =
'0.3.1'
MAGIC =

Magic numbers

{ "(\xB5/\xFD".force_encoding('BINARY') => :zstd
}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(io, fields, mode, version: FILE_VERSION, tags: {}, sections: {}) ⇒ SimpleData

Returns a new instance of SimpleData.



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/simple-data.rb', line 34

def initialize(io, fields, mode, version: FILE_VERSION,
               tags: {}, sections: {})
    @io         = io
    @mode       = mode
    @fields     = fields
    @fields_key = fields.map {|(_, name)| name }
    @tags       = tags
    @sections   = sections
    @version    = version

    @read_ok, @write_ok =
              case mode
              when :create, :append then [ false, true  ]
              when :read            then [ true,  false ]
              else raise Error,
                         'mode must be one of (:create, :append, or :read)'
              end
end

Instance Attribute Details

#fieldsObject (readonly)

Returns the value of attribute fields.



30
31
32
# File 'lib/simple-data.rb', line 30

def fields
  @fields
end

#sectionsObject (readonly)

Returns the value of attribute sections.



32
33
34
# File 'lib/simple-data.rb', line 32

def sections
  @sections
end

#tagsObject (readonly)

Returns the value of attribute tags.



31
32
33
# File 'lib/simple-data.rb', line 31

def tags
  @tags
end

#versionObject (readonly)

Attributes



29
30
31
# File 'lib/simple-data.rb', line 29

def version
  @version
end

Class Method Details

.each(file) ⇒ Object

Iterate over sda file



240
241
242
243
244
245
246
247
248
# File 'lib/simple-data.rb', line 240

def self.each(file)
    return to_enum(:each) unless block_given?

    self.open(file, :read) do |sda|
        while d = sda.get
            yield(d)
        end
    end
end

.generate(file, fields, compress: const_defined?(:IOCompressedWrite), tags: nil, sections: nil, &block) ⇒ Object

Generating file



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/simple-data.rb', line 133

def self.generate(file, fields,
                  compress: const_defined?(:IOCompressedWrite),
                  tags: nil, sections: nil, &block)
    # Sanity check
    if compress && !const_defined?(:IOCompressedWrite)
        raise Error, 'compression not supported (add zstd-ruby gem)'
    end

    # Open file
    io = File.open(file, 'w')

    # Magic string
    io.puts "# simple-data:1"

    # Tags
    if tags && !tags.empty?
        io.puts "#"
        tags.each do |name, value|
            io.puts "# @%-8s %s" % [ name, value ]
        end
    end

    # Spec
    io.puts "#"
    io.puts "# --<spec>--"
    name_maxlen = fields.map {|(_, name)| name.size }.max
    unit_maxlen = fields.map {|(_, _, unit)| unit&.size || 0 }.max
    
    fields.each do |(type, name, unit, desc)|
        if desc
            if unit
                io.puts "# %-4s : %-*s [%-*s] (%s)" % [
                            type, name_maxlen, name, unit_maxlen, unit, desc ]
            else
                io.puts "# %-4s : %-*s  %-*s  (%s)" % [
                            type, name_maxlen, name, unit_maxlen, '',   desc ]
            end
        else
            if unit
                io.puts "# %-4s : %-*s [%-*s]" % [
                            type, name_maxlen, name, unit_maxlen, unit, ]
            else
                io.puts "# %-4s : %s" % [ type, name ]
            end
        end
    end

    # Custom sections
    if sections && !sections.empty?
        sections.each do |name, value|
            io.puts "#"
            io.puts "# --<#{name}>--"
            value.split(/\r?\n/).each do |line|
                io.puts "# #{line}"
            end
        end
    end

    # Data 
    io.puts "#"
    io.puts "# --<%s>--" % [ compress ? 'data:compressed' : 'data' ]

    # Deal with compression
    io = IOCompressedWrite.new(io) if compress
    
    # Instantiate SimpleData
    sda = self.new(io, fields, :create, tags: tags, sections: sections)
    block ? block.call(sda) : sda
ensure
    sda.close if sda && block
end

.open(file, mode = :read, &block) ⇒ Object

Open file for reading



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/simple-data.rb', line 206

def self.open(file, mode = :read, &block)
    # Open file
    io = case mode
         when :read
             File.open(file, 'r:BINARY')
         when :append
             File.open(file, 'r+:BINARY').tap {|io|
                 io.seek(0, :END)
             }
         else raise ArgumentError,
                    "mode must be one of :read, :append"
         end

    # Read textual information
    version                         = self.get_magic(io)
    fields, tags, sections, dataopt = self.(io)

    # Deal with compression
    if dataopt&.include?(:compressed)
        unless const_defined?(:IOCompressedRead)
            raise Error, 'compression not supported (add zstd-ruby gem)'
        end
        io = IOCompressedRead.new(io)
    end

    # Instantiate SimpleData
    sda               = self.new(io, fields, mode, version: version,
                                 tags: tags, sections: sections)
    block ? block.call(sda) : sda
ensure
    sda.close if sda && block
end

Instance Method Details

#closeObject



128
129
130
# File 'lib/simple-data.rb', line 128

def close
    @io.close
end

#getObject

Raises:



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/simple-data.rb', line 98

def get
    # Checking mode
    raise Error, "read is not allowed in #{@mode} mode" unless @read_ok

    # No-op if end of file
    return if @io.eof?

    # Retrieve data
    @fields.map {|(type)|
        case type
        when :i8   then @io.read(1).unpack1('c' )
        when :i16  then @io.read(2).unpack1('s<')
        when :i32  then @io.read(4).unpack1('l<')
        when :i64  then @io.read(8).unpack1('q<')
        when :u8   then @io.read(1).unpack1('C' )
        when :u16  then @io.read(2).unpack1('S<')
        when :u32  then @io.read(4).unpack1('L<')
        when :u64  then @io.read(8).unpack1('q<')
        when :f32  then @io.read(4).unpack1('e' )
        when :f64  then @io.read(8).unpack1('E' )
        when :cstr then @io.each_byte.lazy.take_while {|b| !b.zero? }
                                          .map {|b| b.chr }.to_a.join
        when :blob then @io.read(2).unpack1('s<')
                           .then {|size| @io.read(size) }
        when :char then @io.read(1)
        when :bool then @io.read(1) == 'T'
        end
    }
end

#put(*data) ⇒ Object

Raises:



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/simple-data.rb', line 54

def put(*data)
    # Checking mode
    raise Error, "write is not allowed in #{@mode} mode" unless @write_ok


    if data.one? && (data.first.is_a?(Array) || data.first.is_a?(Hash))
        data = data.first
    end

    if data.size != @fields.size
        raise Error, 'dataset size doesn\'t match definition'
    end
            
    if data.is_a?(Hash)
        if ! (data.keys - @fields_key).empty?
            raise Error, 'dataset key mismatch'
        end

        data = @fields.map {|k| data[k] }
    end

    s = @fields.each.with_index.map {|(type,name), i| 
        d = data.fetch(i) { raise "missing data (#{name})" }
        case type
        when :i8   then [ d ].pack('c' )
        when :i16  then [ d ].pack('s<')
        when :i32  then [ d ].pack('l<')
        when :i64  then [ d ].pack('q<')
        when :u8   then [ d ].pack('C' )
        when :u16  then [ d ].pack('S<')
        when :u32  then [ d ].pack('L<')
        when :u64  then [ d ].pack('q<')
        when :f32  then [ d ].pack('e' )
        when :f64  then [ d ].pack('E' )
        when :cstr then [ d ].pack('Z*')
        when :blob then raise ParserError, 'not implemented'
        when :char then [ d ].pack('c' )
        when :bool then [ d ? 'T' : 'F' ]
        end
    }.join
    
    @io.write(s)
end