has_metadata_column 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Tim Morgan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,106 @@
1
+ Has Metadata Column -- Keep your tables narrow
2
+ ===================
3
+
4
+ | | |
5
+ |:------------|:--------------------------------|
6
+ | **Author** | Tim Morgan |
7
+ | **Version** | 1.0 (Mar 1, 2012) |
8
+ | **License** | Released under the MIT License. |
9
+
10
+ About
11
+ -----
12
+
13
+ So you're wondering why it is you need to make, test, schedule, and deploy a
14
+ whole nother migration just to add one more freeform "Favorite Music"-type
15
+ column to your users model? Wish there were an easier way?
16
+
17
+ There is! Combine all of those "about me," "favorite music," etc. type fields
18
+ into _one_ JSON-serialized `TEXT` column, and now every model can have
19
+ schemaless, migration-free data.
20
+
21
+ If you're interested in moving your metadata out to another table (or database)
22
+ entirely, consider using
23
+ [HasMetadata](https://github.com/riscfuture/has_metadata).
24
+
25
+ This gem does use some "metaprogramming magic" to make the metadata fields
26
+ appear like first-class fields, for purposes of validation and easy access. If
27
+ this is unsettling to you, I recommend using my gem
28
+ [JsonSerialize](https://github.com/riscfuture/json_serialize) instead, as it
29
+ does not get its little fingers all up in ActiveRecord's business.
30
+
31
+ (Why yes, I _do_ have a gem for a every use case!)
32
+
33
+ h2. Installation
34
+
35
+ **Important Note:** This gem is only compatible with Ruby 1.9 and Rails 3.0.
36
+
37
+ Merely add the gem to your Rails project's `Gemfile`:
38
+
39
+ ```` ruby
40
+ gem 'has_metadata_column'
41
+ ````
42
+
43
+ Usage
44
+ -----
45
+
46
+ The first thing to think about is what columns to keep. You will need to keep
47
+ any indexed columns, or any columns you perform lookups or other SQL queries
48
+ with. You should also keep any frequently accessed columns, especially if they
49
+ are small (integers or booleans). Good candidates for the metadata column are
50
+ the `TEXT`- and `VARCHAR`-type columns that you only need to render a page or
51
+ two in your app.
52
+
53
+ You'll need to add a `TEXT` column to your model to store the metadata. You can
54
+ call it what you want; `metadata` is assumed by default.
55
+
56
+ ```` ruby
57
+ t.text :metadata
58
+ ````
59
+
60
+ Next, include the `HasMetadataColumn` module in your model, and call the
61
+ `has_metadata_column` method to define the schema of your metadata. You can get
62
+ more information in the {HasMetadataColumn::ClassMethods#has_metadata_column}
63
+ documentation, but for starters, here's a basic example:
64
+
65
+ ```` ruby
66
+ class User < ActiveRecord::Base
67
+ include HasMetadataColumn
68
+ has_metadata(
69
+ :my_metadata_column,
70
+ about_me: { type: String, length: { maximum: 512 } },
71
+ birthdate: { type: Date, presence: true },
72
+ zipcode: { type: Number, numericality: { greater_than: 9999, less_than: 10_000 } }
73
+ )
74
+ end
75
+ ````
76
+
77
+ As you can see, you pass field names mapped to a hash. The hash describes the
78
+ validation that will be performed, and is in the same format as a call to
79
+ `validates`. In addition to the `EachValidator` keys shown above, you can also
80
+ pass a `type` key, to constrain the Ruby type that can be assigned to the field.
81
+ You can only assign types that can be JSON-serialized: strings, numbers, arrays,
82
+ hashes, dates/times, booleans, and `nil`.
83
+
84
+ Each of these fields (in this case, `about_me`, `birthdate`, and `zipcode`) can
85
+ be accessed and set as first_level methods on an instance of your model:
86
+
87
+ ```` ruby
88
+ user.about_me #=> "I was born in 1982 in Aberdeen. My father was a carpenter from..."
89
+ ````
90
+
91
+ ... and thus, used as part of `form_for` fields:
92
+
93
+ ```` ruby
94
+ form_for user do |f|
95
+ f.text_area :about_me, rows: 5, cols: 80
96
+ end
97
+ ````
98
+
99
+ ... and validations.
100
+
101
+ The only thing you _can't_ do is use these fields in a query, obviously. You
102
+ can't do something like `User.where(zipcode: 90210)`, because that column
103
+ doesn't exist on the `users` table.
104
+
105
+ ... Unless you use PostgreSQL 9.2, and define your metadata column as type
106
+ `json`. Support for _that_ is coming...
@@ -0,0 +1,61 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "has_metadata_column"
8
+ s.version = "1.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Tim Morgan"]
12
+ s.date = "2012-03-02"
13
+ s.description = "Reduce your table width and migration overhead by moving non-indexed columns to a separate metadata column."
14
+ s.email = "git@timothymorgan.info"
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.md"
18
+ ]
19
+ s.files = [
20
+ "README.md",
21
+ "has_metadata_column.gemspec",
22
+ "lib/has_metadata_column.rb"
23
+ ]
24
+ s.homepage = "http://github.com/riscfuture/has_metadata_column"
25
+ s.licenses = ["MIT"]
26
+ s.require_paths = ["lib"]
27
+ s.required_ruby_version = Gem::Requirement.new(">= 1.9")
28
+ s.rubygems_version = "1.8.17"
29
+ s.summary = "Schemaless metadata using JSON columns"
30
+
31
+ if s.respond_to? :specification_version then
32
+ s.specification_version = 3
33
+
34
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
35
+ s.add_runtime_dependency(%q<rails>, [">= 3.0"])
36
+ s.add_runtime_dependency(%q<boolean>, [">= 0"])
37
+ s.add_development_dependency(%q<rspec>, [">= 0"])
38
+ s.add_development_dependency(%q<sqlite3>, [">= 0"])
39
+ s.add_development_dependency(%q<yard>, [">= 0"])
40
+ s.add_development_dependency(%q<redcarpet>, [">= 0"])
41
+ s.add_development_dependency(%q<jeweler>, [">= 0"])
42
+ else
43
+ s.add_dependency(%q<rails>, [">= 3.0"])
44
+ s.add_dependency(%q<boolean>, [">= 0"])
45
+ s.add_dependency(%q<rspec>, [">= 0"])
46
+ s.add_dependency(%q<sqlite3>, [">= 0"])
47
+ s.add_dependency(%q<yard>, [">= 0"])
48
+ s.add_dependency(%q<redcarpet>, [">= 0"])
49
+ s.add_dependency(%q<jeweler>, [">= 0"])
50
+ end
51
+ else
52
+ s.add_dependency(%q<rails>, [">= 3.0"])
53
+ s.add_dependency(%q<boolean>, [">= 0"])
54
+ s.add_dependency(%q<rspec>, [">= 0"])
55
+ s.add_dependency(%q<sqlite3>, [">= 0"])
56
+ s.add_dependency(%q<yard>, [">= 0"])
57
+ s.add_dependency(%q<redcarpet>, [">= 0"])
58
+ s.add_dependency(%q<jeweler>, [">= 0"])
59
+ end
60
+ end
61
+
@@ -0,0 +1,275 @@
1
+ require 'boolean'
2
+
3
+ # @private
4
+ class Object
5
+
6
+ # Creates a deep copy of this object.
7
+ #
8
+ # @raise [TypeError] If the object cannot be deep-copied. All objects that can
9
+ # be marshalled can be deep-copied.
10
+
11
+ def deep_clone
12
+ Marshal.load Marshal.dump(self)
13
+ end
14
+ end
15
+
16
+ # Provides the {ClassMethods#has_metadata_column} method to subclasses of
17
+ # `ActiveRecord::Base`.
18
+
19
+ module HasMetadataColumn
20
+ extend ActiveSupport::Concern
21
+
22
+ # Valid values for the `:type` option.
23
+ TYPES = [ String, Fixnum, Integer, Float, Hash, Array, TrueClass, FalseClass, Boolean, NilClass, Date, Time ]
24
+
25
+ # @private
26
+ def self.metadata_typecast(value, type=nil)
27
+ type ||= String
28
+ raise ArgumentError, "Can't convert objects of type #{type.to_s}" unless TYPES.include?(type)
29
+
30
+ if value.kind_of?(String) then
31
+ if type == Integer or type == Fixnum then
32
+ begin
33
+ return Integer(value.sub(/^0+/, '')) # so that it doesn't think it's in octal
34
+ rescue ArgumentError
35
+ return value
36
+ end
37
+ elsif type == Float then
38
+ begin
39
+ return Float(value)
40
+ rescue ArgumentError
41
+ return value
42
+ end
43
+ elsif type == Boolean then
44
+ return value.parse_bool
45
+ elsif type == Date then
46
+ return nil if value.nil?
47
+ begin
48
+ return Date.parse(value)
49
+ rescue ArgumentError
50
+ return value
51
+ end
52
+ elsif type == Time then
53
+ return nil if value.nil?
54
+ begin
55
+ return Time.parse(value)
56
+ rescue ArgumentError
57
+ return value
58
+ end
59
+ end
60
+ end
61
+ return value
62
+ end
63
+
64
+ # Class methods that are added to your model.
65
+
66
+ module ClassMethods
67
+
68
+ # Defines a set of fields whose values exist in the JSON metadata column.
69
+ # Each key in the `fields` hash is the name of a metadata field, and the
70
+ # value is a set of options to pass to the `validates` method. If you do not
71
+ # want to perform any validation on a field, simply pass `true` as its key
72
+ # value.
73
+ #
74
+ # In addition to the normal `validates` keys, you can also include a `:type`
75
+ # key to restrict values to certain classes, or a `:default` key to specify
76
+ # a value to return for the getter should none be set (normal default is
77
+ # `nil`). See {TYPES} for a list of valid values.
78
+ #
79
+ # @overload has_metadata_column(column, fields)
80
+ # @param [Symbol] column (:metadata) The column containing the metadata
81
+ # information.
82
+ # @param [Hash<Symbol, Hash>] fields A mapping of field names to their
83
+ # validation options (and/or the `:type` key).
84
+ # @raise [ArgumentError] If invalid arguments are given, or an invalid
85
+ # class for the `:type` key.
86
+ # @raise [StandardError] If invalid field names are given (see source).
87
+ #
88
+ # @example Three metadata fields, one basic, one validated, and one type-checked.
89
+ # has_metadata_column(optional: true, required: { presence: true }, number: { type: Fixnum })
90
+
91
+ def has_metadata_column(*args)
92
+ fields = args.extract_options!
93
+ column = args.shift
94
+
95
+ raise ArgumentError, "has_metadata_column takes a column name and a hash of fields" unless args.empty?
96
+ raise "Can't define Rails-magic timestamped columns as metadata" if Rails.version >= '3.2.0' && (fields.keys & [:created_at, :created_on, :updated_at, :updated_on]).any?
97
+ classes = fields.values.select { |o| o[:type] && !TYPES.include?(o[:type]) }
98
+ raise ArgumentError, "#{classes.to_sentence} cannot be serialized to JSON" if classes.any?
99
+
100
+ if !respond_to?(:metadata_column_fields) then
101
+ class_attribute :metadata_column_fields
102
+ self.metadata_column_fields = fields.deep_clone
103
+ class_attribute :metadata_column
104
+ self.metadata_column = column || :metadata
105
+
106
+ after_save :_reset_metadata
107
+
108
+ alias_method_chain :changed_attributes, :metadata_column
109
+ alias_method_chain :attribute_will_change!, :metadata_column
110
+ alias_method_chain :attribute_method?, :metadata
111
+ alias_method_chain :attribute, :metadata
112
+ alias_method_chain :attribute_before_type_cast, :metadata
113
+ alias_method_chain :_attribute, :metadata
114
+ alias_method_chain :attribute=, :metadata
115
+ alias_method_chain :query_attribute, :metadata
116
+ alias_method_chain :reload, :metadata
117
+ else
118
+ raise "Cannot redefine existing metadata column #{self.metadata_column}" if column && column != self.metadata_column
119
+ if metadata_column_fields.slice(*fields.keys) != fields
120
+ raise "Cannot redefine existing metadata fields: #{(fields.keys & self.metadata_column_fields.keys).to_sentence}" unless (fields.keys & self.metadata_column_fields.keys).empty?
121
+ self.metadata_column_fields = self.metadata_column_fields.merge(fields)
122
+ end
123
+ end
124
+
125
+ fields.each do |name, options|
126
+ if options.kind_of?(Hash) then
127
+ type = options.delete(:type)
128
+ type_validate = !options.delete(:skip_type_validation)
129
+ options.delete :default
130
+
131
+ validate do |obj|
132
+ value = obj.send(name)
133
+ errors.add(name, :incorrect_type) if !HasMetadataColumn.metadata_typecast(value, type).kind_of?(type) &&
134
+ (!options[:allow_nil] || (options[:allow_nil] && !value.nil?)) &&
135
+ (!options[:allow_blank] || (options[:allow_blank] && !value.blank?))
136
+ end if type && type_validate
137
+ validates(name, options) unless options.empty? or (options.keys - [:allow_nil, :allow_blank]).empty?
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ # @private
144
+ def as_json(options={})
145
+ options ||= Hash.new # the JSON encoder can sometimes give us nil options?
146
+ options[:except] = Array.wrap(options[:except]) + [ self.class.metadata_column ]
147
+ options[:methods] = Array.wrap(options[:methods]) + self.class.metadata_column_fields.keys - options[:except].map(&:to_sym)
148
+ super options
149
+ end
150
+
151
+ # @private
152
+ def to_xml(options={})
153
+ options[:except] = Array.wrap(options[:except]) + [ self.class.metadata_column ]
154
+ options[:methods] = Array.wrap(options[:methods]) + self.class.metadata_column_fields.keys - options[:except].map(&:to_sym)
155
+ super options
156
+ end
157
+
158
+ # @private
159
+ def assign_multiparameter_attributes(pairs)
160
+ fake_attributes = pairs.select { |(field, _)| self.class.metadata_column_fields.include? field[0, field.index('(')].to_sym }
161
+
162
+ fake_attributes.group_by { |(field, _)| field[0, field.index('(')] }.each do |field_name, parts|
163
+ options = self.class.metadata_column_fields[field_name.to_sym]
164
+ if options[:type] then
165
+ args = parts.each_with_object([]) do |(part_name, value), ary|
166
+ part_ann = part_name[part_name.index('(') + 1, part_name.length]
167
+ index = part_ann.to_i - 1
168
+ raise "Out-of-bounds multiparameter argument index" unless index >= 0
169
+ ary[index] = if value.blank? then
170
+ nil
171
+ elsif part_ann.ends_with?('i)') then
172
+ value.to_i
173
+ elsif part_ann.ends_with?('f)') then
174
+ value.to_f
175
+ else
176
+ value
177
+ end
178
+ end
179
+ send :"#{field_name}=", options[:type].new(*args) unless args.empty?
180
+ else
181
+ raise "#{field_name} has no type and cannot be used for multiparameter assignment"
182
+ end
183
+ end
184
+
185
+ super(pairs - fake_attributes)
186
+ end
187
+
188
+ # @private
189
+ def inspect
190
+ "#<#{self.class.to_s} #{attributes.except(self.class.metadata_column.to_s).merge(_metadata_hash.try(:stringify_keys) || {}).map { |k, v| "#{k}: #{v.inspect}" }.join(', ')}>"
191
+ end
192
+
193
+ # @private
194
+ def reload_with_metadata
195
+ res = reload_without_metadata
196
+ @_metadata_hash = nil
197
+ @_changed_metadata = nil
198
+ res
199
+ end
200
+
201
+ private
202
+
203
+ def changed_attributes_with_metadata_column
204
+ changed_attributes_without_metadata_column.merge(_changed_metadata)
205
+ end
206
+
207
+ def attribute_will_change_with_metadata_column!(attr)
208
+ unless attribute_names.include?(attr)
209
+ send :"#{self.class.metadata_column}_will_change!"
210
+ end
211
+ attribute_will_change_without_metadata_column! attr
212
+ end
213
+
214
+ def _metadata_hash
215
+ @_metadata_hash ||= begin
216
+ send(self.class.metadata_column) ? JSON.parse(send(self.class.metadata_column)) : {}
217
+ end
218
+ end
219
+
220
+ def _changed_metadata
221
+ @_changed_metadata ||= {}
222
+ end
223
+
224
+ ## ATTRIBUTE MATCHER METHODS
225
+
226
+ def attribute_with_metadata(attr)
227
+ return attribute_without_metadata(attr) unless self.class.metadata_column_fields.include?(attr.to_sym)
228
+
229
+ options = self.class.metadata_column_fields[attr.to_sym] || {}
230
+ default = options.include?(:default) ? options[:default] : nil
231
+ _metadata_hash.include?(attr) ? HasMetadataColumn.metadata_typecast(_metadata_hash[attr], options[:type]) : default
232
+ end
233
+
234
+ def attribute_before_type_cast_with_metadata(attr)
235
+ return attribute_before_type_cast_without_metadata(attr) unless self.class.metadata_column_fields.include?(attr.to_sym)
236
+ options = self.class.metadata_column_fields[attr.to_sym] || {}
237
+ default = options.include?(:default) ? options[:default] : nil
238
+ _metadata_hash.include?(attr) ? _metadata_hash[attr] : default
239
+ end
240
+
241
+ def _attribute_with_metadata(attr)
242
+ return _attribute_without_metadata(attr) unless self.class.metadata_column_fields.include?(attr.to_sym)
243
+ attribute_with_metadata attr
244
+ end
245
+
246
+ def attribute_with_metadata=(attr, value)
247
+ return send(:attribute_without_metadata=, attr, value) unless self.class.metadata_column_fields.include?(attr.to_sym)
248
+
249
+ options = self.class.metadata_column_fields[attr.to_sym] || {}
250
+ attribute_will_change! attr
251
+ @_metadata_hash ||= {}
252
+ old = @_metadata_hash[attr.to_s]
253
+ send :"#{self.class.metadata_column}=", @_metadata_hash.merge(attr.to_s => value).to_json
254
+ @_metadata_hash = nil
255
+ @_changed_metadata[attr] = old
256
+ value
257
+ end
258
+
259
+ def query_attribute_with_metadata(attr)
260
+ return query_attribute_without_metadata(attr) unless self.class.metadata_column_fields.include?(attr.to_sym)
261
+ return false unless (value = send(attr))
262
+ options = self.class.metadata_column_fields[attr.to_sym] || {}
263
+ type = options[:type] || String
264
+ return !value.to_i.zero? if type.ancestors.include?(Numeric)
265
+ return !value.blank?
266
+ end
267
+
268
+ def attribute_method_with_metadata?(attr)
269
+ self.class.metadata_column_fields.include?(attr.to_sym) || attribute_method_without_metadata?(attr)
270
+ end
271
+
272
+ def _reset_metadata
273
+ @_changed_metadata = {}
274
+ end
275
+ end
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: has_metadata_column
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Tim Morgan
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-03-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ requirement: &70347550669760 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '3.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70347550669760
25
+ - !ruby/object:Gem::Dependency
26
+ name: boolean
27
+ requirement: &70347550669280 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70347550669280
36
+ - !ruby/object:Gem::Dependency
37
+ name: rspec
38
+ requirement: &70347550668800 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70347550668800
47
+ - !ruby/object:Gem::Dependency
48
+ name: sqlite3
49
+ requirement: &70347550668320 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *70347550668320
58
+ - !ruby/object:Gem::Dependency
59
+ name: yard
60
+ requirement: &70347550667840 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *70347550667840
69
+ - !ruby/object:Gem::Dependency
70
+ name: redcarpet
71
+ requirement: &70347550667360 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *70347550667360
80
+ - !ruby/object:Gem::Dependency
81
+ name: jeweler
82
+ requirement: &70347551403380 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: *70347551403380
91
+ description: Reduce your table width and migration overhead by moving non-indexed
92
+ columns to a separate metadata column.
93
+ email: git@timothymorgan.info
94
+ executables: []
95
+ extensions: []
96
+ extra_rdoc_files:
97
+ - LICENSE.txt
98
+ - README.md
99
+ files:
100
+ - README.md
101
+ - has_metadata_column.gemspec
102
+ - lib/has_metadata_column.rb
103
+ - LICENSE.txt
104
+ homepage: http://github.com/riscfuture/has_metadata_column
105
+ licenses:
106
+ - MIT
107
+ post_install_message:
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ none: false
113
+ requirements:
114
+ - - ! '>='
115
+ - !ruby/object:Gem::Version
116
+ version: '1.9'
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ none: false
119
+ requirements:
120
+ - - ! '>='
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ requirements: []
124
+ rubyforge_project:
125
+ rubygems_version: 1.8.17
126
+ signing_key:
127
+ specification_version: 3
128
+ summary: Schemaless metadata using JSON columns
129
+ test_files: []