freeb 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +327 -0
  3. data/Rakefile +38 -0
  4. data/app/models/freebase_model_relation.rb +6 -0
  5. data/app/models/freebase_topic.rb +9 -0
  6. data/app/models/freebase_topic_relation.rb +6 -0
  7. data/db/migrate/20121226072811_create_freebase_topics.rb +12 -0
  8. data/db/migrate/20121226073507_create_freebase_topic_relations.rb +15 -0
  9. data/db/migrate/20121226185919_create_freebase_model_relations.rb +15 -0
  10. data/lib/freeb.rb +28 -0
  11. data/lib/freeb/api.rb +91 -0
  12. data/lib/freeb/config.rb +25 -0
  13. data/lib/freeb/converter.rb +60 -0
  14. data/lib/freeb/dsl.rb +25 -0
  15. data/lib/freeb/engine.rb +5 -0
  16. data/lib/freeb/exceptions.rb +3 -0
  17. data/lib/freeb/model_config.rb +172 -0
  18. data/lib/freeb/models.rb +175 -0
  19. data/lib/freeb/topic.rb +57 -0
  20. data/lib/freeb/version.rb +3 -0
  21. data/lib/generators/freeb/migration_generator.rb +24 -0
  22. data/lib/generators/freeb/templates/migration.rb +13 -0
  23. data/test/dummy/README.rdoc +261 -0
  24. data/test/dummy/Rakefile +7 -0
  25. data/test/dummy/app/assets/javascripts/application.js +15 -0
  26. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  27. data/test/dummy/app/controllers/application_controller.rb +3 -0
  28. data/test/dummy/app/helpers/application_helper.rb +2 -0
  29. data/test/dummy/app/models/music_artist.rb +7 -0
  30. data/test/dummy/app/models/us_state.rb +6 -0
  31. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  32. data/test/dummy/config.ru +4 -0
  33. data/test/dummy/config/application.rb +59 -0
  34. data/test/dummy/config/boot.rb +10 -0
  35. data/test/dummy/config/database.yml +25 -0
  36. data/test/dummy/config/environment.rb +5 -0
  37. data/test/dummy/config/environments/development.rb +37 -0
  38. data/test/dummy/config/environments/production.rb +67 -0
  39. data/test/dummy/config/environments/test.rb +37 -0
  40. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  41. data/test/dummy/config/initializers/freeb.rb +3 -0
  42. data/test/dummy/config/initializers/inflections.rb +15 -0
  43. data/test/dummy/config/initializers/mime_types.rb +5 -0
  44. data/test/dummy/config/initializers/secret_token.rb +7 -0
  45. data/test/dummy/config/initializers/session_store.rb +8 -0
  46. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  47. data/test/dummy/config/locales/en.yml +5 -0
  48. data/test/dummy/config/routes.rb +58 -0
  49. data/test/dummy/db/migrate/20121227043946_create_freebase_topics.freeb.rb +13 -0
  50. data/test/dummy/db/migrate/20121227043947_create_freebase_topic_relations.freeb.rb +16 -0
  51. data/test/dummy/db/migrate/20121227043948_create_freebase_model_relations.freeb.rb +16 -0
  52. data/test/dummy/db/migrate/20121227044336_create_music_artists.rb +15 -0
  53. data/test/dummy/db/migrate/20121227170033_create_us_states.rb +13 -0
  54. data/test/dummy/db/schema.rb +71 -0
  55. data/test/dummy/public/404.html +26 -0
  56. data/test/dummy/public/422.html +26 -0
  57. data/test/dummy/public/500.html +25 -0
  58. data/test/dummy/public/favicon.ico +0 -0
  59. data/test/dummy/script/rails +6 -0
  60. data/test/dummy/spec/freeb_spec.rb +32 -0
  61. data/test/dummy/spec/music_artist_spec.rb +58 -0
  62. data/test/dummy/spec/spec_helper.rb +38 -0
  63. data/test/dummy/spec/us_state_spec.rb +11 -0
  64. data/test/freeb_test.rb +7 -0
  65. data/test/test_helper.rb +15 -0
  66. metadata +217 -0
