meta_content 0.0.4
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.
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +82 -0
- data/Rakefile +2 -0
- data/lib/meta_content/dsl.rb +62 -0
- data/lib/meta_content/query.rb +70 -0
- data/lib/meta_content/sanitizer.rb +155 -0
- data/lib/meta_content/version.rb +3 -0
- data/lib/meta_content.rb +122 -0
- data/meta_content.gemspec +19 -0
- metadata +58 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Brian Leonard
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# MetaContent
|
2
|
+
|
3
|
+
Store document values in MySQL in a separate key/value table.
|
4
|
+
I'm sure this exists already, but we couldn't find one.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
gem 'meta_content'
|
11
|
+
|
12
|
+
And then execute:
|
13
|
+
|
14
|
+
$ bundle
|
15
|
+
|
16
|
+
Or install it yourself as:
|
17
|
+
|
18
|
+
$ gem install meta_content
|
19
|
+
|
20
|
+
## Migration
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
class CreateTaskMeta < ActiveRecord::Migration
|
24
|
+
def up
|
25
|
+
create_table :tasks_meta do |t|
|
26
|
+
t.integer :object_id, :null => false
|
27
|
+
t.string :namespace, :null => false
|
28
|
+
t.string :lookup, :null => false
|
29
|
+
t.string :value
|
30
|
+
t.integer :int_value
|
31
|
+
end
|
32
|
+
|
33
|
+
add_index :tasks_meta, [:object_id, :namespace, :lookup], unique: true
|
34
|
+
add_index :tasks_meta, :object_id
|
35
|
+
end
|
36
|
+
|
37
|
+
def down
|
38
|
+
drop_table :tasks_meta
|
39
|
+
end
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
## Model
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
class Task < ActiveRecord::Base
|
47
|
+
|
48
|
+
meta do
|
49
|
+
string :name
|
50
|
+
integer :price
|
51
|
+
float :hours, :default => 1.0
|
52
|
+
end
|
53
|
+
|
54
|
+
meta :timing do
|
55
|
+
datetime :start_at
|
56
|
+
string :description
|
57
|
+
boolean :flexible, :default => false
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
## Usage
|
64
|
+
|
65
|
+
This gives getters and setters for everything defined:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
task = Task.new
|
69
|
+
|
70
|
+
task.name = "Store meta info"
|
71
|
+
task.price = 20
|
72
|
+
task.hours = 3.2
|
73
|
+
|
74
|
+
task.timing_start_at = 3.hours.from_now
|
75
|
+
task.timing_description = "Tonight or otherwise by Friday"
|
76
|
+
task.timing_flexible = true
|
77
|
+
|
78
|
+
task.save!
|
79
|
+
```
|
80
|
+
|
81
|
+
Everything gets saved in the `tasks_meta` table. Updating and saving updates those rows.
|
82
|
+
overall, it works like they were regular columns.
|
data/Rakefile
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
module MetaContent
|
2
|
+
class Dsl
|
3
|
+
|
4
|
+
def initialize(klass, namespace)
|
5
|
+
@klass = klass
|
6
|
+
@namespace = namespace
|
7
|
+
end
|
8
|
+
|
9
|
+
%w(integer int float number date datetime time boolean bool symbol sym string).each do |type|
|
10
|
+
class_eval <<-CODE
|
11
|
+
def #{type}(*fields)
|
12
|
+
options = fields.extract_options!
|
13
|
+
options[:type] = :#{type}
|
14
|
+
fields.each do |f|
|
15
|
+
field(f, options)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
CODE
|
19
|
+
end
|
20
|
+
|
21
|
+
def field(*fields)
|
22
|
+
options = fields.extract_options!
|
23
|
+
options[:namespace] = @namespace
|
24
|
+
fields.each do |field|
|
25
|
+
create_accessors_for_meta_field(field, options)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
|
31
|
+
def create_accessors_for_meta_field(field, options = {})
|
32
|
+
given_namespace = options[:namespace]
|
33
|
+
implied_namespace = given_namespace || :class
|
34
|
+
|
35
|
+
@klass.meta_content_fields[implied_namespace] ||= {}
|
36
|
+
@klass.meta_content_fields[implied_namespace][field] = options.except(:namespace)
|
37
|
+
|
38
|
+
method_name = [given_namespace, field].compact.join('_')
|
39
|
+
|
40
|
+
@klass.class_eval <<-EV, __FILE__, __LINE__+1
|
41
|
+
def #{method_name}
|
42
|
+
read_meta(:#{implied_namespace}, :#{field})
|
43
|
+
end
|
44
|
+
|
45
|
+
def #{method_name}=(val)
|
46
|
+
write_meta(:#{implied_namespace}, :#{field}, val)
|
47
|
+
end
|
48
|
+
|
49
|
+
def #{method_name}?
|
50
|
+
![nil, 0, false, ""].include?(#{method_name})
|
51
|
+
end
|
52
|
+
|
53
|
+
def #{method_name}_changed?
|
54
|
+
changes = meta_changes
|
55
|
+
changes[0].keys.include?(:#{method_name}) ||
|
56
|
+
changes[1].keys.include?(:#{method_name})
|
57
|
+
end
|
58
|
+
EV
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module MetaContent
|
2
|
+
class Query
|
3
|
+
|
4
|
+
def initialize(record)
|
5
|
+
@record = record
|
6
|
+
end
|
7
|
+
|
8
|
+
def h(val)
|
9
|
+
ActiveRecord::Base.sanitize(val)
|
10
|
+
end
|
11
|
+
|
12
|
+
def select_all
|
13
|
+
sql = "SELECT #{qtn}.namespace, #{qtn}.lookup, #{qtn}.value, #{qtn}.int_value FROM #{qtn} WHERE #{qtn}.object_id = #{h(pk)}"
|
14
|
+
results = {}
|
15
|
+
execute(sql).each do |row|
|
16
|
+
results[row[0]] ||= {}
|
17
|
+
results[row[0]][row[1]] = row[2]
|
18
|
+
end
|
19
|
+
results
|
20
|
+
end
|
21
|
+
|
22
|
+
def update_all(changes)
|
23
|
+
sql = "INSERT INTO #{qtn}(namespace,object_id,lookup,value,int_value) VALUES "
|
24
|
+
values = changes.map do |namespace, namespaced_changes|
|
25
|
+
namespaced_changes.map do |k, change|
|
26
|
+
"(#{h(namespace)},#{h(pk)},#{h(k)},#{h(change.value)},#{h(change.int_value)})"
|
27
|
+
end
|
28
|
+
end.flatten
|
29
|
+
sql << values.join(',')
|
30
|
+
sql << " ON DUPLICATE KEY UPDATE value = VALUES(value), int_value = VALUES(int_value)"
|
31
|
+
execute(sql) if values.any?
|
32
|
+
end
|
33
|
+
|
34
|
+
def delete_all(deletes)
|
35
|
+
deletes.each do |namespace, keys|
|
36
|
+
next unless keys.any?
|
37
|
+
key_clause = keys.map{|k| h(k) }.join(',')
|
38
|
+
sql = "DELETE FROM #{qtn} WHERE #{qtn}.object_id = #{h(pk)} AND #{qtn}.namespace = #{h(namespace)} AND #{qtn}.lookup IN (#{key_clause})"
|
39
|
+
execute(sql)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
def qtn
|
46
|
+
"`#{klass.table_name}_meta`"
|
47
|
+
end
|
48
|
+
|
49
|
+
def pk
|
50
|
+
@record.id
|
51
|
+
end
|
52
|
+
|
53
|
+
def klass
|
54
|
+
@record.class
|
55
|
+
end
|
56
|
+
|
57
|
+
def connection
|
58
|
+
if klass.respond_to? :meta_content_connection
|
59
|
+
klass.meta_content_connection
|
60
|
+
else
|
61
|
+
klass.connection
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def execute(sql)
|
66
|
+
connection.execute(sql)
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
module MetaContent
|
2
|
+
class Sanitizer
|
3
|
+
|
4
|
+
def initialize(record)
|
5
|
+
@record = record
|
6
|
+
end
|
7
|
+
|
8
|
+
def from_database(raw_results)
|
9
|
+
sanitized_results = HashWithIndifferentAccess.new
|
10
|
+
raw_results.each do |namespace, results|
|
11
|
+
results.each do |k,v|
|
12
|
+
options = schema[namespace] || {}
|
13
|
+
options = options[k]
|
14
|
+
next unless options
|
15
|
+
sanitized_results[namespace] ||= {}
|
16
|
+
sanitized_results[namespace][k] = sanitize_from_database(v, options[:type] || :string)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
sanitized_results
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_database(changes)
|
23
|
+
sanitized_results = HashWithIndifferentAccess.new
|
24
|
+
changes.each do |namespace, namespaced_changes|
|
25
|
+
namespaced_changes.map do |k,v|
|
26
|
+
options = schema[namespace] || {}
|
27
|
+
options = options[k]
|
28
|
+
next unless options
|
29
|
+
|
30
|
+
sanitized_results[namespace] ||= {}
|
31
|
+
sanitized_results[namespace][k] = sanitize_to_database(v, options[:type] || :string)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
sanitized_results
|
35
|
+
end
|
36
|
+
|
37
|
+
def sanitize(value, type)
|
38
|
+
return nil if value.nil?
|
39
|
+
# simulate writing and reading from database
|
40
|
+
change = sanitize_to_database(value, type)
|
41
|
+
sanitize_from_database(change.value, type)
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
def sanitize_from_database(value, type)
|
47
|
+
return nil if value.nil?
|
48
|
+
|
49
|
+
case type
|
50
|
+
when :integer, :fixnum, :int
|
51
|
+
value.to_i
|
52
|
+
when :float, :number
|
53
|
+
value.to_f
|
54
|
+
when :date
|
55
|
+
Date.parse(value)
|
56
|
+
when :datetime, :time
|
57
|
+
Time.parse(value)
|
58
|
+
when :boolean, :bool
|
59
|
+
value == "true"
|
60
|
+
when :symbol, :sym
|
61
|
+
value.to_sym
|
62
|
+
else
|
63
|
+
value.to_s
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def sanitize_to_database(value, type)
|
68
|
+
int_value = nil
|
69
|
+
float_value = nil
|
70
|
+
case type
|
71
|
+
when :integer, :fixnum, :int
|
72
|
+
begin
|
73
|
+
if value.respond_to? :strftime
|
74
|
+
int_value = value.strftime("%s").to_i
|
75
|
+
else
|
76
|
+
int_value = value.to_i
|
77
|
+
end
|
78
|
+
rescue StandardError => e
|
79
|
+
int_value = 0
|
80
|
+
end
|
81
|
+
str_value = int_value.to_s
|
82
|
+
when :float, :number
|
83
|
+
begin
|
84
|
+
float_value = value.to_f
|
85
|
+
rescue StandardError => e
|
86
|
+
float_value = 0.0
|
87
|
+
end
|
88
|
+
str_value = float_value.to_s
|
89
|
+
when :datetime, :time
|
90
|
+
value = parse_time(value)
|
91
|
+
|
92
|
+
if value.respond_to? :strftime
|
93
|
+
str_value = value.strftime("%Y-%m-%dT%H:%M:%S%:z")
|
94
|
+
int_value = value.strftime("%s").to_i # epoch time
|
95
|
+
else
|
96
|
+
str_value = nil
|
97
|
+
end
|
98
|
+
when :date
|
99
|
+
value = parse_time(value)
|
100
|
+
|
101
|
+
if value.respond_to? :strftime
|
102
|
+
str_value = value.strftime("%Y-%m-%d")
|
103
|
+
int_value = value.strftime("%s").to_i # epoch time
|
104
|
+
else
|
105
|
+
str_value = nil
|
106
|
+
end
|
107
|
+
when :boolean, :bool
|
108
|
+
if value != true && value != false
|
109
|
+
value = ["1", "true", "yes"].include?(value.to_s)
|
110
|
+
end
|
111
|
+
|
112
|
+
if value == true
|
113
|
+
str_value = "true"
|
114
|
+
int_value = 1
|
115
|
+
else
|
116
|
+
str_value = "false"
|
117
|
+
int_value = 0
|
118
|
+
end
|
119
|
+
when :symbol, :sym
|
120
|
+
str_value = value.to_s
|
121
|
+
else
|
122
|
+
str_value = value.to_s
|
123
|
+
end
|
124
|
+
|
125
|
+
int_value ||= float_value.to_i if float_value
|
126
|
+
int_value ||= str_value.length if str_value
|
127
|
+
float_value ||= int_value.to_f
|
128
|
+
|
129
|
+
Change.new(str_value, int_value, float_value)
|
130
|
+
end
|
131
|
+
|
132
|
+
def parse_time(value)
|
133
|
+
result = nil
|
134
|
+
if value.is_a? String
|
135
|
+
result = Time.parse(value) rescue nil # don't care if it blows up, nbd
|
136
|
+
value = value.to_i if value.to_i > 0 && result.nil?
|
137
|
+
end
|
138
|
+
result ||= Time.at(value) if value.is_a? Integer
|
139
|
+
result || value
|
140
|
+
end
|
141
|
+
|
142
|
+
def klass
|
143
|
+
@record.class
|
144
|
+
end
|
145
|
+
|
146
|
+
def schema
|
147
|
+
klass.meta_content_fields
|
148
|
+
end
|
149
|
+
|
150
|
+
|
151
|
+
class Change < Struct.new(:value, :int_value, :float_value)
|
152
|
+
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
data/lib/meta_content.rb
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
require "meta_content/version"
|
2
|
+
|
3
|
+
module MetaContent
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
autoload :Dsl, 'meta_content/dsl'
|
7
|
+
autoload :Query, 'meta_content/query'
|
8
|
+
autoload :Sanitizer, 'meta_content/sanitizer'
|
9
|
+
|
10
|
+
included do
|
11
|
+
class_attribute :meta_content_fields
|
12
|
+
self.meta_content_fields = HashWithIndifferentAccess.new
|
13
|
+
|
14
|
+
after_save :store_meta
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
|
19
|
+
def meta(namespace = nil, &block)
|
20
|
+
dsl = MetaContent::Dsl.new(self, namespace)
|
21
|
+
dsl.instance_eval(&block)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
def reload(*args)
|
27
|
+
@meta = nil
|
28
|
+
super
|
29
|
+
end
|
30
|
+
|
31
|
+
def meta
|
32
|
+
@meta ||= retrieve_meta
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def retrieve_meta
|
39
|
+
return @meta unless @meta.nil?
|
40
|
+
return {} if new_record?
|
41
|
+
|
42
|
+
meta_sanitizer.from_database(meta_query.select_all)
|
43
|
+
end
|
44
|
+
|
45
|
+
def store_meta
|
46
|
+
updates, deletes = meta_changes
|
47
|
+
updates = meta_sanitizer.to_database(updates)
|
48
|
+
meta_query.update_all(updates)
|
49
|
+
meta_query.delete_all(deletes)
|
50
|
+
end
|
51
|
+
|
52
|
+
def meta_query
|
53
|
+
@meta_query ||= ::MetaContent::Query.new(self)
|
54
|
+
end
|
55
|
+
|
56
|
+
def meta_sanitizer
|
57
|
+
@meta_sanitizer ||= ::MetaContent::Sanitizer.new(self)
|
58
|
+
end
|
59
|
+
|
60
|
+
def meta_changes
|
61
|
+
return [{}, {}] if @meta.nil?
|
62
|
+
|
63
|
+
was = self.send(:attribute_was, :meta) || {}
|
64
|
+
is = self.meta
|
65
|
+
|
66
|
+
updates = {}
|
67
|
+
deletes = {}
|
68
|
+
|
69
|
+
is.each do |namespace,namespaced_is|
|
70
|
+
namespaced_was = was[namespace] || {}
|
71
|
+
deletes[namespace] = namespaced_was.keys - namespaced_is.keys
|
72
|
+
updates[namespace] ||= {}
|
73
|
+
|
74
|
+
namespaced_is.each do |k,v|
|
75
|
+
if v == nil
|
76
|
+
deletes[namespace] << k
|
77
|
+
elsif was[k] != v
|
78
|
+
updates[namespace][k] = v
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
[updates, deletes]
|
84
|
+
end
|
85
|
+
|
86
|
+
def default_meta(namespace, field)
|
87
|
+
options = field_meta(namespace, field)
|
88
|
+
options.fetch(:default, nil)
|
89
|
+
end
|
90
|
+
|
91
|
+
def field_meta(namespace, field)
|
92
|
+
options = namespace_meta(namespace)
|
93
|
+
options.fetch(field, {})
|
94
|
+
end
|
95
|
+
|
96
|
+
def namespace_meta(namespace)
|
97
|
+
self.class.meta_content_fields.fetch(namespace, {})
|
98
|
+
end
|
99
|
+
|
100
|
+
def read_namespace(namespace)
|
101
|
+
self.meta.fetch(namespace, {})
|
102
|
+
end
|
103
|
+
|
104
|
+
def read_meta(namespace, field)
|
105
|
+
namespaced_meta = read_namespace(namespace)
|
106
|
+
namespaced_meta.fetch(field, default_meta(namespace, field))
|
107
|
+
end
|
108
|
+
|
109
|
+
def write_meta(namespace, field, value)
|
110
|
+
self.meta[namespace] ||= {}
|
111
|
+
unless self.meta[namespace][field] == value
|
112
|
+
attribute_name = namespace.to_s == 'class' ? field : [namespace, field].join('_')
|
113
|
+
attribute_will_change!(attribute_name)
|
114
|
+
attribute_will_change!(:meta)
|
115
|
+
|
116
|
+
type = field_meta(namespace, field)[:type]
|
117
|
+
self.meta[namespace][field] = meta_sanitizer.sanitize(value, type || :string)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'meta_content/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "meta_content"
|
8
|
+
gem.version = MetaContent::VERSION
|
9
|
+
gem.authors = ["Mike Nelson", "Brian Leonard"]
|
10
|
+
gem.email = ["mike@mikeonrails.com", "brian@bleonard.com"]
|
11
|
+
gem.description = %q{Store your data in a key/value table in MySQL}
|
12
|
+
gem.summary = %q{Store your data in a key/value table in MySQL}
|
13
|
+
gem.homepage = ""
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: meta_content
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.4
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Mike Nelson
|
9
|
+
- Brian Leonard
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2013-04-30 00:00:00.000000000 Z
|
14
|
+
dependencies: []
|
15
|
+
description: Store your data in a key/value table in MySQL
|
16
|
+
email:
|
17
|
+
- mike@mikeonrails.com
|
18
|
+
- brian@bleonard.com
|
19
|
+
executables: []
|
20
|
+
extensions: []
|
21
|
+
extra_rdoc_files: []
|
22
|
+
files:
|
23
|
+
- .gitignore
|
24
|
+
- Gemfile
|
25
|
+
- LICENSE
|
26
|
+
- README.md
|
27
|
+
- Rakefile
|
28
|
+
- lib/meta_content.rb
|
29
|
+
- lib/meta_content/dsl.rb
|
30
|
+
- lib/meta_content/query.rb
|
31
|
+
- lib/meta_content/sanitizer.rb
|
32
|
+
- lib/meta_content/version.rb
|
33
|
+
- meta_content.gemspec
|
34
|
+
homepage: ''
|
35
|
+
licenses: []
|
36
|
+
post_install_message:
|
37
|
+
rdoc_options: []
|
38
|
+
require_paths:
|
39
|
+
- lib
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
47
|
+
none: false
|
48
|
+
requirements:
|
49
|
+
- - ! '>='
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
requirements: []
|
53
|
+
rubyforge_project:
|
54
|
+
rubygems_version: 1.8.24
|
55
|
+
signing_key:
|
56
|
+
specification_version: 3
|
57
|
+
summary: Store your data in a key/value table in MySQL
|
58
|
+
test_files: []
|