records 0.0.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Manifest.txt +1 -0
- data/README.md +89 -2
- data/Rakefile +1 -1
- data/lib/records.rb +1 -1
- data/lib/records/record.rb +132 -0
- data/lib/records/version.rb +2 -2
- data/test/test_account.rb +107 -0
- metadata +4 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 13d06e265fb13c8800ea2c98f699a839bef77a8e
|
4
|
+
data.tar.gz: 530dcd1156f9e7146bb3ff157db9159f4aa8d72d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: eafaa2aa7bdcb10b0d4138337e11157d5afc035569d4829c81dadac1f1e20a2c08801f8811208f0e9108c3b531a1d0af436ac2aea49a56fc9c21821406c703ea
|
7
|
+
data.tar.gz: e7ae023c0bf8dbdf08b85472e7eb64145973e4a79662d8b4c03c51ce2e4179066613f67e0291acf40062c5df98afe263c4d3052b84d5a34b64ddcf86d0f0ee2d
|
data/Manifest.txt
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
|
2
2
|
# Records - Frozen / Immutable Structs with Copy on Updates
|
3
3
|
|
4
|
-
records gem / library - frozen / immutable
|
4
|
+
records gem / library - frozen / immutable structs with copy on updates
|
5
5
|
|
6
6
|
|
7
7
|
* home :: [github.com/s6ruby/records](https://github.com/s6ruby/records)
|
@@ -12,7 +12,94 @@ records gem / library - frozen / immutable (frozen) structs with copy on updates
|
|
12
12
|
|
13
13
|
## Usage
|
14
14
|
|
15
|
-
|
15
|
+
Use `Record.new` like `Struct.new` to build / create a new frozen / immutable
|
16
|
+
record class. Example:
|
17
|
+
|
18
|
+
``` ruby
|
19
|
+
|
20
|
+
Record.new( :Account,
|
21
|
+
balance: Integer,
|
22
|
+
allowances: Hash )
|
23
|
+
|
24
|
+
# -or-
|
25
|
+
|
26
|
+
Record.new :Account,
|
27
|
+
balance: Integer,
|
28
|
+
allowances: Hash
|
29
|
+
|
30
|
+
# -or-
|
31
|
+
|
32
|
+
Record.new :Account, { balance: Integer,
|
33
|
+
allowances: Hash }
|
34
|
+
|
35
|
+
# -or-
|
36
|
+
|
37
|
+
class Account < Record::Base
|
38
|
+
field :balance, Integer
|
39
|
+
field :allowances, Hash
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
|
44
|
+
And use the new record class like:
|
45
|
+
|
46
|
+
``` ruby
|
47
|
+
account1a = Account.new( 1, {} )
|
48
|
+
account1a.frozen? #=> true
|
49
|
+
account1a.values.frozen? #=> true
|
50
|
+
account1a.balance #=> 1
|
51
|
+
account1a.allowances #=> {}
|
52
|
+
account1a.values #=> [1, {}]
|
53
|
+
|
54
|
+
Account.keys #=> [:balance, :allowances]
|
55
|
+
Account.fields #=> [<Field @key=:balance, @index=0, @type=Integer>,
|
56
|
+
# <Field @key=:allowances, @index=1, @type=Hash>]
|
57
|
+
Account.index( :balance ) #=> 0
|
58
|
+
Account.index( :allowances ) #=> 1
|
59
|
+
```
|
60
|
+
|
61
|
+
Note: The `update` method (or the `<<` alias)
|
62
|
+
ALWAYS returns a new record.
|
63
|
+
|
64
|
+
``` ruby
|
65
|
+
account1a.update( balance: 20 ) #=> [20, {}]
|
66
|
+
account1a.update( balance: 30 ) #=> [30, {}]
|
67
|
+
account1a.update( { balance: 30 } ) #=> [30, {}]
|
68
|
+
|
69
|
+
account1a << { balance: 20 } #=> [20, {}]
|
70
|
+
account1a << { balance: 30 } #=> [30, {}]
|
71
|
+
|
72
|
+
account1b = account1a.update( balance: 40, allowances: { 'Alice': 20 } )
|
73
|
+
account1b.balance #=> 40
|
74
|
+
account1b.allowances #=> { 'Alice': 20 }
|
75
|
+
account1b.values #=> [40, { 'Alice': 20 } ]
|
76
|
+
# ...
|
77
|
+
```
|
78
|
+
|
79
|
+
And so on and so forth.
|
80
|
+
|
81
|
+
|
82
|
+
## Bonus - Record Update Language Syntax Pragmas - `{...}` and `={...}`
|
83
|
+
|
84
|
+
Using the Record Update Pragma. Lets you
|
85
|
+
|
86
|
+
``` ruby
|
87
|
+
account1a {... balance: 20 } #=> [20, {}]
|
88
|
+
account1a {... balance: 30 } #=> [30, {}]
|
89
|
+
|
90
|
+
account1a = {... balance: 40, allowances: { 'Alice': 20 }}
|
91
|
+
```
|
92
|
+
|
93
|
+
turn into:
|
94
|
+
|
95
|
+
``` ruby
|
96
|
+
account1a.update( balance: 20 ) #=> [20, {}]
|
97
|
+
account1a.update( balance: 30 ) #=> [30, {}]
|
98
|
+
|
99
|
+
account1a = account1a.update( balance: 40, allowances: { 'Alice': 20 } )
|
100
|
+
```
|
101
|
+
|
102
|
+
See [Language Syntax Pragmas - Let's Evolve Ruby by Experimenting in a Pragma(tic) Way](https://github.com/s6ruby/pragmas) for more.
|
16
103
|
|
17
104
|
|
18
105
|
## License
|
data/Rakefile
CHANGED
@@ -5,7 +5,7 @@ Hoe.spec 'records' do
|
|
5
5
|
|
6
6
|
self.version = Records::VERSION
|
7
7
|
|
8
|
-
self.summary = "records
|
8
|
+
self.summary = "records - frozen / immutable structs with copy on updates"
|
9
9
|
self.description = summary
|
10
10
|
|
11
11
|
self.urls = ['https://github.com/s6ruby/records']
|
data/lib/records.rb
CHANGED
@@ -0,0 +1,132 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
|
4
|
+
|
5
|
+
class Record
|
6
|
+
|
7
|
+
class Type; end
|
8
|
+
Types = Type # note Types is an alias for Type
|
9
|
+
|
10
|
+
|
11
|
+
|
12
|
+
class Field
|
13
|
+
attr_reader :key, :type
|
14
|
+
attr_reader :index ## note: zero-based position index (0,1,2,3,...)
|
15
|
+
|
16
|
+
def initialize( key, index, type )
|
17
|
+
@key = key.to_sym ## note: always symbol-ify (to_sym) key
|
18
|
+
@index = index
|
19
|
+
@type = type
|
20
|
+
end
|
21
|
+
end # class Field
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
class Base < Record
|
26
|
+
|
27
|
+
def self.fields ## note: use class instance variable (@fields and NOT @@fields)!!!! (derived classes get its own copy!!!)
|
28
|
+
@fields ||= []
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.keys
|
32
|
+
@keys ||= fields.map {|field| field.key }.freeze
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.index( key ) ## indef of key (0,1,2,etc.)
|
36
|
+
## note: returns nil now for unknown keys
|
37
|
+
## use/raise IndexError or something - why? why not?
|
38
|
+
@index_by_key ||= Hash[ keys.zip( (0...fields.size).to_a ) ].freeze
|
39
|
+
@index_by_key[key]
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
|
44
|
+
def self.field( key, type )
|
45
|
+
index = fields.size ## auto-calc num(ber) / position index - always gets added at the end
|
46
|
+
field = Field.new( key, index, type )
|
47
|
+
fields << field
|
48
|
+
|
49
|
+
define_field( field ) ## auto-add getter,setter,parse/typecast
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.define_field( field )
|
53
|
+
key = field.key ## note: always assumes a "cleaned-up" (symbol) name
|
54
|
+
index = field.index
|
55
|
+
|
56
|
+
define_method( key ) do
|
57
|
+
instance_variable_get( "@values" )[index]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
## note: "skip" overloaded new Record.new (and use old_new version)
|
62
|
+
def self.new( *args ) old_new( *args ); end
|
63
|
+
|
64
|
+
|
65
|
+
|
66
|
+
attr_reader :values
|
67
|
+
|
68
|
+
def initialize( *args )
|
69
|
+
#####
|
70
|
+
## todo/fix: add allow keyword init too
|
71
|
+
### note:
|
72
|
+
### if init( 1, {} ) assumes last {} is a kwargs!!!!!
|
73
|
+
## and NOT a "plain" arg in args!!!
|
74
|
+
|
75
|
+
## puts "[#{self.class.name}] Record::Base.initialize:"
|
76
|
+
## pp args
|
77
|
+
|
78
|
+
##
|
79
|
+
## fix/todo: check that number of args are equal fields.size !!!
|
80
|
+
## check types too :-)
|
81
|
+
|
82
|
+
@values = args
|
83
|
+
@values.freeze
|
84
|
+
self.freeze ## freeze self too - why? why not?
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
def update( **kwargs )
|
90
|
+
new_values = @values.dup ## note: use dup NOT clone (will "undo" frozen state?)
|
91
|
+
kwargs.each do |key,value|
|
92
|
+
index = self.class.index( key )
|
93
|
+
new_values[ index ] = value
|
94
|
+
end
|
95
|
+
self.class.new( *new_values )
|
96
|
+
end
|
97
|
+
|
98
|
+
## "convenience" shortcut for update e.g.
|
99
|
+
## << { balance: 5 }
|
100
|
+
## equals
|
101
|
+
## .update( balance: 5 )
|
102
|
+
def <<( hash ) update( hash ); end
|
103
|
+
|
104
|
+
|
105
|
+
###
|
106
|
+
## note: compare by value for now (and NOT object id)
|
107
|
+
def ==(other)
|
108
|
+
if other.instance_of?( self.class )
|
109
|
+
values == other.values
|
110
|
+
else
|
111
|
+
false
|
112
|
+
end
|
113
|
+
end
|
114
|
+
alias_method :eql?, :==
|
115
|
+
|
116
|
+
end # class Base
|
117
|
+
|
118
|
+
|
119
|
+
def self.build_class( class_name, **attributes )
|
120
|
+
klass = Class.new( Base )
|
121
|
+
attributes.each do |key, type|
|
122
|
+
klass.field( key, type )
|
123
|
+
end
|
124
|
+
|
125
|
+
Type.const_set( class_name, klass ) ## returns klass (plus sets global constant class name)
|
126
|
+
end
|
127
|
+
|
128
|
+
class << self
|
129
|
+
alias_method :old_new, :new # note: store "old" orginal version of new
|
130
|
+
alias_method :new, :build_class # replace original version with create
|
131
|
+
end
|
132
|
+
end # class Record
|
data/lib/records/version.rb
CHANGED
data/test/test_account.rb
CHANGED
@@ -10,4 +10,111 @@ require 'helper'
|
|
10
10
|
|
11
11
|
class TestAccount < MiniTest::Test
|
12
12
|
|
13
|
+
Record.build_class( :Account,
|
14
|
+
balance: Integer,
|
15
|
+
allowances: Hash )
|
16
|
+
Account = Record::Type::Account
|
17
|
+
|
18
|
+
|
19
|
+
Record.new( :Account2,
|
20
|
+
balance: Integer,
|
21
|
+
allowances: Hash )
|
22
|
+
Account2 = Record::Type::Account2
|
23
|
+
|
24
|
+
|
25
|
+
class Account3 < Record::Base
|
26
|
+
field :balance, Integer
|
27
|
+
field :allowances, Hash
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
|
32
|
+
def test_account
|
33
|
+
pp Record::Type::Account
|
34
|
+
pp Account.ancestors
|
35
|
+
|
36
|
+
puts "Record::Type.constants:"
|
37
|
+
pp Record::Type.constants
|
38
|
+
|
39
|
+
account1a = Account.new( 1, {} )
|
40
|
+
assert_equal Account.new( 1, {} ), account1a
|
41
|
+
assert_equal true, account1a.frozen?
|
42
|
+
assert_equal true, account1a.values.frozen?
|
43
|
+
assert_equal 1, account1a.balance
|
44
|
+
assert_equal Hash({}), account1a.allowances
|
45
|
+
|
46
|
+
|
47
|
+
assert_equal Account.new( 20, {} ), account1a.update( balance: 20 )
|
48
|
+
assert_equal Account.new( 30, {} ), account1a.update( balance: 30 )
|
49
|
+
assert_equal Account.new( 30, {} ), account1a.update( { balance: 30 } )
|
50
|
+
|
51
|
+
assert_equal Account.new( 20, {} ), account1a << { balance: 20 }
|
52
|
+
assert_equal Account.new( 30, {} ), account1a << { balance: 30 }
|
53
|
+
assert_equal Account.new( 30, {} ), account1a << Hash( { balance: 30 } )
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
def test_account2
|
58
|
+
pp Record::Type::Account2
|
59
|
+
pp Account2.ancestors
|
60
|
+
|
61
|
+
puts "Record::Type.constants:"
|
62
|
+
pp Record::Type.constants
|
63
|
+
|
64
|
+
assert_equal [:balance, :allowances], Account2.keys
|
65
|
+
# assert_equal [Record::Field.new( :balance, 0, Integer),
|
66
|
+
# Record::Field.new( :allowances, 1, Hash )
|
67
|
+
# ], Account2.fields
|
68
|
+
|
69
|
+
assert_equal 0, Account2.index( :balance )
|
70
|
+
assert_equal 1, Account2.index( :allowances )
|
71
|
+
|
72
|
+
account1a = Account2.new( 1, {} )
|
73
|
+
assert_equal Account2.new( 1, {} ), account1a
|
74
|
+
assert_equal [1, {}], account1a.values
|
75
|
+
|
76
|
+
assert_equal Account2.new( 20, {} ), account1a.update( balance: 20 )
|
77
|
+
assert_equal Account2.new( 30, {} ), account1a.update( balance: 30 )
|
78
|
+
assert_equal [20, {}], account1a.update( balance: 20 ).values
|
79
|
+
assert_equal [30, {}], account1a.update( balance: 30 ).values
|
80
|
+
|
81
|
+
|
82
|
+
account1b = account1a.update( balance: 10, allowances: { '0xaa': 20 } )
|
83
|
+
assert_equal Account2.new( 10, { '0xaa': 20 } ), account1b
|
84
|
+
assert_equal Hash({ '0xaa': 20 }), account1b.allowances
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
def test_account3
|
89
|
+
pp Account3
|
90
|
+
pp Account3.ancestors
|
91
|
+
|
92
|
+
puts "Record::Type.constants:"
|
93
|
+
pp Record::Type.constants
|
94
|
+
|
95
|
+
account1a = Account3.new( 1, {} )
|
96
|
+
assert_equal Account3.new( 1, {} ), account1a
|
97
|
+
assert_equal true, account1a.is_a?( Record )
|
98
|
+
assert_equal true, account1a.is_a?( Record::Base )
|
99
|
+
|
100
|
+
|
101
|
+
assert_equal Account3.new( 20, {} ), account1a.update( balance: 20 )
|
102
|
+
assert_equal Account3.new( 30, {} ), account1a.update( balance: 30 )
|
103
|
+
assert_equal [30, {}], account1a.update( balance: 30 ).values
|
104
|
+
|
105
|
+
account1b = account1a.update( balance: 10 )
|
106
|
+
assert_equal Account3.new( 10, {} ), account1b
|
107
|
+
assert_equal 10, account1b.balance
|
108
|
+
assert_equal Hash({}), account1b.allowances
|
109
|
+
assert_equal [10, {}], account1b.values
|
110
|
+
|
111
|
+
account1b = account1a.update( balance: 10, allowances: { '0xaa': 20 } )
|
112
|
+
assert_equal Hash({'0xaa': 20 }), account1b.allowances
|
113
|
+
assert_equal [10, {'0xaa': 20 }], account1b.values
|
114
|
+
|
115
|
+
account2 = Account3.new( 2, {} )
|
116
|
+
assert_equal Account3.new( 2, {} ), account2
|
117
|
+
assert_equal [2, {}], account2.values
|
118
|
+
end
|
119
|
+
|
13
120
|
end # class TestAccount
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: records
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gerald Bauer
|
@@ -38,8 +38,7 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '3.16'
|
41
|
-
description: records
|
42
|
-
on updates
|
41
|
+
description: records - frozen / immutable structs with copy on updates
|
43
42
|
email: wwwmake@googlegroups.com
|
44
43
|
executables: []
|
45
44
|
extensions: []
|
@@ -55,6 +54,7 @@ files:
|
|
55
54
|
- README.md
|
56
55
|
- Rakefile
|
57
56
|
- lib/records.rb
|
57
|
+
- lib/records/record.rb
|
58
58
|
- lib/records/version.rb
|
59
59
|
- test/helper.rb
|
60
60
|
- test/test_account.rb
|
@@ -83,6 +83,5 @@ rubyforge_project:
|
|
83
83
|
rubygems_version: 2.5.2
|
84
84
|
signing_key:
|
85
85
|
specification_version: 4
|
86
|
-
summary: records
|
87
|
-
updates
|
86
|
+
summary: records - frozen / immutable structs with copy on updates
|
88
87
|
test_files: []
|