@@ -0,0 +1,28 @@
1
+ module Freeb
2
+ def self.config
3
+ yield Freeb::Config
4
+ end
5
+
6
+ def self.get(freebase_id)
7
+ API.get(freebase_id)
8
+ end
9
+
10
+ def self.search(params)
11
+ API.search(params)
12
+ end
13
+
14
+ def self.topic(mql)
15
+ API.topic(mql)
16
+ end
17
+
18
+ def self.mqlread(mql)
19
+ API.mqlread(mql)
20
+ end
21
+ end
22
+
23
+ directory = File.dirname(File.absolute_path(__FILE__))
24
+ Dir.glob("#{directory}/freeb/*.rb") { |file| require file }
25
+ Dir.glob("#{directory}/generators/freeb/*.rb") { |file| require file }
26
+ Dir.glob("#{directory}/../app/models/*.rb") { |file| require file }
27
+
28
+ ActiveRecord::Base.extend Freeb::Models::ClassMethods
@@ -0,0 +1,91 @@
1
+ require 'httparty'
2
+ require 'uri'
3
+ require 'json'
4
+
5
+ module Freeb
6
+ class API
7
+ @base_url = "https://www.googleapis.com/freebase/v1/"
8
+
9
+ def self.get(id)
10
+ mql = {
11
+ "id" => id,
12
+ "name" => nil
13
+ }
14
+ topic(mql)
15
+ end
16
+
17
+ def self.topic(mql)
18
+ result = mqlread(mql)
19
+ return nil if result.blank?
20
+ if result.is_a?(Array)
21
+ result.collect { |r| Topic.new(r) }
22
+ else
23
+ Topic.new(result)
24
+ end
25
+ end
26
+
27
+ def self.search(params)
28
+ log "Search Request: #{params}"
29
+ url = "#{@base_url}search"
30
+ result = get_result(url, params)
31
+ log "Search Response: #{result}"
32
+ result["result"].collect { |r| Topic.new(r) }
33
+ end
34
+
35
+ def self.mqlread(mql)
36
+ log "MQL Request: #{mql}"
37
+ url = "#{@base_url}mqlread"
38
+ result = get_result(url, :query => mql.to_json)
39
+ log "MQL Response: #{result}"
40
+ return nil if result["result"].blank?
41
+ result["result"]
42
+ end
43
+
44
+ def self.description(id)
45
+ url = "#{@base_url}text#{id}"
46
+ result = get_result(url, nil)
47
+ result["result"]
48
+ end
49
+
50
+ def self.get_result(url, params={})
51
+ unless params.nil?
52
+ params[:key] = Config.settings[:api_key] unless Config.settings[:api_key].blank?
53
+ url = "#{url}?#{params.to_query}"
54
+ end
55
+ if Config.settings[:cache][:is_active]
56
+ cache_key = cache_key_for_url(url)
57
+ result = Rails.cache.read(cache_key)
58
+ if result
59
+ log "Read cache for #{url}"
60
+ result
61
+ else
62
+ result = get_uncached_result(url)
63
+ Rails.cache.write(cache_key, result, :expires_in => Config.settings[:cache][:expires_in])
64
+ log "Wrote cache for #{url}"
65
+ result
66
+ end
67
+ else
68
+ get_uncached_result(url)
69
+ end
70
+ end
71
+
72
+ def self.get_uncached_result(url)
73
+ response = HTTParty.get(url)
74
+ if response.code == 200
75
+ return JSON.parse(response.body)
76
+ end
77
+ raise ResponseException, "Freebase Response #{response.code}: #{JSON.parse(response.body).inspect}"
78
+ nil
79
+ end
80
+
81
+ private
82
+
83
+ def self.cache_key_for_url(url)
84
+ {:gem => "Freeb", :class => "API", :key => "get_result", :url => url}
85
+ end
86
+
87
+ def self.log(message)
88
+ Rails.logger.debug("Freeb: #{message}")
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,25 @@
1
+ require 'active_support/core_ext/numeric'
2
+
3
+ module Freeb
4
+ class Config
5
+ class << self
6
+ attr_reader :settings
7
+ end
8
+
9
+ @settings = {
10
+ :api_key => nil,
11
+ :cache => {
12
+ :is_active => true,
13
+ :expires_in => 1.day
14
+ }
15
+ }
16
+
17
+ def self.api_key(api_key)
18
+ @settings[:api_key] = api_key
19
+ end
20
+
21
+ def self.cache(options)
22
+ @settings[:cache].merge!(options)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,60 @@
1
+ module Freeb
2
+ class Converter
3
+ def self.freebase_id_to_topic(freebase_id, model)
4
+ config = ModelConfig.get(model)
5
+ query_properties = ModelConfig.get_query_properties(model)
6
+ mql = {
7
+ :id => freebase_id,
8
+ :type => config[:type],
9
+ :name => nil
10
+ }.merge(query_properties)
11
+ API.topic(mql)
12
+ end
13
+
14
+ def self.name_to_topic(name, model)
15
+ config = ModelConfig.get(model)
16
+ query_properties = ModelConfig.get_query_properties(model)
17
+ mql = {
18
+ :id => nil,
19
+ :type => config[:type],
20
+ :name => name
21
+ }.merge(query_properties)
22
+ API.topic(mql)
23
+ end
24
+
25
+ def self.topic_to_record_attributes(topic, model)
26
+ config = ModelConfig.get(model)
27
+ attributes = {
28
+ :freebase_id => topic.id,
29
+ :name => topic.name
30
+ }
31
+ config[:properties].each do |key, property_config|
32
+ if property_config[:method].blank?
33
+ attributes[key] = topic[property_config[:id]]
34
+ else
35
+ attributes[key] = model.send(property_config[:method], topic)
36
+ end
37
+ end
38
+ config[:topics].each do |key, topic_config|
39
+ attributes[key] = topic[topic_config[:id]].collect { |hash| topic_hash_to_freebase_topic_record(hash) }
40
+ end
41
+ config[:has_many].each do |key, association_config|
42
+ records = topic[association_config[:id]]
43
+ model = association_config[:class_name].constantize
44
+ attributes[key] = records.collect { |hash|
45
+ topic = Topic.new(hash)
46
+ hash = topic_to_record_attributes(topic, model)
47
+ model.ffind_or_create(hash) }
48
+ end
49
+ attributes
50
+ end
51
+
52
+ def self.topic_hash_to_freebase_topic_record(topic_hash)
53
+ hash = {
54
+ :freebase_id => topic_hash["id"],
55
+ :name => topic_hash["name"]
56
+ }
57
+ record = FreebaseTopic.find_or_create_by_freebase_id(hash)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,25 @@
1
+ module Freeb
2
+ class DSL
3
+ attr_reader :config
4
+
5
+ def initialize
6
+ @config = {}
7
+ end
8
+
9
+ def type(value)
10
+ @config[:type] = value
11
+ end
12
+
13
+ def properties(*args)
14
+ @config[:properties] = args
15
+ end
16
+
17
+ def topics(*args)
18
+ @config[:topics] = args
19
+ end
20
+
21
+ def has_many(*args)
22
+ @config[:has_many] = args
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ module Freeb
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Freeb
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module Freeb
2
+ class ResponseException < Exception; end
3
+ end
@@ -0,0 +1,172 @@
1
+ module Freeb
2
+ class ModelConfig
3
+ @models = {}
4
+
5
+ def self.register(model, options)
6
+ key = model_to_key(model)
7
+ @models[key] = normalize_options(options)
8
+ end
9
+
10
+ def self.get(model)
11
+ key = model_to_key(model)
12
+ @models[key]
13
+ end
14
+
15
+ def self.get_query_properties(model)
16
+ config = get(model)
17
+ query_properties = {}
18
+ raise "Empty Freeb config for #{model}" if config.blank?
19
+ query_properties.merge!(config[:properties].inject({}) { |h, (k, property)| h[property[:id]] = nil; h })
20
+ query_properties.delete("description")
21
+ query_properties.merge!(config[:topics].inject({}) { |h, (k, topic)| h[topic[:id]] = [{:id => nil, :name => nil}]; h })
22
+ query_properties.merge!(get_has_many_properties(model))
23
+ query_properties
24
+ end
25
+
26
+ def self.get_has_many_properties(model)
27
+ config = get(model)
28
+ properties = {}
29
+ config[:has_many].each do |key, association|
30
+ association_class = association[:class_name].classify.constantize
31
+ association_class_config = get(association_class)
32
+ association_properties = {:id => nil, :name => nil}
33
+ association_properties.merge!(get_query_properties(association_class))
34
+ key = association[:id]
35
+ properties[key] = [association_properties]
36
+ end
37
+ properties
38
+ end
39
+
40
+ def self.get_migration_properties(model)
41
+ config = get(model)
42
+ schema = Freeb.mqlread({
43
+ :name => nil,
44
+ :id => config[:type],
45
+ :type => [{:id => "/type/type"}],
46
+ "!/type/property/schema" => [{"/type/property/expected_type" => nil, "id" => nil, "name" => nil}]
47
+ })
48
+ property_types = {}
49
+ schema["!/type/property/schema"].each do |property|
50
+ property_types[property["id"]] = property["/type/property/expected_type"]
51
+ end
52
+ config[:properties].collect do |key, property|
53
+ expected_type = property_types[property[:id]]
54
+ type = case expected_type
55
+ when "/type/boolean"
56
+ :boolean
57
+ when "/type/datetime"
58
+ :datetime
59
+ when "/type/float"
60
+ :float
61
+ when "/type/int"
62
+ :integer
63
+ when "/type/text"
64
+ :text
65
+ else
66
+ :string
67
+ end
68
+ if property[:id] == "description"
69
+ type = "text"
70
+ end
71
+ { :key => key, :type => type }
72
+ end
73
+ end
74
+
75
+ def self.model_to_key(model)
76
+ model.name.underscore.to_sym
77
+ end
78
+
79
+ def self.normalize_options(options)
80
+ normalize_properties(options)
81
+ normalize_topics(options)
82
+ normalize_has_many(options)
83
+ options
84
+ end
85
+
86
+ def self.normalize_properties(options)
87
+ properties = {}
88
+ options[:properties].flatten(1).each do |property|
89
+ if property.is_a?(String)
90
+ properties[property.to_sym] = {
91
+ :key => property,
92
+ :id => key_to_id(property, options)
93
+ }
94
+ elsif property.is_a?(Hash)
95
+ property.each do |key, value|
96
+ if value.is_a?(String)
97
+ properties[key.to_sym] = {
98
+ :key => value,
99
+ :id => key_to_id(value, options)
100
+ }
101
+ elsif value.is_a?(Hash)
102
+ defaults = {
103
+ :key => key,
104
+ :id => key_to_id(key, options)
105
+ }
106
+ properties[key.to_sym] = defaults.merge(value)
107
+ end
108
+ end
109
+ end
110
+ end
111
+ options[:properties] = properties
112
+ end
113
+
114
+ def self.normalize_topics(options)
115
+ topics = {}
116
+ options[:topics] = [options[:topics]] if !options[:topics].is_a?(Array)
117
+ options[:topics].each do |topic|
118
+ if topic.is_a?(String) || topic.is_a?(Symbol)
119
+ topic = topic.to_s
120
+ topics[topic.to_sym] = {
121
+ :key => topic,
122
+ :id => key_to_id(topic, options)
123
+ }
124
+ elsif topic.is_a?(Hash)
125
+ topic.each do |key, value|
126
+ topics[key.to_sym] = {
127
+ :key => key,
128
+ :id => key_to_id(value, options)
129
+ }
130
+ end
131
+ end
132
+ end
133
+ options[:topics] = topics
134
+ end
135
+
136
+ def self.normalize_has_many(options)
137
+ associations = {}
138
+ options[:has_many] = [options[:has_many]] if !options[:has_many].is_a?(Array)
139
+ options[:has_many].each do |association|
140
+ if association.is_a?(String) || association.is_a?(Symbol)
141
+ association = association.to_s
142
+ associations[association.to_sym] = {
143
+ :key => association.to_sym,
144
+ :id => key_to_id(value, options),
145
+ :class_name => association.singularize.camelize
146
+ }
147
+ elsif association.is_a?(Hash)
148
+ association.each do |key, value|
149
+ key = key.to_s
150
+ associations[key.to_sym] = {
151
+ :key => key,
152
+ :id => key_to_id(value, options),
153
+ :class_name => key.singularize.camelize
154
+ }
155
+ end
156
+ end
157
+ end
158
+ options[:has_many] = associations
159
+ end
160
+
161
+ def self.key_to_id(key, options)
162
+ key = key.to_s
163
+ if key == "description"
164
+ key
165
+ elsif key[0,1] == "/"
166
+ key
167
+ else
168
+ "#{options[:type]}/#{key}"
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,175 @@
1
+ module Freeb
2
+ module Models
3
+ module ClassMethods
4
+ def freeb(&block)
5
+ include InstanceMethods
6
+ dsl = DSL.new
7
+ dsl.instance_eval(&block)
8
+ options = ModelConfig.register(self, dsl.config)
9
+
10
+ accessible_attributes = [:freebase_id, :name]
11
+ accessible_attributes += (options[:properties].merge(options[:topics]).merge(options[:has_many])).keys
12
+
13
+ attr_reader :freeb_config
14
+ attr_accessible *accessible_attributes
15
+ validates_presence_of :freebase_id, :name
16
+
17
+ initialize_topic_associations(options)
18
+ initialize_has_many_associations(options)
19
+
20
+ @freeb_config = ModelConfig.get(self)
21
+ end
22
+
23
+ def initialize_topic_associations(options)
24
+ return if options[:topics].blank?
25
+ options[:topics].each do |key, value|
26
+ join_association = :"#{key}_freebase_topic_relations"
27
+ has_many join_association, :as => :subject, :class_name => "FreebaseTopicRelation",
28
+ :conditions => {:property => value[:id]}, :before_add => :"before_add_#{join_association}"
29
+ define_method :"before_add_#{join_association}" do |record|
30
+ record.property = value[:id]
31
+ end
32
+ has_many key, :through => join_association, :source => :freebase_topic
33
+ join_association = :"freebase_topic_relations_#{key}"
34
+ FreebaseTopic.has_many join_association, :class_name => "FreebaseTopicRelation",
35
+ :conditions => {:property => value[:id]}
36
+ FreebaseTopic.has_many self.name.tableize, :through => join_association,
37
+ :source => :subject, :source_type => self.name
38
+ end
39
+ end
40
+
41
+ def initialize_has_many_associations(options)
42
+ return if options[:has_many].blank?
43
+ options[:has_many].each do |key, association|
44
+ # Two-sided polymorphic has_many relationships are supported by Rails, so we'll do this with a HABTM and
45
+ # custom insert SQL. Is there a better approach?
46
+ join_table = "freebase_model_relations"
47
+ has_and_belongs_to_many key, :join_table => join_table,
48
+ :foreign_key => "subject_id", :association_foreign_key => "object_id",
49
+ :conditions => {
50
+ "#{join_table}.subject_type" => self.name,
51
+ "#{join_table}.object_type" => association[:class_name],
52
+ "#{join_table}.property" => association[:id]
53
+ },
54
+ :insert_sql => proc { |object|
55
+ %{INSERT INTO `#{join_table}`
56
+ (`subject_type`, `subject_id`, `property`, `object_type`, `object_id`)
57
+ VALUES
58
+ ("#{self.class.name}", "#{id}", "#{association[:id]}", "#{association[:class_name]}", "#{object.id}" )}
59
+ }
60
+ end
61
+ end
62
+
63
+ def fnew(freebase_id)
64
+ return nil if freebase_id.nil?
65
+ return fnew_with_array(freebase_id) if freebase_id.is_a?(Array)
66
+ topic = Converter.freebase_id_to_topic(freebase_id, self)
67
+ new(Converter.topic_to_record_attributes(topic, self))
68
+ end
69
+
70
+ def fnew_by_name(name)
71
+ return nil if name.nil?
72
+ return names.collect { |name| fnew_by_name(name) } if name.is_a?(Array)
73
+ topic = Converter.name_to_topic(name, self)
74
+ new(Converter.topic_to_record_attributes(topic, self))
75
+ end
76
+
77
+ def fcreate(freebase_id)
78
+ return nil if freebase_id.nil?
79
+ return fcreate_with_array(freebase_id) if freebase_id.is_a?(Array)
80
+ existing = find_by_freebase_id(freebase_id)
81
+ return existing unless existing.blank?
82
+ begin
83
+ topic = Converter.freebase_id_to_topic(freebase_id, self)
84
+ rescue ResponseException
85
+ return nil
86
+ end
87
+ return nil if topic.blank?
88
+ create(Converter.topic_to_record_attributes(topic, self))
89
+ end
90
+
91
+ def fcreate_by_name(name)
92
+ return nil if name.nil?
93
+ if name.is_a?(Array)
94
+ names = name
95
+ return names.collect { |name| fcreate_by_name(name) }
96
+ end
97
+ existing = find_by_name(name)
98
+ return existing unless existing.blank?
99
+ begin
100
+ topic = Converter.name_to_topic(name, self)
101
+ rescue ResponseException
102
+ return nil
103
+ end
104
+ return nil if topic.blank?
105
+ create(Converter.topic_to_record_attributes(topic, self))
106
+ end
107
+
108
+ def fcreate_all
109
+ return fcreate([{}])
110
+ end
111
+
112
+ def ffind_or_create(hash)
113
+ find_or_create_by_freebase_id(hash)
114
+ end
115
+
116
+ protected
117
+
118
+ def fnew_with_array(array)
119
+ if array.first.is_a?(String)
120
+ freebase_ids = array
121
+ freebase_ids.collect { |freebase_id| fnew(freebase_id) }
122
+ elsif array.first.is_a?(Hash)
123
+ fnew_with_mql(array)
124
+ end
125
+ end
126
+
127
+ def fcreate_with_array(array)
128
+ if array.first.is_a?(String)
129
+ freebase_ids = array
130
+ freebase_ids.collect { |freebase_id| fcreate(freebase_id) }
131
+ elsif array.first.is_a?(Hash)
132
+ fcreate_with_mql(array)
133
+ end
134
+ end
135
+
136
+ def fnew_with_mql(mql)
137
+ results = mql_to_results(mql)
138
+ results.collect do |hash|
139
+ topic = Topic.new(hash)
140
+ new(Converter.topic_to_record_attributes(topic, self))
141
+ end
142
+ end
143
+
144
+ def fcreate_with_mql(mql)
145
+ results = mql_to_results(mql)
146
+ results.collect do |hash|
147
+ topic = Topic.new(hash)
148
+ ffind_or_create(Converter.topic_to_record_attributes(topic, self))
149
+ end
150
+ end
151
+
152
+ def mql_to_results(mql)
153
+ mql[0] = mql[0].inject({}){|hash, (k,v)| hash[k.to_s] = v; hash}
154
+ mql[0]["id"] = nil if mql[0]["id"].blank?
155
+ mql[0]["name"] = nil if mql[0]["name"].blank?
156
+ mql[0]["type"] = @freeb_config[:type] if mql[0]["type"].blank?
157
+ API.mqlread(mql)
158
+ end
159
+ end
160
+
161
+ module InstanceMethods
162
+ def fupdate
163
+ topic = Converter.freebase_id_to_topic(freebase_id, self)
164
+ attributes = Converter.topic_to_record_attributes(topic, self)
165
+ update_attributes(attributes)
166
+ end
167
+
168
+ def fimage(options={})
169
+ url = "https://usercontent.googleapis.com/freebase/v1/image#{freebase_id}"
170
+ url << "?#{options.to_query}" unless options.blank?
171
+ url
172
+ end
173
+ end
174
+ end
175
+ end