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* (?:\((?<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.1.0'
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.



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

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.



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

def fields
  @fields
end

#sectionsObject (readonly)

Returns the value of attribute sections.



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

def sections
  @sections
end

#tagsObject (readonly)

Returns the value of attribute tags.



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

def tags
  @tags
end

#versionObject (readonly)

Attributes



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

def version
  @version
end

Class Method Details

.generate(file, fields, compress = false, tags: nil, sections: nil, &block) ⇒ Object

Generating file



131
132
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
# File 'lib/simple-data.rb', line 131

def self.generate(file, fields, compress = false,
                  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>--"
    maxlen = fields.map {|(_, name)| name.size }.max
    fields.each do |(type, name, desc)|
        if desc
            io.puts "# %-4s : %-*s (%s)" % [ type, maxlen, name, desc ]
        else
            io.puts "# %-4s : %s" % [ type, name ]
        end
    end

    # Custom sections
    if sections && !sections.empty?
        io.puts "#"
        sections.each do |name, value|
            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



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/simple-data.rb', line 190

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



126
127
128
# File 'lib/simple-data.rb', line 126

def close
    @io.close
end

#getObject

Raises:



97
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
# File 'lib/simple-data.rb', line 97

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 raise ParserError, 'not implemented'
        when :char then @io.read(1)
        when :bool then @io.read(1) == 'T'
        end
    }
end

#put(*data) ⇒ Object

Raises:



53
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
# File 'lib/simple-data.rb', line 53

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' ].pack('c')
        end
    }.join
    
    @io.write(s)
end