Skip to content

Commit

Permalink
[ACTIVEMODEL] Added the support for casting model properties as Ruby …
Browse files Browse the repository at this point in the history
…objects

In Tire::Persistence, you define a model property to be casted
as a custom Ruby class instance.

Currently, the implementation expects your class to take a Hash of attributes
on initialization. (There are plans to support custom initialization logic.)

You can cast either single values (see `Author` in the example below),
or collections of values (see `[Comment]`). The behaviour was inspired
by the CouchRest-Model gem.

Also, all strings which conform to the UTC time format are automatically
converted to Time objects.

Also, all Hashes are automatically converted to Hashr [https://rubygems.org/gems/hashr]
instances, allowing easy "dot-style" access to nested hash properties.

Example:

    class Article
      include Tire::Model::Persistence

      validates_presence_of :title, :author

      property :title,        :analyzer => 'snowball'
      property :published_on, :type => 'date'
      property :tags,         :default => [], :analyzer => 'keyword'
      property :author,       :class => Author
      property :comments,     :class => [Comment]
    end

See the test suite for more information.
  • Loading branch information
karmi committed Nov 29, 2011
1 parent 1db7c86 commit 91e39fb
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 12 deletions.
22 changes: 11 additions & 11 deletions README.markdown
Expand Up @@ -671,12 +671,11 @@ Well, things stay mostly the same:
include Tire::Model::Search
include Tire::Model::Callbacks

# Let's use a different index name so stuff doesn't get mixed up
# Let's use a different index name so stuff doesn't get mixed up.
#
index_name 'mongo-articles'

# These Mongo guys sure do some funky stuff with their IDs
# in +serializable_hash+, let's fix it.
# These Mongo guys sure do get funky with their IDs in +serializable_hash+, let's fix it.
#
def to_indexed_json
self.to_json
Expand All @@ -696,14 +695,16 @@ _Tire_ implements not only _searchable_ features, but also _persistence_ feature

Well, because you're tired of database migrations and lots of hand-holding with your
database to store stuff like `{ :name => 'Tire', :tags => [ 'ruby', 'search' ] }`.
Because what you need is to just dump a JSON-representation of your data into a database and
load it back when needed.
Because all you need, really, is to just dump a JSON-representation of your data into a database and load it back again.
Because you've noticed that _searching_ your data is a much more effective way of retrieval
then constructing elaborate database query conditions.
Because you have _lots_ of data and want to use _ElasticSearch's_
advanced distributed features.
Because you have _lots_ of data and want to use _ElasticSearch's_ advanced distributed features.

To use the persistence features, just include the `Tire::Persistence` module in your class and define the properties (like with _CouchDB_- or _MongoDB_-based models):
All good reasons to use _ElasticSearch_ as a schema-free and highly-scalable storage and retrieval/aggregation engine for your data.

To use the persistence mode, we'll include the `Tire::Persistence` module in our class and define its properties;
we can add the standard mapping declarations, set default values, or define casting for the property to create
lightweight associations between the models.

```ruby
class Article
Expand All @@ -714,12 +715,11 @@ To use the persistence features, just include the `Tire::Persistence` module in
property :title, :analyzer => 'snowball'
property :published_on, :type => 'date'
property :tags, :default => [], :analyzer => 'keyword'
property :author, :class => Author
property :comments, :class => [Comment]
end
```

Of course, not all validations or `ActionPack` helpers will be available to your models, but if you can live with that,
you've just got a schema-free, highly-scalable storage and retrieval engine for your data.

Please be sure to peruse the [integration test suite](https://github.com/karmi/tire/tree/master/test/integration)
for examples of the API and _ActiveModel_ integration usage.

Expand Down
33 changes: 32 additions & 1 deletion lib/tire/model/persistence/attributes.rb
Expand Up @@ -49,6 +49,9 @@ def property(name, options = {})
property_defaults[name.to_sym] = default_value
end

# Save property casting (when relevant):
property_types[name.to_sym] = options[:class] if options[:class]

# Store mapping for the property:
mapping[name] = options
self
Expand All @@ -62,6 +65,10 @@ def property_defaults
@property_defaults ||= {}
end

def property_types
@property_types ||= {}
end

private

def define_query_method name
Expand Down Expand Up @@ -93,7 +100,31 @@ def has_attribute?(name)
alias :has_property? :has_attribute?

def __update_attributes(attributes)
attributes.each { |name, value| send "#{name}=", value }
attributes.each { |name, value| send "#{name}=", __cast_value(name, value) }
end

# Casts the values according to the <tt>:class</tt> option set when
# defining the property, cast Hashes as Hashr[http://rubygems.org/gems/hashr]
# instances and automatically convert UTC formatted strings to Time.
#
def __cast_value(name, value)
case

when klass = self.class.property_types[name.to_sym]
if klass.is_a?(Array) && value.is_a?(Array)
value.map { |v| klass.first.new(v) }
else
klass.new(value)
end

when value.is_a?(Hash)
Hashr.new(value)

else
# Strings formatted as <http://en.wikipedia.org/wiki/ISO8601> are automatically converted to Time
value = Time.parse(value) if value.is_a?(String) && value =~ /^\d{4}[\/\-]\d{2}[\/\-]\d{2}T\d{2}\:\d{2}\:\d{2}Z$/
value
end
end

end
Expand Down
28 changes: 28 additions & 0 deletions test/models/persistent_article_with_casting.rb
@@ -0,0 +1,28 @@
class Author
attr_accessor :first_name, :last_name
def initialize(attributes)
@first_name = HashWithIndifferentAccess.new(attributes)[:first_name]
@last_name = HashWithIndifferentAccess.new(attributes)[:last_name]
end
end

class Comment
def initialize(params); @attributes = HashWithIndifferentAccess.new(params); end
def method_missing(method_name, *arguments); @attributes[method_name]; end
def as_json(*); @attributes; end
end

class PersistentArticleWithCastedItem
include Tire::Model::Persistence

property :title
property :author, :class => Author
property :stats
end

class PersistentArticleWithCastedCollection
include Tire::Model::Persistence

property :title
property :comments, :class => [Comment]
end
2 changes: 2 additions & 0 deletions test/test_helper.rb
Expand Up @@ -10,6 +10,8 @@
require 'turn' unless ENV["TM_FILEPATH"] || ENV["CI"]
require 'mocha'

require 'active_support/core_ext/hash/indifferent_access'

require 'tire'

Dir[File.dirname(__FILE__) + '/models/**/*.rb'].each { |m| require m }
Expand Down
31 changes: 31 additions & 0 deletions test/unit/model_persistence_test.rb
Expand Up @@ -255,6 +255,37 @@ class << a

end

context "with casting" do

should "cast the value as custom class" do
article = PersistentArticleWithCastedItem.new :title => 'Test',
:author => { :first_name => 'John', :last_name => 'Smith' }
assert_instance_of Author, article.author
assert_equal 'John', article.author.first_name
end

should "cast the value as collection of custom classes" do
article = PersistentArticleWithCastedCollection.new :title => 'Test',
:comments => [{:nick => '4chan', :body => 'WHY U NO?'}]
assert_instance_of Array, article.comments
assert_instance_of Comment, article.comments.first
assert_equal '4chan', article.comments.first.nick
end

should "automatically format strings in UTC format as Time" do
article = PersistentArticle.new :published_on => '2011-11-01T23:00:00Z'
assert_instance_of Time, article.published_on
assert_equal 2011, article.published_on.year
end

should "cast anonymous Hashes as Hashr instances" do
article = PersistentArticleWithCastedItem.new :stats => { :views => 100, :meta => { :tags => 'A' } }
assert_equal 100, article.stats.views
assert_equal 'A', article.stats.meta.tags
end

end

context "when creating" do

should "save the document with generated ID in the database" do
Expand Down

0 comments on commit 91e39fb

Please sign in to comment.