public
Description: A ruby client for freebase
Clone URL: git://github.com/dustin/ruby-freebase.git
chriseppstein (author)
Sun Feb 03 12:15:22 -0800 2008
commit  a631c2035df055d70560a7a7a248a146cc1ee69f
tree    1dba0c62d357e1b0a8e15588fc2f3dd531c055f3
parent  70ce157b1061accaa86c21b5694dd592dec21ac5
ruby-freebase / lib / freebase.rb
100644 160 lines (147 sloc) 5.73 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# (c) Copyright 2007 Chris Eppstein. All Rights Reserved.
 
require 'rubygems'
require 'activesupport'
require 'net/http'
require 'core_extensions'
 
module Freebase
end
 
require 'freebase/api'
 
module Freebase
  # This module is a namespace scope for the Freebase domains
  module Types
    # Automagically creates modules for domains and classes for types
    # for matching namespaces to Freebase's domain/type naming structure.
    # Shouldn't need to be called manually because this is called by the module
    # whenever the constant is missing.
    def new_freebase_type(name)
      Freebase::Api::Logger.trace {"new freebase module = #{name}"}
      self.const_set(name, Module.new do
        def new_freebase_type(name)
          Freebase::Api::Logger.trace {"new freebase class = #{name}"}
          klass = self.const_set(name, Class.new(Freebase::Base))
          klass.class_eval do
            cattr_accessor :properties, :schema_loaded
          end
          returning(klass) do |tc|
            tc.load_schema! unless tc.schema_loaded?
            Freebase::Api::Logger.trace { "Attempting Mixin include: #{tc.name.sub(/Types/,"Mixins")}" }
            begin
              tc.send(:include, tc.name.sub(/Types/,"Mixins").constantize)
            rescue NameError => e
              Freebase::Api::Logger.trace "failed: #{e}"
            end
          end
        end
 
        module_function :new_freebase_type
      end)
    end
 
    module_function :new_freebase_type
  end
  
  # Add a module within this module that corresponds to a freebase class within Freebase::Types
  # and the methods will be mixed in automatically. E.g.:
  # module Freebase::Mixins::Music
  # module Track
  # def formatted_length
  # "#{self.length.to_i / 60}:#{sprintf("%02i", self.length.to_i % 60)}"
  # end
  # end
  # end
  #
  # Will be mixed in to the Freebase::Types:Music:Track class
  module Mixins
  end
  
  # This is the base class for all dynamically defined Freebase Types.
  class Base < Api::FreebaseResult
    extend Api
    alias_method :attributes, :result
    def self.schema_loaded?
      self.schema_loaded || false
    end
    def self.freebase_type
      @freebase_type ||= self.name["Freebase::Types".length..self.name.length].underscore
    end
    def self.load_schema!
      self.properties = {}
      propobjs = mqlread(:type => '/type/type', :id => self.freebase_type, :properties => [{:name => nil, :id => nil, :type => nil, :expected_type => nil}]).properties
      propobjs.each {|propobj|
        self.properties[propobj.id.split(/\//).last.to_sym] = propobj
      }
      self.schema_loaded = true
    end
    def self.find(*args)
      options = args.extract_options!
      case args.first
      when :first
        raise ArgumentError.new("Too many arguments for find(:first)") if args.size > 1
        find_first(options)
      when :all
        raise ArgumentError.new("Too many arguments for find(:all)") if args.size > 1
        find_all(options)
      end
    end
    def self.add_required_query_attributes(conditions)
      case conditions
      when Array
        conditions.map! {|c| add_required_query_attributes(c)}
      when Hash
        if conditions.delete(:fb_object)
          conditions.reverse_merge!(:type => [], :id => nil) unless conditions.has_key?(:*)
        else
          conditions.reverse_merge!(:type => nil) unless conditions.has_key?(:*)
        end
        conditions.each {|k,v| add_required_query_attributes(v) unless k == :*}
      else
        conditions
      end
    end
    # Don't to call this directly. find(:first, options) will be dispatched here.
    # This method is provided for extensibility
    def self.find_first(options = {})
      conditions = options.fetch(:conditions, {}).reverse_merge(:type => self.freebase_type, :name=>nil, :* => [{}], :limit => 1)
      add_required_query_attributes(conditions)
      self.new(mqlread(conditions, :raw => true))
    end
    
    # Don't to call this directly. find(:all, options) will be dispatched here.
    # This method is provided for extensibility
    def self.find_all(options = {})
      query = options.fetch(:conditions, {}).merge(:type => self.freebase_type, :name=>nil, :* => [{}])
      query[:limit] = options[:limit] if options[:limit]
      add_required_query_attributes(query)
      mqlread([query], :raw => true).map{|i| self.new(i)}
    end
    
    # ActiveRecord:Base-like to_s for the class
    def self.to_s
      if respond_to?(:properties) && !self.properties.blank?
        %Q{#<#{name} #{self.properties.map{|k,v| "#{k}:#{v.expected_type}"}.join(", ")}>}
      else
        "#<#{name}>"
      end
    end
    
    # (re)load all properties of this object
    def reload
      query = {:id => self.id, :type=>self.class.freebase_type, :name=> nil, :limit => 1}
      self.class.properties.each do |k,v|
        query[k] = [{}] unless query.has_key?(k)
      end
      @result = self.class.mqlread(query, :raw => true).symbolize_keys!
      Freebase::Api::Logger.trace { @result.inspect }
      return self
    end
    
    # access the properties of this object, lazy loading associations as required.
    def method_missing(name,*args)
      if self.class.properties.has_key?(name)
        reload unless attributes.has_key?(name)
        resultify attributes[name]
      elsif self.class.properties.has_key?((singularized_name = name.to_s.singularize.to_sym))
        reload unless attributes.has_key?(singularized_name)
        resultify attributes[singularized_name]
      else
        super
      end
    end
    
    # If the object has a name, return it, otherwise the id.
    def to_s
      respond_to?(:name) ? name : id
    end
  end
end