freeb 0.0.1

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.
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