records 0.0.1 → 1.0.0
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 +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: []
|