bagman 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/Gemfile +4 -0
- data/License +20 -0
- data/Rakefile +1 -0
- data/Readme.md +23 -0
- data/bagman.gemspec +20 -0
- data/lib/bagman.rb +8 -0
- data/lib/bagman/active_record.rb +48 -0
- data/lib/bagman/bag.rb +156 -0
- data/lib/bagman/document.rb +131 -0
- data/lib/bagman/extension.rb +31 -0
- data/lib/bagman/simple_column.rb +51 -0
- data/lib/bagman/version.rb +3 -0
- metadata +80 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/License
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (C) 2011 by PLUS2 Pty. Ltd., Ben Askins and Lachie Cox
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
20
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
data/Readme.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# Bagman
|
2
|
+
|
3
|
+
Add a bag of attributes to ActiveRecord models.
|
4
|
+
|
5
|
+
We built Bagman for a project. We wanted fast prototyping and development, and some level of escape from the schema on relational databases.
|
6
|
+
|
7
|
+
It works by serialising and deserialising a JSON string to a `text` field in the database.
|
8
|
+
|
9
|
+
Deep getters and setters are provided by `AngryHash`.
|
10
|
+
|
11
|
+
## Basic usage
|
12
|
+
|
13
|
+
`TODO`
|
14
|
+
|
15
|
+
## Encryption - the crypto pocket.
|
16
|
+
|
17
|
+
`TODO`
|
18
|
+
|
19
|
+
## License
|
20
|
+
|
21
|
+
Copyright (C) 2011 by PLUS2 Pty. Ltd., Ben Askins <ben.askins@gmail.com> and Lachie Cox <lachiec@gmail.com>.
|
22
|
+
|
23
|
+
Bagman is licensed under the MIT license (enclosed).
|
data/bagman.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "bagman/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "bagman"
|
7
|
+
s.version = Bagman::VERSION
|
8
|
+
s.authors = ["Lachie Cox", "Ben Askins"]
|
9
|
+
s.email = ["lachiec@gmail.com", "ben.askins@gmail.com"]
|
10
|
+
s.homepage = "http://github.com/plus2/bagman"
|
11
|
+
s.summary = %q{Add a bag of attributes to ActiveRecord models.}
|
12
|
+
s.description = %q{We built Bagman for a project. We wanted fast prototyping and development, and some level of escape from the schema on relational databases.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "bagman"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
end
|
data/lib/bagman.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
module Bagman
|
2
|
+
module ConnectionAdapters
|
3
|
+
module TableDefinition
|
4
|
+
def bag_for(klass_symbol)
|
5
|
+
klass = klass_symbol.to_s.classify.constantize
|
6
|
+
raise "#{klass} doesn't have a bag" unless klass.respond_to?(:bag)
|
7
|
+
Bagman::TableBuilder.build_table(self, klass.bag)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module TableBuilder
|
13
|
+
def self.build_table(table, bag)
|
14
|
+
table.text :bag
|
15
|
+
|
16
|
+
# build indexed columns
|
17
|
+
bag.columns.each do |column|
|
18
|
+
table.send(column.type, column.index_name) if column.index?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# add indexes for indexed columns
|
23
|
+
def self.add_indexes(table, table_name, bag)
|
24
|
+
bag.columns.each do |column|
|
25
|
+
table.add_index(table_name, column.index_name) if column.index?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
module ActiveRecord
|
33
|
+
module ConnectionAdapters
|
34
|
+
module SchemaStatements
|
35
|
+
def bag_for_table(table)
|
36
|
+
klass = table.to_s.classify.constantize
|
37
|
+
raise "#{klass} doesn't have a bag" unless klass.respond_to?(:bag)
|
38
|
+
klass.bag
|
39
|
+
end
|
40
|
+
|
41
|
+
def add_bag_indexes_for(table_name)
|
42
|
+
Bagman::TableBuilder.add_indexes(self, table_name, bag_for_table(table_name.singularize.to_sym))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
ActiveRecord::ConnectionAdapters::TableDefinition.send(:include, Bagman::ConnectionAdapters::TableDefinition)
|
data/lib/bagman/bag.rb
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
module Bagman
|
2
|
+
class Bag
|
3
|
+
attr_reader :target_class, :top_level_mixin, :columns
|
4
|
+
|
5
|
+
def initialize(target_class,&blk)
|
6
|
+
@target_class = target_class
|
7
|
+
@top_level_mixin = Module.new do
|
8
|
+
include AngryHash::Extension
|
9
|
+
end
|
10
|
+
|
11
|
+
@columns = []
|
12
|
+
|
13
|
+
instance_eval(&blk)
|
14
|
+
end
|
15
|
+
|
16
|
+
alias :fields :columns
|
17
|
+
|
18
|
+
|
19
|
+
def field(name,*args,&blk)
|
20
|
+
options = args.extract_options!
|
21
|
+
type = args.shift || :string
|
22
|
+
|
23
|
+
if options[:unbagged]
|
24
|
+
# no-op
|
25
|
+
# unbagged_field(name, type, options, &blk)
|
26
|
+
elsif options[:encrypted]
|
27
|
+
crypto_field(name, type, options, &blk)
|
28
|
+
else
|
29
|
+
bag_field(name, type, options, &blk)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def bag_field(name, type, options, &blk)
|
35
|
+
@columns << (column = SimpleColumn.new(name, :bag, type, options))
|
36
|
+
|
37
|
+
target_class.send :define_method, name do
|
38
|
+
column.type_cast( self.bag[name] )
|
39
|
+
end
|
40
|
+
|
41
|
+
if type == :boolean
|
42
|
+
target_class.send :define_method, "#{name}?" do
|
43
|
+
column.type_cast( self.bag[name] )
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
target_class.send :define_method, "#{name}=" do |value|
|
48
|
+
if index_name = column.index_name
|
49
|
+
write_attribute(index_name, value)
|
50
|
+
end
|
51
|
+
|
52
|
+
self.bag[name] = if s = options[:serialize]
|
53
|
+
s[value]
|
54
|
+
elsif value.is_a? Date
|
55
|
+
value.to_s(:db)
|
56
|
+
else
|
57
|
+
value.to_s
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
def crypto_field(name, type, options, &blk)
|
64
|
+
name = name.to_s
|
65
|
+
@columns << (column = SimpleColumn.new(name, :crypto, type, options))
|
66
|
+
|
67
|
+
target_class.send :define_method, name do
|
68
|
+
column.type_cast( self.crypto_pocket[name] )
|
69
|
+
end
|
70
|
+
|
71
|
+
target_class.send :define_method, "#{name}_before_type_cast" do
|
72
|
+
self.crypto_pocket[name]
|
73
|
+
end
|
74
|
+
|
75
|
+
if options[:shadow]
|
76
|
+
target_class.send :define_method, "#{name}_shadow" do
|
77
|
+
read_attribute(name)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
target_class.send :define_method, "#{name}=" do |value|
|
83
|
+
# we need to flag bag as dirty, or we're never saved.
|
84
|
+
self.bag_will_change!
|
85
|
+
|
86
|
+
self.crypto_pocket[name] = final_value = Bagman::Bag.serialise_value(value, options)
|
87
|
+
|
88
|
+
if pepper = options[:shadow]
|
89
|
+
shadowed = case pepper
|
90
|
+
when String
|
91
|
+
Gibberish::SHA256( pepper + '--' + final_value )
|
92
|
+
when Symbol
|
93
|
+
send(pepper, final_value)
|
94
|
+
else
|
95
|
+
Gibberish::SHA256( final_value )
|
96
|
+
end
|
97
|
+
|
98
|
+
write_attribute(name, shadowed)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
def unbagged_field(name, type, options, &blk)
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
|
109
|
+
def self.serialise_value(value, options)
|
110
|
+
if s = options[:serialize]
|
111
|
+
s[value]
|
112
|
+
elsif value.is_a? Date
|
113
|
+
value.to_s(:db)
|
114
|
+
else
|
115
|
+
value.to_s
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
def collection(name,type,options={},&blk)
|
121
|
+
@top_level_mixin.module_eval do
|
122
|
+
extend_array name, type
|
123
|
+
end
|
124
|
+
|
125
|
+
target_class.send :define_method, name do
|
126
|
+
self.bag[name]
|
127
|
+
end
|
128
|
+
|
129
|
+
target_class.send :define_method, "#{name}=" do |value|
|
130
|
+
if value.is_a? Hash
|
131
|
+
value = value.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
|
132
|
+
end
|
133
|
+
self.bag[name] = value
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
|
138
|
+
## Indices
|
139
|
+
def index(*)
|
140
|
+
end
|
141
|
+
|
142
|
+
## Materialise
|
143
|
+
def materialise(*)
|
144
|
+
end
|
145
|
+
|
146
|
+
## Validations
|
147
|
+
def validates_presence_of(*)
|
148
|
+
end
|
149
|
+
|
150
|
+
def validates_storage_type_of(*)
|
151
|
+
end
|
152
|
+
|
153
|
+
def validate(*)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
module Bagman
|
2
|
+
module Document
|
3
|
+
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
|
7
|
+
CryptoPocketKey = '_crypto'
|
8
|
+
|
9
|
+
|
10
|
+
included do
|
11
|
+
before_save :serialize_bag
|
12
|
+
end
|
13
|
+
|
14
|
+
def bag
|
15
|
+
@bag ||= decode_or_initialize_bag
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def bag=(bag)
|
20
|
+
@bag = AngryHash[bag]
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def decode_or_initialize_bag
|
25
|
+
begin
|
26
|
+
AngryHash[ ActiveSupport::JSON.decode( read_attribute('bag') ) ]
|
27
|
+
rescue
|
28
|
+
initialize_bag(AngryHash.new)
|
29
|
+
end.tap {|h|
|
30
|
+
h.extend self.class.bag.top_level_mixin
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def initialize_bag(bag)
|
36
|
+
bag.tap do |b|
|
37
|
+
self.class.bag.columns.select { |c| c.options.has_key?(:default) }.each do |column|
|
38
|
+
b[column.name] = column.options[:default]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def serialize_bag
|
45
|
+
if @bag
|
46
|
+
encrypt_crypto_pocket
|
47
|
+
write_attribute(:bag, ActiveSupport::JSON.encode(@bag))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
|
53
|
+
###################
|
54
|
+
# crypto pocket #
|
55
|
+
###################
|
56
|
+
|
57
|
+
def crypto_pocket
|
58
|
+
@crypto_pocket ||= decrypt_crypto_pocket
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
def crypto_pocket=(bag)
|
63
|
+
@crypto_pocket = AngryHash[bag] if bag
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
def decrypt_crypto_pocket
|
68
|
+
if crypto_pocket = bag[CryptoPocketKey]
|
69
|
+
ActiveSupport::JSON.decode( encryptor.dec(crypto_pocket) )
|
70
|
+
else
|
71
|
+
{}
|
72
|
+
end
|
73
|
+
rescue
|
74
|
+
{}
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
def encrypt_crypto_pocket
|
79
|
+
if @crypto_pocket.is_a?(Hash)
|
80
|
+
bag[CryptoPocketKey] = encryptor.enc(@crypto_pocket.to_json)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
def encryptor
|
86
|
+
@encryptor ||= begin
|
87
|
+
cfg = Davidson.app_config
|
88
|
+
password = cfg.crypto.password || cfg.missing!('crypto.password')
|
89
|
+
Gibberish::AES.new(password)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
|
95
|
+
# Fills a bag with data from a source document, setting only those values present in the target document
|
96
|
+
def fill_bag_from(source)
|
97
|
+
self.class.bag.columns.each do |column|
|
98
|
+
column_name = column.name
|
99
|
+
if source.respond_to?(column_name) && self.send(column_name).blank?
|
100
|
+
self.send("#{column_name}=", source.send(column_name))
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
def reload(*args)
|
107
|
+
@bag = nil
|
108
|
+
super
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
def as_json(opts={})
|
113
|
+
bag.dup.tap {|b|
|
114
|
+
b.id = id
|
115
|
+
}
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
module ClassMethods
|
120
|
+
def bag(&blk)
|
121
|
+
if block_given?
|
122
|
+
@bag = Bag.new(self, &blk)
|
123
|
+
else
|
124
|
+
@bag
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'angry_hash/extension'
|
2
|
+
|
3
|
+
module Bagman
|
4
|
+
module Extension
|
5
|
+
include AngryHash::Extension
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
base.extend AngryHash::Extension::ClassMethods
|
9
|
+
base.extend ClassMethods
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def field(name,*args,&blk)
|
14
|
+
options = args.extract_options!
|
15
|
+
type = args.shift || :string
|
16
|
+
|
17
|
+
column = SimpleColumn.new(name, :extended, type)
|
18
|
+
|
19
|
+
define_method name do
|
20
|
+
column.type_cast( self[name] )
|
21
|
+
# TODO typecasting, racial profiling
|
22
|
+
end
|
23
|
+
|
24
|
+
define_method "#{name}=" do |value|
|
25
|
+
self[name] = value.to_s
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'active_record/connection_adapters/abstract/schema_definitions'
|
2
|
+
|
3
|
+
module Bagman
|
4
|
+
class SimpleColumn < ActiveRecord::ConnectionAdapters::Column
|
5
|
+
|
6
|
+
attr_reader :name, :options, :type
|
7
|
+
|
8
|
+
def initialize(name, role, type, options={})
|
9
|
+
@name, @role, @type, @options = name, role, type, options
|
10
|
+
end
|
11
|
+
|
12
|
+
def type_cast(value)
|
13
|
+
case type
|
14
|
+
when Class
|
15
|
+
type.new(value)
|
16
|
+
else
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def index_name
|
22
|
+
case options[:index]
|
23
|
+
when TrueClass
|
24
|
+
"index_#{name}"
|
25
|
+
when String,Symbol
|
26
|
+
options[:index]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def index?
|
31
|
+
!! index_name
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.string_to_date(string)
|
35
|
+
dd_mm_yyyy_to_date(string) || super
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.dd_mm_yyyy_to_date(string)
|
39
|
+
Date.strptime(string, "%d/%m/%Y") rescue nil
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.string_to_time(string)
|
43
|
+
dd_mm_yyyy_to_time(string) || super
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.dd_mm_yyyy_to_time(string)
|
47
|
+
DateTime.strptime(string, "%d/%m/%Y") rescue nil
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bagman
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Lachie Cox
|
14
|
+
- Ben Askins
|
15
|
+
autorequire:
|
16
|
+
bindir: bin
|
17
|
+
cert_chain: []
|
18
|
+
|
19
|
+
date: 2011-07-19 00:00:00 Z
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: We built Bagman for a project. We wanted fast prototyping and development, and some level of escape from the schema on relational databases.
|
23
|
+
email:
|
24
|
+
- lachiec@gmail.com
|
25
|
+
- ben.askins@gmail.com
|
26
|
+
executables: []
|
27
|
+
|
28
|
+
extensions: []
|
29
|
+
|
30
|
+
extra_rdoc_files: []
|
31
|
+
|
32
|
+
files:
|
33
|
+
- .gitignore
|
34
|
+
- Gemfile
|
35
|
+
- License
|
36
|
+
- Rakefile
|
37
|
+
- Readme.md
|
38
|
+
- bagman.gemspec
|
39
|
+
- lib/bagman.rb
|
40
|
+
- lib/bagman/active_record.rb
|
41
|
+
- lib/bagman/bag.rb
|
42
|
+
- lib/bagman/document.rb
|
43
|
+
- lib/bagman/extension.rb
|
44
|
+
- lib/bagman/simple_column.rb
|
45
|
+
- lib/bagman/version.rb
|
46
|
+
homepage: http://github.com/plus2/bagman
|
47
|
+
licenses: []
|
48
|
+
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options: []
|
51
|
+
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
hash: 3
|
60
|
+
segments:
|
61
|
+
- 0
|
62
|
+
version: "0"
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
hash: 3
|
69
|
+
segments:
|
70
|
+
- 0
|
71
|
+
version: "0"
|
72
|
+
requirements: []
|
73
|
+
|
74
|
+
rubyforge_project: bagman
|
75
|
+
rubygems_version: 1.8.5
|
76
|
+
signing_key:
|
77
|
+
specification_version: 3
|
78
|
+
summary: Add a bag of attributes to ActiveRecord models.
|
79
|
+
test_files: []
|
80
|
+
|