Skip to content

Commit 5f668ab

Browse files
committed
add record
1 parent 4a6d175 commit 5f668ab

File tree

11 files changed

+496
-0
lines changed

11 files changed

+496
-0
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ Gems:
2626
- [date-formatter](date-formatter) - date formatter by example; auto-builds the strftime format string from an example date
2727

2828

29+
<!-- break -->
30+
- [records](records) - frozen / immutable structs with copy on updates
31+
32+
33+
2934
<!-- break -->
3035
- [shell-lite](shell-lite) - run / execute shell commands
3136

records/.gitignore

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
*.gem
2+
*.rbc
3+
/.config
4+
/coverage/
5+
/InstalledFiles
6+
/pkg/
7+
/spec/reports/
8+
/test/tmp/
9+
/test/version_tmp/
10+
/tmp/
11+
12+
## Specific to RubyMotion:
13+
.dat*
14+
.repl_history
15+
build/
16+
17+
## Documentation cache and generated files:
18+
/.yardoc/
19+
/_yardoc/
20+
/doc/
21+
/rdoc/
22+
23+
## Environment normalisation:
24+
/.bundle/
25+
/vendor/bundle
26+
/lib/bundler/man/
27+
28+
# for a library or gem, you might want to ignore these files since the code is
29+
# intended to run in multiple environments; otherwise, check them in:
30+
# Gemfile.lock
31+
# .ruby-version
32+
# .ruby-gemset
33+
34+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
35+
.rvmrc

records/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### 0.0.1 / 2019-04-13
2+
3+
* Everything is new. First release

records/Manifest.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
CHANGELOG.md
2+
Manifest.txt
3+
README.md
4+
Rakefile
5+
lib/records.rb
6+
lib/records/record.rb
7+
lib/records/version.rb
8+
test/helper.rb
9+
test/test_account.rb

records/README.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
2+
# Records - Frozen / Immutable Structs with Copy on Updates
3+
4+
records gem / library - frozen / immutable structs with copy on updates
5+
6+
7+
* home :: [github.com/s6ruby/records](https://github.com/s6ruby/records)
8+
* bugs :: [github.com/s6ruby/records/issues](https://github.com/s6ruby/records/issues)
9+
* gem :: [rubygems.org/gems/records](https://rubygems.org/gems/records)
10+
* rdoc :: [rubydoc.info/gems/records](http://rubydoc.info/gems/records)
11+
12+
13+
## Usage
14+
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+
account = Account.new( 1, {} )
48+
account.frozen? #=> true
49+
account.values.frozen? #=> true
50+
account.balance #=> 1
51+
account.allowances #=> {}
52+
account.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+
account.update( balance: 20 ) #=> [20, {}]
66+
account.update( balance: 30 ) #=> [30, {}]
67+
account.update( { balance: 30 } ) #=> [30, {}]
68+
69+
account << { balance: 20 } #=> [20, {}]
70+
account << { balance: 30 } #=> [30, {}]
71+
72+
account = account.update( balance: 40, allowances: { 'Alice': 20 } )
73+
account.balance #=> 40
74+
account.allowances #=> { 'Alice': 20 }
75+
account.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+
account {... balance: 20 } #=> [20, {}]
88+
account {... balance: 30 } #=> [30, {}]
89+
90+
account = {... balance: 40, allowances: { 'Alice': 20 }}
91+
```
92+
93+
turn into:
94+
95+
``` ruby
96+
account.update( balance: 20 ) #=> [20, {}]
97+
account.update( balance: 30 ) #=> [30, {}]
98+
99+
account = account.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.
103+
104+
105+
## License
106+
107+
![](https://publicdomainworks.github.io/buttons/zero88x31.png)
108+
109+
The `records` scripts are dedicated to the public domain.
110+
Use it as you please with no restrictions whatsoever.
111+
112+
113+
## Questions? Comments?
114+
115+
Send them along to the [wwwmake forum](http://groups.google.com/group/wwwmake).
116+
Thanks!

records/Rakefile

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
require 'hoe'
2+
require './lib/records/version.rb'
3+
4+
Hoe.spec 'records' do
5+
6+
self.version = Records::VERSION
7+
8+
self.summary = "records - frozen / immutable structs with copy on updates"
9+
self.description = summary
10+
11+
self.urls = ['https://github.com/s6ruby/records']
12+
13+
self.author = 'Gerald Bauer'
14+
self.email = '[email protected]'
15+
16+
# switch extension to .markdown for gihub formatting
17+
self.readme_file = 'README.md'
18+
self.history_file = 'CHANGELOG.md'
19+
20+
self.extra_deps = [
21+
]
22+
23+
self.licenses = ['Public Domain']
24+
25+
self.spec_extras = {
26+
required_ruby_version: '>= 2.2.2'
27+
}
28+
29+
end

records/lib/records.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# encoding: utf-8
2+
3+
require 'pp'
4+
5+
## our own code
6+
require 'records/version' # note: let version always go first
7+
require 'records/record'
8+
9+
10+
11+
puts Records.banner ## say hello

records/lib/records/record.rb

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

records/lib/records/version.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# encoding: utf-8
2+
3+
4+
###
5+
# note: do not "pollute" Record namespace / module
6+
# use its own module
7+
8+
9+
module Records
10+
11+
MAJOR = 1
12+
MINOR = 0
13+
PATCH = 0
14+
VERSION = [MAJOR,MINOR,PATCH].join('.')
15+
16+
def self.version
17+
VERSION
18+
end
19+
20+
def self.banner
21+
"records/#{VERSION} on Ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
22+
end
23+
24+
def self.root
25+
"#{File.expand_path( File.dirname(File.dirname(File.dirname(__FILE__))) )}"
26+
end
27+
28+
end # module Records

records/test/helper.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
## minitest setup
2+
3+
require 'minitest/autorun'
4+
5+
6+
## our own code
7+
8+
require 'records'

0 commit comments

Comments
 (0)