record 1.1.0 → 1.1.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 +4 -4
- data/Manifest.txt +6 -0
- data/lib/record/base.rb +172 -0
- data/lib/record/builder.rb +37 -0
- data/lib/record/field.rb +80 -0
- data/lib/record/version.rb +1 -1
- data/test/helper.rb +9 -0
- data/test/test_record.rb +165 -0
- data/test/test_version.rb +20 -0
- metadata +7 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 905b06c2d1e681e1ae4a2f99b31e1293ab8d455d
|
4
|
+
data.tar.gz: 9a2876f8c7ca014181d75a1e0e02052b17c1546f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a6a98f18f4b684db22b4099ccd0bf2a962d7cde44216facd03de97733000ddff62120709ab3766b85fadef58d5e60b88871a278282f9d7a3451cf6432a4edf67
|
7
|
+
data.tar.gz: c48238cbdedd1f50f36aad3ecbc5e9b437af5fd8f356bda68e1d16f5182c5d5af2d1d7866eb0d9a6d04832aff459fec1a133cf2af1a6af746a093aaab30ec485
|
data/Manifest.txt
CHANGED
data/lib/record/base.rb
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Record
|
4
|
+
|
5
|
+
class Base
|
6
|
+
|
7
|
+
def self.fields ## note: use class instance variable (@fields and NOT @@fields)!!!! (derived classes get its own copy!!!)
|
8
|
+
@fields ||= []
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.field_names ## rename to header - why? why not?
|
12
|
+
## return header row, that is, all field names in an array
|
13
|
+
## todo: rename to field_names or just names - why? why not?
|
14
|
+
## note: names are (always) symbols!!!
|
15
|
+
fields.map {|field| field.name }
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.field_types
|
19
|
+
## note: types are (always) classes!!!
|
20
|
+
fields.map {|field| field.type }
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
def self.field( name, type=:string )
|
26
|
+
num = fields.size ## auto-calc num(ber) / position index - always gets added at the end
|
27
|
+
field = Field.new( name, num, type )
|
28
|
+
fields << field
|
29
|
+
|
30
|
+
define_field( field ) ## auto-add getter,setter,parse/typecast
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.define_field( field )
|
34
|
+
name = field.name ## note: always assumes a "cleaned-up" (symbol) name
|
35
|
+
num = field.num
|
36
|
+
|
37
|
+
define_method( name ) do
|
38
|
+
instance_variable_get( "@values" )[num]
|
39
|
+
end
|
40
|
+
|
41
|
+
define_method( "#{name}=" ) do |value|
|
42
|
+
instance_variable_get( "@values" )[num] = value
|
43
|
+
end
|
44
|
+
|
45
|
+
define_method( "parse_#{name}") do |value|
|
46
|
+
instance_variable_get( "@values" )[num] = field.typecast( value )
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
## column/columns aliases for field/fields
|
51
|
+
## use self << with alias_method - possible? works? why? why not?
|
52
|
+
def self.column( name, type=:string ) field( name, type ); end
|
53
|
+
def self.columns() fields; end
|
54
|
+
def self.column_names() field_names; end
|
55
|
+
def self.column_types() field_types; end
|
56
|
+
|
57
|
+
|
58
|
+
|
59
|
+
|
60
|
+
def self.build_hash( values ) ## find a better name - build_attrib? or something?
|
61
|
+
## convert to key-value (attribute) pairs
|
62
|
+
## puts "== build_hash:"
|
63
|
+
## pp values
|
64
|
+
|
65
|
+
## e.g. [[],[]] return zipped pairs in array as (attribute - name/value pair) hash
|
66
|
+
Hash[ field_names.zip(values) ]
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
|
71
|
+
def self.typecast( new_values )
|
72
|
+
values = []
|
73
|
+
|
74
|
+
##
|
75
|
+
## todo: check that new_values.size <= fields.size
|
76
|
+
##
|
77
|
+
## fields without values will get auto-filled with nils (or default field values?)
|
78
|
+
|
79
|
+
##
|
80
|
+
## use fields.zip( new_values ).map |field,value| ... instead - why? why not?
|
81
|
+
fields.each_with_index do |field,i|
|
82
|
+
value = new_values[i] ## note: returns nil if new_values.size < fields.size
|
83
|
+
values << field.typecast( value )
|
84
|
+
end
|
85
|
+
values
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
def parse( new_values ) ## use read (from array) or read_values or read_row - why? why not?
|
90
|
+
|
91
|
+
## todo: check if values overshadowing values attrib is ok (without warning?) - use just new_values (not values)
|
92
|
+
@values = self.class.typecast( new_values )
|
93
|
+
self ## return self for chaining
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
def values
|
98
|
+
## return array of all record values (typed e.g. float, integer, date, ..., that is,
|
99
|
+
## as-is and NOT auto-converted to string
|
100
|
+
@values
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
|
105
|
+
def [](key)
|
106
|
+
if key.is_a? Integer
|
107
|
+
@values[ key ]
|
108
|
+
elsif key.is_a? Symbol
|
109
|
+
## try attribute access
|
110
|
+
send( key )
|
111
|
+
else ## assume string
|
112
|
+
## downcase and symbol-ize
|
113
|
+
## remove spaces too -why? why not?
|
114
|
+
## todo/fix: add a lookup mapping for (string) titles (Team 1, etc.)
|
115
|
+
send( key.downcase.to_sym )
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
|
121
|
+
def to_h ## use to_hash - why? why not? - add attributes alias - why? why not?
|
122
|
+
self.class.build_hash( @values )
|
123
|
+
end
|
124
|
+
|
125
|
+
def initialize( **kwargs )
|
126
|
+
@values = []
|
127
|
+
update( kwargs )
|
128
|
+
end
|
129
|
+
|
130
|
+
def update( **kwargs )
|
131
|
+
pp kwargs
|
132
|
+
kwargs.each do |name,value|
|
133
|
+
## note: only convert/typecast string values
|
134
|
+
if value.is_a?( String )
|
135
|
+
send( "parse_#{name}", value ) ## note: use parse_<name> setter (for typecasting)
|
136
|
+
else ## use "regular" plain/classic attribute setter
|
137
|
+
send( "#{name}=", value )
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
## todo: check if args.first is an array (init/update from array)
|
142
|
+
self ## return self for chaining
|
143
|
+
end
|
144
|
+
end ## class Base
|
145
|
+
|
146
|
+
|
147
|
+
|
148
|
+
#########
|
149
|
+
# alternative class (record) builder
|
150
|
+
|
151
|
+
def self.define( super_class=Base, &block ) ## check: rename super_class to base - why? why not?
|
152
|
+
builder = Builder.new( super_class )
|
153
|
+
if block.arity == 1
|
154
|
+
block.call( builder )
|
155
|
+
## e.g. allows "yield" dsl style e.g.
|
156
|
+
## Record.define do |rec|
|
157
|
+
## rec.string :team1
|
158
|
+
## rec.string :team2
|
159
|
+
## end
|
160
|
+
##
|
161
|
+
else
|
162
|
+
builder.instance_eval( &block )
|
163
|
+
## e.g. shorter "instance eval" dsl style e.g.
|
164
|
+
## Record.define do
|
165
|
+
## string :team1
|
166
|
+
## string :team2
|
167
|
+
## end
|
168
|
+
end
|
169
|
+
builder.to_record
|
170
|
+
end
|
171
|
+
|
172
|
+
end # module Record
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
## (record) builder mini language / domain-specific language (dsl)
|
4
|
+
|
5
|
+
module Record
|
6
|
+
|
7
|
+
class Builder # check: rename to RecordDefinition or RecordDsl or similar - why? why not?
|
8
|
+
def initialize( super_class=Base )
|
9
|
+
@clazz = Class.new( super_class )
|
10
|
+
end
|
11
|
+
|
12
|
+
def field( name, type=:string ) ## note: type defaults to string
|
13
|
+
puts " adding field >#{name}< with type >#{type}<"
|
14
|
+
@clazz.field( name, type ) ## auto-add getter and setter
|
15
|
+
end
|
16
|
+
|
17
|
+
def string( name )
|
18
|
+
puts " adding string field >#{name}<"
|
19
|
+
field( name, :string )
|
20
|
+
end
|
21
|
+
|
22
|
+
def integer( name ) ## use number for alias for integer - why? why not???
|
23
|
+
puts " adding integer number field >#{name}<"
|
24
|
+
field( name, :integer )
|
25
|
+
end
|
26
|
+
|
27
|
+
def float( name )
|
28
|
+
puts " adding float number field >#{name}<"
|
29
|
+
field( name, :float )
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def to_record ## check: rename to just record or obj or finish or end something?
|
34
|
+
@clazz
|
35
|
+
end
|
36
|
+
end # class Builder
|
37
|
+
end # module Record
|
data/lib/record/field.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
|
4
|
+
module Record
|
5
|
+
|
6
|
+
## note on naming:
|
7
|
+
## use naming convention from awk and tabular data package/json schema for now
|
8
|
+
## use - records (use rows for "raw" untyped (string) data rows )
|
9
|
+
## - fields (NOT columns or attributes) -- might add an alias later - why? why not?
|
10
|
+
|
11
|
+
class Field ## ruby record class field
|
12
|
+
attr_reader :name, :type
|
13
|
+
|
14
|
+
## zero-based position index (0,1,2,3,...)
|
15
|
+
## todo: use/rename to index/idx/pos - add an alias name - why?
|
16
|
+
attr_reader :num
|
17
|
+
|
18
|
+
def initialize( name, num, type )
|
19
|
+
## note: always symbol-ify (to_sym) name and type
|
20
|
+
|
21
|
+
## todo: add title or titles for header field/column title as is e.g. 'Team 1' etc.
|
22
|
+
## incl. numbers only or even an empty header title
|
23
|
+
@name = name.to_sym
|
24
|
+
@num = num
|
25
|
+
|
26
|
+
if type.is_a?( Class )
|
27
|
+
@type = type ## assign class to its own property - why? why not?
|
28
|
+
else
|
29
|
+
@type = Type.registry[type.to_sym]
|
30
|
+
if @type.nil?
|
31
|
+
puts "!!!! warn unknown type >#{type}< - no class mapping found; add missing type to Record::Type.registry[]"
|
32
|
+
## todo/fix: raise exception!!!!
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
def typecast( value ) ## cast (convert) from string value to type (e.g. float, integer, etc.)
|
39
|
+
## todo: add typecast to class itself (monkey patch String/Float etc. - why? why not)
|
40
|
+
## use __typecast or something?
|
41
|
+
## or use a new "wrapper" class Type::String or StringType - why? why not?
|
42
|
+
|
43
|
+
## convert string value to (field) type
|
44
|
+
if @type == String
|
45
|
+
value ## pass through as is
|
46
|
+
elsif @type == Float
|
47
|
+
## note: allow/check for nil values - why? why not?
|
48
|
+
float = (value.nil? || value.empty?) ? nil : value.to_f
|
49
|
+
puts "typecast >#{value}< to float number >#{float}<"
|
50
|
+
float
|
51
|
+
elsif @type == Integer
|
52
|
+
number = (value.nil? || value.empty?) ? nil : value.to_i(10) ## always use base10 for now (e.g. 010 => 10 etc.)
|
53
|
+
puts "typecast >#{value}< to integer number >#{number}<"
|
54
|
+
number
|
55
|
+
else
|
56
|
+
## todo/fix: raise exception about unknow type
|
57
|
+
pp @type
|
58
|
+
puts "!!!! unknown type >#{@type}< - don't know how to convert/typecast string value >#{value}<"
|
59
|
+
value
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end # class Field
|
63
|
+
|
64
|
+
|
65
|
+
|
66
|
+
class Type ## todo: use a module - it's just a namespace/module now - why? why not?
|
67
|
+
|
68
|
+
## e.g. use Type.registry[:string] = String etc.
|
69
|
+
## note use @@ - there is only one registry
|
70
|
+
def self.registry() @@registry ||={} end
|
71
|
+
|
72
|
+
## add built-in types:
|
73
|
+
registry[:string] = String
|
74
|
+
registry[:integer] = Integer ## todo/check: add :number alias for integer? why? why not?
|
75
|
+
registry[:float] = Float
|
76
|
+
## todo: add some more
|
77
|
+
end # class Type
|
78
|
+
|
79
|
+
|
80
|
+
end # module Record
|
data/lib/record/version.rb
CHANGED
data/test/helper.rb
ADDED
data/test/test_record.rb
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
###
|
4
|
+
# to run use
|
5
|
+
# ruby -I ./lib -I ./test test/test_record.rb
|
6
|
+
|
7
|
+
|
8
|
+
require 'helper'
|
9
|
+
|
10
|
+
class TestRecord < MiniTest::Test
|
11
|
+
|
12
|
+
def test_class_style1
|
13
|
+
clazz1 = Record.define do
|
14
|
+
field :brewery, :string # fix: do NOT use 'Brewery' - name SHOULD be a valid variable name
|
15
|
+
field :city, :string
|
16
|
+
field :name ## default type is :string
|
17
|
+
field :abv, Float ## allow type specified as class
|
18
|
+
end
|
19
|
+
pp clazz1
|
20
|
+
pp clazz1.fields
|
21
|
+
|
22
|
+
assert true ## assume ok if we get here
|
23
|
+
end
|
24
|
+
|
25
|
+
Beer = Record.define do
|
26
|
+
string :brewery # fix: do NOT use 'Brewery' - name SHOULD be a valid variable name
|
27
|
+
string :city
|
28
|
+
string :name
|
29
|
+
float :abv
|
30
|
+
end
|
31
|
+
|
32
|
+
class BeerClassic < Record::Base
|
33
|
+
field :brewery
|
34
|
+
field :city
|
35
|
+
field :name
|
36
|
+
field :abv, Float
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
def test_class_style2
|
41
|
+
clazz2 = Record.define do
|
42
|
+
string :brewery # fix: do NOT use 'Brewery' - name SHOULD be a valid variable name
|
43
|
+
string :city
|
44
|
+
string :name
|
45
|
+
float :abv
|
46
|
+
end
|
47
|
+
pp clazz2
|
48
|
+
pp clazz2.class.name
|
49
|
+
pp clazz2.fields
|
50
|
+
|
51
|
+
clazz2b = Record.define do |rec| ## try "yield"-style dsl with block.arity == 1
|
52
|
+
rec.string :brewery
|
53
|
+
rec.string :city
|
54
|
+
rec.string :name
|
55
|
+
rec.float :abv
|
56
|
+
end
|
57
|
+
pp clazz2b
|
58
|
+
pp clazz2b.class.name
|
59
|
+
pp clazz2b.fields
|
60
|
+
|
61
|
+
assert true ## assume ok if we get here
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
|
66
|
+
def test_classic
|
67
|
+
pp BeerClassic.fields
|
68
|
+
pp BeerClassic.field_names
|
69
|
+
pp BeerClassic.columns ## try fields alias
|
70
|
+
pp BeerClassic.column_names ## try field_names alias
|
71
|
+
|
72
|
+
assert_equal [:brewery, :city, :name, :abv], BeerClassic.field_names
|
73
|
+
assert_equal [String, String, String, Float], BeerClassic.field_types
|
74
|
+
|
75
|
+
beer = BeerClassic.new
|
76
|
+
pp beer
|
77
|
+
beer.update( abv: 7.0 )
|
78
|
+
beer.update( brewery: 'Andechser Klosterbrauerei',
|
79
|
+
city: 'Andechs',
|
80
|
+
name: 'Doppelbock Dunkel' )
|
81
|
+
pp beer
|
82
|
+
|
83
|
+
assert_equal 7.0, beer.abv
|
84
|
+
assert_equal 'Andechser Klosterbrauerei', beer.brewery
|
85
|
+
assert_equal 'Andechs', beer.city
|
86
|
+
assert_equal 'Doppelbock Dunkel', beer.name
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
def test_new
|
91
|
+
beer = Beer.new
|
92
|
+
pp beer
|
93
|
+
beer.update( abv: 7.0 )
|
94
|
+
beer.update( brewery: 'Andechser Klosterbrauerei',
|
95
|
+
city: 'Andechs',
|
96
|
+
name: 'Doppelbock Dunkel' )
|
97
|
+
pp beer
|
98
|
+
|
99
|
+
assert_equal 7.0, beer.abv
|
100
|
+
assert_equal 'Andechser Klosterbrauerei', beer.brewery
|
101
|
+
assert_equal 'Andechs', beer.city
|
102
|
+
assert_equal 'Doppelbock Dunkel', beer.name
|
103
|
+
|
104
|
+
|
105
|
+
pp beer.abv
|
106
|
+
pp beer.abv = 7.0
|
107
|
+
pp beer.abv
|
108
|
+
assert_equal 7.0, beer.abv
|
109
|
+
|
110
|
+
pp beer.parse_abv( '7%' ) ## (auto-)converts/typecasts string to specified type (e.g. float)
|
111
|
+
assert_equal 7.0, beer.abv
|
112
|
+
|
113
|
+
pp beer.name
|
114
|
+
pp beer.name = 'Doppelbock Dunkel'
|
115
|
+
pp beer.name
|
116
|
+
assert_equal 'Doppelbock Dunkel', beer.name
|
117
|
+
|
118
|
+
|
119
|
+
beer2 = Beer.new( brewery: 'Andechser Klosterbrauerei',
|
120
|
+
city: 'Andechs',
|
121
|
+
name: 'Doppelbock Dunkel' )
|
122
|
+
pp beer2
|
123
|
+
|
124
|
+
assert_nil beer2.abv
|
125
|
+
assert_equal 'Andechser Klosterbrauerei', beer2.brewery
|
126
|
+
assert_equal 'Andechs', beer2.city
|
127
|
+
assert_equal 'Doppelbock Dunkel', beer2.name
|
128
|
+
end
|
129
|
+
|
130
|
+
def test_parse
|
131
|
+
values = ['Andechser Klosterbrauerei',
|
132
|
+
'Andechs',
|
133
|
+
'Doppelbock Dunkel',
|
134
|
+
'7%']
|
135
|
+
|
136
|
+
beer = Beer.new
|
137
|
+
beer.parse( values )
|
138
|
+
|
139
|
+
assert_equal values[0], beer.brewery
|
140
|
+
assert_equal values[1], beer.city
|
141
|
+
assert_equal values[2], beer.name
|
142
|
+
assert_equal values[3].to_f, beer.abv
|
143
|
+
|
144
|
+
assert_equal values[0], beer[0]
|
145
|
+
assert_equal values[1], beer[1]
|
146
|
+
assert_equal values[2], beer[2]
|
147
|
+
assert_equal values[3].to_f, beer[3]
|
148
|
+
|
149
|
+
assert_equal values[0], beer.values[0]
|
150
|
+
assert_equal values[1], beer.values[1]
|
151
|
+
assert_equal values[2], beer.values[2]
|
152
|
+
assert_equal values[3].to_f, beer.values[3]
|
153
|
+
|
154
|
+
assert_equal values[0], beer[:brewery]
|
155
|
+
assert_equal values[1], beer[:city]
|
156
|
+
assert_equal values[2], beer[:name]
|
157
|
+
assert_equal values[3].to_f, beer[:abv]
|
158
|
+
|
159
|
+
assert_equal values[0], beer['Brewery']
|
160
|
+
assert_equal values[1], beer['City']
|
161
|
+
assert_equal values[2], beer['Name']
|
162
|
+
assert_equal values[3].to_f, beer['Abv']
|
163
|
+
end
|
164
|
+
|
165
|
+
end # class TestRecord
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
###
|
4
|
+
# to run use
|
5
|
+
# ruby -I ./lib -I ./test test/test_version.rb
|
6
|
+
|
7
|
+
|
8
|
+
require 'helper'
|
9
|
+
|
10
|
+
class TestVersion < MiniTest::Test
|
11
|
+
|
12
|
+
def test_version
|
13
|
+
pp Record::VERSION
|
14
|
+
pp Record.banner
|
15
|
+
pp Record.root
|
16
|
+
|
17
|
+
assert true ## assume ok if we get here
|
18
|
+
end
|
19
|
+
|
20
|
+
end # class TestVersion
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: record
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gerald Bauer
|
@@ -54,7 +54,13 @@ files:
|
|
54
54
|
- README.md
|
55
55
|
- Rakefile
|
56
56
|
- lib/record.rb
|
57
|
+
- lib/record/base.rb
|
58
|
+
- lib/record/builder.rb
|
59
|
+
- lib/record/field.rb
|
57
60
|
- lib/record/version.rb
|
61
|
+
- test/helper.rb
|
62
|
+
- test/test_record.rb
|
63
|
+
- test/test_version.rb
|
58
64
|
homepage: https://github.com/rubylibs/record
|
59
65
|
licenses:
|
60
66
|
- Public Domain
|