has_metadata_column 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.
@@ -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: []