tableless_model 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in tableless_model.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,50 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ tableless_model (0.0.1)
5
+ activerecord
6
+ hashie
7
+ validatable
8
+
9
+ GEM
10
+ remote: http://rubygems.org/
11
+ specs:
12
+ activemodel (3.0.3)
13
+ activesupport (= 3.0.3)
14
+ builder (~> 2.1.2)
15
+ i18n (~> 0.4)
16
+ activerecord (3.0.3)
17
+ activemodel (= 3.0.3)
18
+ activesupport (= 3.0.3)
19
+ arel (~> 2.0.2)
20
+ tzinfo (~> 0.3.23)
21
+ activesupport (3.0.3)
22
+ arel (2.0.6)
23
+ builder (2.1.2)
24
+ diff-lcs (1.1.2)
25
+ hashie (0.4.0)
26
+ i18n (0.5.0)
27
+ mocha (0.9.10)
28
+ rake
29
+ rake (0.8.7)
30
+ rspec (2.3.0)
31
+ rspec-core (~> 2.3.0)
32
+ rspec-expectations (~> 2.3.0)
33
+ rspec-mocks (~> 2.3.0)
34
+ rspec-core (2.3.1)
35
+ rspec-expectations (2.3.0)
36
+ diff-lcs (~> 1.1.2)
37
+ rspec-mocks (2.3.0)
38
+ tzinfo (0.3.23)
39
+ validatable (1.6.7)
40
+
41
+ PLATFORMS
42
+ ruby
43
+
44
+ DEPENDENCIES
45
+ activerecord
46
+ hashie
47
+ mocha
48
+ rspec
49
+ tableless_model!
50
+ validatable
data/README.rdoc ADDED
@@ -0,0 +1,97 @@
1
+ = Tableless Model
2
+
3
+ This is an extended Hash that has a defined collection of method-like attributes, and only these attributes can be set or read from the hash.
4
+ Optionally, you can also set default values and enforce data types for these attributes.
5
+
6
+ Tableless Model behaves in a similar way to normal ActiveRecord models in that it also supports validations and can be useful, for example, to reduce database complexity in some cases, by removing associations and therefore tables.
7
+
8
+ In particular, by using Tableless Model, you could save tables whenever you have one to one associations between a parent model and a child model containing options, settings, debugging information or any other collection of attributes that belongs uniquely to a single parent object.
9
+
10
+
11
+
12
+ == Installation
13
+
14
+ Tableless Model is available as a Rubygem:
15
+
16
+ gem install tableless_model
17
+
18
+
19
+
20
+ == Usage
21
+
22
+ For example's sake, say we have these two models:
23
+
24
+ 1)
25
+
26
+ class Page < ActiveRecord::Base
27
+
28
+ # having columns such as id, title, etc
29
+
30
+ has_one :seo_settings
31
+
32
+ end
33
+
34
+ 2)
35
+
36
+ class SeoOptions < ActiveRecord::Base
37
+
38
+ set_table_name "seo_options"
39
+
40
+ # having columns such as id, title_tag, meta_description, meta_keywords,
41
+ # noindex, nofollow, noarchive, page_id
42
+
43
+ belongs_to :page
44
+
45
+ end
46
+
47
+
48
+ So that each instance of Page has its own SEO options, and these options/settings only belong to a page, so we have a one to one association, and our database will have the tables "pages", and "seo_options".
49
+
50
+ Using Tableless Model, we could remove the association and the table seo_options altogether, by storing those options in a column in the pages table, in a YAML::serialized form. So the models become:
51
+
52
+
53
+ 1)
54
+
55
+ class Page < ActiveRecord::Base
56
+
57
+ # having columns such as id, title, seo, etc
58
+
59
+ has_tableless :seo => SeoOptions
60
+
61
+ end
62
+
63
+ 2)
64
+
65
+ class SeoOptions < ActiveRecord::TablelessModel
66
+
67
+ attribute :title_tag, :type => :string, :default => ""
68
+ attribute :meta_description, :type => :string, :default => ""
69
+ attribute :meta_keywords, :type => :string, :default => ""
70
+ attribute :noindex, :type => :boolean, :default => false
71
+ attribute :nofollow, :type => :boolean, :default => false
72
+ attribute :noarchive, :type => :boolean, :default => false
73
+
74
+ end
75
+
76
+
77
+ That's it. Each instance of Page will now store directly its YAML-serialized SEO settings in the column named "seo". This is how the content of that column would look like in the database:
78
+
79
+ --- !map:SeoOptions
80
+ noarchive: false
81
+ meta_description:
82
+ meta_keywords:
83
+ nofollow: false
84
+ title_tag:
85
+ noindex: false
86
+
87
+
88
+
89
+ == TODO
90
+
91
+ * Support for associations
92
+
93
+
94
+ == Authors
95
+
96
+ * Vito Botta ( http://vitobotta.com )
97
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,97 @@
1
+ module Base
2
+ module ClassMethods
3
+
4
+ #
5
+ #
6
+ # Macro to attach a tableless model to a parent, table-based model.
7
+ # The parent model is expected to own a property/column having as name the first argument
8
+ # (or the key if the argument is a hash )
9
+ #
10
+ # Can be used this way:
11
+ #
12
+ # class Parent < ActiveRecord::Base
13
+ #
14
+ # has_tableless :settings
15
+ #
16
+ # # or...
17
+ #
18
+ # has_tableless :settings => ParentSettings
19
+ #
20
+ # end
21
+ #
22
+ #
23
+ # NOTE: the serialized column is expected to be of type string or text in the database
24
+ #
25
+ def has_tableless(column)
26
+ column_name = column.class == Hash ? column.collect{|k,v| k}.first.to_sym : column
27
+
28
+ # if only the column name is given, the tableless model's class is expected to have that name, classified, as class name
29
+ class_type = column.class == Hash ? column.collect{|k,v| v}.last : column.to_s.classify.constantize
30
+
31
+
32
+ # injecting in the parent object a getter and a setter for the
33
+ # attribute that will store an instance of a tableless model
34
+ class_eval do
35
+
36
+ # Making sure the serialized column contains a new instance of the tableless model
37
+ # if it hasn't been set yet
38
+ default_value_for column_name => class_type.new
39
+
40
+ # Telling AR that the column has to store an instance of the given tableless model in
41
+ # YAML serialized format
42
+ serialize column_name, ActiveRecord::TablelessModel
43
+
44
+ # Adding getter for the serialized column,
45
+ # making sure it always returns an instance of the specified tableless
46
+ # model and not just a normal hash or the value of the attribute in the database,
47
+ # which is plain text
48
+ define_method column_name.to_s do
49
+ class_type.new(read_attribute(class_name.to_sym) || {})
50
+ end
51
+
52
+ # Adding setter for the serialized column,
53
+ # making sure it always stores in it an instance of
54
+ # the specified tableless model (as the argument may also be a regular hash)
55
+ define_method "#{column_name.to_s}=" do |value|
56
+ super class_type.new(value)
57
+ end
58
+ end
59
+ end
60
+
61
+
62
+ #
63
+ #
64
+ # Overriding the setter for the serialized column in the AR model,
65
+ # to make sure that, if it is still nil, the column always
66
+ # returns at least a new instance of the specified tableless model
67
+ # having the default values, if any, declared in the tableless model itself
68
+ #
69
+ def default_value_for(property, default_value)
70
+ unless method_defined? "after_initialize_with_default_value_for_#{property.to_s}"
71
+
72
+ unless method_defined? "after_initialize"
73
+ define_method "after_initialize" do |*args|
74
+ end
75
+ end
76
+
77
+ define_method "after_initialize_with_default_value_for_#{property.to_s}" do |*args|
78
+ send("after_initialize_without_default_value_for_#{property.to_s}", *args)
79
+ return unless new_record?
80
+ return unless self.respond_to?(property.to_sym)
81
+
82
+ self.send("#{property.to_s}=".to_sym, self.send(property.to_sym) || default_value)
83
+ end
84
+
85
+ alias_method_chain "after_initialize", "default_value_for_#{property.to_s}"
86
+ end
87
+ end
88
+
89
+ end
90
+ end
91
+
92
+
93
+ # Extending ActiveRecord::Base class with a macro required by the Tableless model,
94
+ # and another one that can be used to serialize a tableless model instance into
95
+ # a parent object's column
96
+
97
+ ActiveRecord::Base.extend Base::ClassMethods
@@ -0,0 +1,81 @@
1
+ module Tableless
2
+ module ClassMethods
3
+
4
+ #
5
+ #
6
+ # Macro to define an attribute of the Tableless model.
7
+ # To be used as follows in a tableless model:
8
+ #
9
+ # class Example < ActiveRecord::TablelessModel
10
+ #
11
+ # attribute :name, :type => :string
12
+ # attribute :active, :type => :boolean, :default => true
13
+ #
14
+ # end
15
+ #
16
+ #
17
+ def attribute(name, options = {})
18
+ # Stringifies all keys... uses a little more memory but it's a bit easier to handle keys internally...
19
+ attribute_name = name.to_s
20
+
21
+ # Defining the new attribute for the tableless model
22
+ self.attributes[attribute_name] = options
23
+
24
+ # Defining method-like getter and setter for the new attribute
25
+ # so it can be used like a regular object's property
26
+ class_eval %Q{
27
+ def #{attribute_name}
28
+ self["#{attribute_name}"]
29
+ end
30
+
31
+ def #{attribute_name}=(value)
32
+ self["#{attribute_name}"] = value
33
+ end
34
+ }
35
+ end
36
+
37
+
38
+ #
39
+ #
40
+ # Initialises @attributes in the context of the class inheriting from the Tableless model
41
+ #
42
+ def inherited(klass)
43
+ super
44
+ (@subclasses ||= Set.new) << klass
45
+ klass.instance_variable_set("@attributes", Hash.new)
46
+ end
47
+
48
+
49
+ #
50
+ #
51
+ # If a data type has been specified for an attribute, its value
52
+ # will be converted accordingly (if necessary) when getting or setting it
53
+ #
54
+ #
55
+ def cast(attribute_name, value)
56
+ return nil if value.nil?
57
+
58
+ type = self.attributes[attribute_name.to_s][:type]
59
+
60
+ return value if type.nil?
61
+
62
+ begin
63
+ case type
64
+ when :string then (value.is_a?(String) ? value : String(value))
65
+ when :integer then (value.is_a?(Integer) ? value : Integer(value))
66
+ when :float then (value.is_a?(Float) ? value : Float(value))
67
+ when :decimal then (value.is_a?(Float) ? value : Float(value))
68
+ when :time then (value.is_a?(Time) ? value : Time.parse(value))
69
+ when :date then (value.is_a?(Date) ? value : Date.parse(value))
70
+ when :datetime then (value.is_a?(DateTime) ? value : DateTime.parse(value))
71
+ when :boolean then (value == true || value == 1 || value.to_s =~ /^(true|1)$/i)
72
+ else value
73
+ end
74
+ rescue Exception => e
75
+ raise StandardError, "Invalid value '#{value.inspect}' for attribute #{attribute_name} - expected data type is #{type} but value is a #{value.class} (Exception details: #{e})"
76
+ value
77
+ end
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,80 @@
1
+ module Tableless
2
+ module InstanceMethods
3
+
4
+ #
5
+ #
6
+ # On initialising an instance of a tableless model,
7
+ # sets the default values for all the attributes defined.
8
+ # Optionally, initialises the tableless model with the values
9
+ # specified as arguments, instead, overriding the default values
10
+ #
11
+ #
12
+ def initialize(init_attributes = {}, &block)
13
+ super &block
14
+
15
+ self.class.attributes.each_pair {|attribute_name, options| self.send("#{attribute_name}=", options[:default])}
16
+ init_attributes.each_pair {|k,v| self.send("#{k}=", v)} if init_attributes
17
+ end
18
+
19
+
20
+ #
21
+ #
22
+ # Returns true if the method name specified corresponds
23
+ # to the key of an attribute defined for the tableless model
24
+ #
25
+ #
26
+ def respond_to?(method_name)
27
+ key?(method_name) ? true : super
28
+ end
29
+
30
+
31
+ #
32
+ #
33
+ # Overriding getter for the underlying hash keys
34
+ # so that only the defined attributes can be read
35
+ #
36
+ def [](attribute_name)
37
+ raise NoMethodError, "The attribute #{attribute_name} is undefined" unless self.class.attributes.has_key? attribute_name.to_s
38
+ self.class.cast(attribute_name, super(attribute_name.to_s))
39
+ end
40
+
41
+
42
+ #
43
+ #
44
+ # Overriding setter for the underlying hash keys
45
+ # so that only the defined attributes can be set
46
+ #
47
+ def []=(attribute_name, value)
48
+ raise NoMethodError, "The attribute #{attribute_name} is undefined" unless self.class.attributes.has_key? attribute_name.to_s
49
+ super attribute_name.to_s, self.class.cast(attribute_name, value)
50
+ end
51
+
52
+
53
+ #
54
+ #
55
+ # The Hash object displays inspect information in the format
56
+ #
57
+ # "{:a=>1, :b=>2}"
58
+ #
59
+ # to make the tableless model look a bit more like regular models,
60
+ # it shows instead the inspect information in this format:
61
+ #
62
+ # "<#MyTablelessModel a=1 b=2>"
63
+ #
64
+ def inspect
65
+ "<##{self.class.to_s}" << self.keys.sort.inject(""){|result, k| result << " #{k}=#{self[k].inspect}"; result } << ">"
66
+ end
67
+
68
+
69
+ #
70
+ #
71
+ # Ensures that when merging with a given hash
72
+ # all the keys are stringified as the keys are always handled
73
+ # as strings in the tableless model
74
+ #
75
+ def merge(hash)
76
+ super hash.stringify_keys
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,3 @@
1
+ module TablelessModel
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,30 @@
1
+ require "validatable"
2
+ require File.expand_path(File.join(File.dirname(__FILE__), "activerecord/base/class_methods"))
3
+
4
+ Dir[File.join(File.dirname(__FILE__), "tableless_model/*rb")].each {|f| require File.expand_path(f)}
5
+
6
+ module ActiveRecord
7
+
8
+ # TablelessModel class is basically an Hash with method-like keys that must be defined in advance
9
+ # as for an ActiveRecord model, but without a table. Trying to set new keys not defined at class level
10
+ # result in NoMethodError raised
11
+
12
+ class TablelessModel < Hash
13
+
14
+ extend Tableless::ClassMethods
15
+ include Tableless::InstanceMethods
16
+
17
+ #
18
+ #
19
+ # Exposes an accessors that will store the names of the attributes defined for the Tableless model,
20
+ # and their default values
21
+ # This accessor is an instance of Set defined in the inheriting class (see self.inherited)
22
+ #
23
+ #
24
+ class << self
25
+ attr_reader :attributes
26
+ end
27
+
28
+ end
29
+ end
30
+
@@ -0,0 +1,8 @@
1
+ require "lib/tableless_model"
2
+
3
+ describe ActiveRecord::TablelessModel do
4
+ it "should say bla" do
5
+ true
6
+ end
7
+
8
+ end
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "tableless_model/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "tableless_model"
7
+ s.version = TablelessModel::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Vito Botta"]
10
+ s.email = ["vito@botta.name"]
11
+ s.homepage = "http://rubygems.org/gems/tableless_model"
12
+ s.summary = %q{A serialisable and validatable table-less model with support for associations, useful to store settings, options, etc in a serialized form in a parent object}
13
+ s.description = %q{A serialisable and validatable table-less model with support for associations, useful to store settings, options, etc in a serialized form in a parent object}
14
+
15
+ s.add_dependency "hashie"
16
+ s.add_dependency "validatable"
17
+
18
+ s.add_development_dependency "rspec"
19
+ s.add_development_dependency "mocha"
20
+
21
+ s.rubyforge_project = "tableless_model"
22
+
23
+ s.files = `git ls-files`.split("\n")
24
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
25
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
26
+ s.require_paths = ["lib"]
27
+ end
metadata ADDED
@@ -0,0 +1,134 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tableless_model
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
+ - Vito Botta
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-01-02 00:00:00 +00:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: hashie
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: validatable
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: rspec
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ hash: 3
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ type: :development
62
+ version_requirements: *id003
63
+ - !ruby/object:Gem::Dependency
64
+ name: mocha
65
+ prerelease: false
66
+ requirement: &id004 !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ hash: 3
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ type: :development
76
+ version_requirements: *id004
77
+ description: A serialisable and validatable table-less model with support for associations, useful to store settings, options, etc in a serialized form in a parent object
78
+ email:
79
+ - vito@botta.name
80
+ executables: []
81
+
82
+ extensions: []
83
+
84
+ extra_rdoc_files: []
85
+
86
+ files:
87
+ - .gitignore
88
+ - Gemfile
89
+ - Gemfile.lock
90
+ - README.rdoc
91
+ - Rakefile
92
+ - lib/activerecord/base/class_methods.rb
93
+ - lib/tableless_model.rb
94
+ - lib/tableless_model/class_methods.rb
95
+ - lib/tableless_model/instance_methods.rb
96
+ - lib/tableless_model/version.rb
97
+ - spec/tableless_model_spec.rb
98
+ - tableless_model.gemspec
99
+ has_rdoc: true
100
+ homepage: http://rubygems.org/gems/tableless_model
101
+ licenses: []
102
+
103
+ post_install_message:
104
+ rdoc_options: []
105
+
106
+ require_paths:
107
+ - lib
108
+ required_ruby_version: !ruby/object:Gem::Requirement
109
+ none: false
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ hash: 3
114
+ segments:
115
+ - 0
116
+ version: "0"
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ none: false
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ hash: 3
123
+ segments:
124
+ - 0
125
+ version: "0"
126
+ requirements: []
127
+
128
+ rubyforge_project: tableless_model
129
+ rubygems_version: 1.4.1
130
+ signing_key:
131
+ specification_version: 3
132
+ summary: A serialisable and validatable table-less model with support for associations, useful to store settings, options, etc in a serialized form in a parent object
133
+ test_files:
134
+ - spec/tableless_model_spec.rb