open_graph_reader 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitmodules +3 -0
  3. data/.rspec +2 -0
  4. data/.yardopts +1 -0
  5. data/lib/open_graph_reader.rb +83 -0
  6. data/lib/open_graph_reader/base.rb +57 -0
  7. data/lib/open_graph_reader/builder.rb +100 -0
  8. data/lib/open_graph_reader/definitions.rb +333 -0
  9. data/lib/open_graph_reader/fetcher.rb +82 -0
  10. data/lib/open_graph_reader/object.rb +95 -0
  11. data/lib/open_graph_reader/object/dsl.rb +130 -0
  12. data/lib/open_graph_reader/object/dsl/types.rb +71 -0
  13. data/lib/open_graph_reader/object/registry.rb +54 -0
  14. data/lib/open_graph_reader/parser.rb +85 -0
  15. data/lib/open_graph_reader/parser/graph.rb +136 -0
  16. data/lib/open_graph_reader/version.rb +4 -0
  17. data/spec/fixtures/examples/apple-touch-icon-precomposed.png +0 -0
  18. data/spec/fixtures/examples/apple-touch-icon.png +0 -0
  19. data/spec/fixtures/examples/article-offset.html +25 -0
  20. data/spec/fixtures/examples/article-utc.html +25 -0
  21. data/spec/fixtures/examples/article.html +25 -0
  22. data/spec/fixtures/examples/audio-array.html +27 -0
  23. data/spec/fixtures/examples/audio-url.html +25 -0
  24. data/spec/fixtures/examples/audio.html +24 -0
  25. data/spec/fixtures/examples/book-isbn10.html +27 -0
  26. data/spec/fixtures/examples/book.html +27 -0
  27. data/spec/fixtures/examples/canadian.html +16 -0
  28. data/spec/fixtures/examples/error.html +17 -0
  29. data/spec/fixtures/examples/errors/article-date.html +25 -0
  30. data/spec/fixtures/examples/errors/book-author.html +27 -0
  31. data/spec/fixtures/examples/errors/book.html +27 -0
  32. data/spec/fixtures/examples/errors/gender.html +20 -0
  33. data/spec/fixtures/examples/errors/geo.html +23 -0
  34. data/spec/fixtures/examples/errors/type.html +16 -0
  35. data/spec/fixtures/examples/errors/video-duration.html +42 -0
  36. data/spec/fixtures/examples/favicon.ico +0 -0
  37. data/spec/fixtures/examples/filters/xss-image.html +15 -0
  38. data/spec/fixtures/examples/image-array.html +26 -0
  39. data/spec/fixtures/examples/image-toosmall.html +24 -0
  40. data/spec/fixtures/examples/image-url.html +22 -0
  41. data/spec/fixtures/examples/image.html +21 -0
  42. data/spec/fixtures/examples/index.html +67 -0
  43. data/spec/fixtures/examples/media/audio/1khz.mp3 +0 -0
  44. data/spec/fixtures/examples/media/audio/250hz.mp3 +0 -0
  45. data/spec/fixtures/examples/media/images/1.png +0 -0
  46. data/spec/fixtures/examples/media/images/50.png +0 -0
  47. data/spec/fixtures/examples/media/images/75.png +0 -0
  48. data/spec/fixtures/examples/media/images/icon.png +0 -0
  49. data/spec/fixtures/examples/media/images/logo.png +0 -0
  50. data/spec/fixtures/examples/media/images/train.jpg +0 -0
  51. data/spec/fixtures/examples/media/video/train.flv +0 -0
  52. data/spec/fixtures/examples/media/video/train.mp4 +0 -0
  53. data/spec/fixtures/examples/media/video/train.webm +0 -0
  54. data/spec/fixtures/examples/min.html +14 -0
  55. data/spec/fixtures/examples/nomedia.html +20 -0
  56. data/spec/fixtures/examples/plain.html +10 -0
  57. data/spec/fixtures/examples/profile.html +25 -0
  58. data/spec/fixtures/examples/required.html +20 -0
  59. data/spec/fixtures/examples/robots.txt +4 -0
  60. data/spec/fixtures/examples/sitemap.xml +23 -0
  61. data/spec/fixtures/examples/video-array.html +36 -0
  62. data/spec/fixtures/examples/video-movie.html +42 -0
  63. data/spec/fixtures/examples/video.html +26 -0
  64. data/spec/integration/invalid_examples_spec.rb +69 -0
  65. data/spec/integration/valid_examples_spec.rb +76 -0
  66. data/spec/open_graph_reader_spec.rb +94 -0
  67. data/spec/spec_helper.rb +35 -0
  68. metadata +247 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fb82d8a9867ad4c1cf0258ef4e97f993cccdc6aa
4
+ data.tar.gz: ef67382716657161c2098380bf00c6abc48e6a4e
5
+ SHA512:
6
+ metadata.gz: a60f2d67610ab9d1cd0587c58b92452f07fff77da489eec68c7906af650247bd42e26506a5d323f9d6ebe4b3bb247b7b26aba2dac5389634c660d80801b6f32b
7
+ data.tar.gz: 423fb2fe90278c64bd089579c5165c9ee468663c3d9eeb0661c1dfa0a5adc74f12a8772f7f5bdeff0d15ecb52493887d822e13a68dae31de98f9056636e21666
data/.gitmodules ADDED
@@ -0,0 +1,3 @@
1
+ [submodule "spec/fixtures/examples"]
2
+ path = spec/fixtures/examples
3
+ url = https://github.com/niallkennedy/open-graph-protocol-examples.git
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --no-private
@@ -0,0 +1,83 @@
1
+ require 'uri'
2
+
3
+ begin
4
+ require 'faraday_middleware/response/follow_redirects'
5
+ rescue LoadError; end
6
+
7
+ require 'open_graph_reader/base'
8
+ require 'open_graph_reader/builder'
9
+ require 'open_graph_reader/definitions'
10
+ require 'open_graph_reader/fetcher'
11
+ require 'open_graph_reader/object'
12
+ require 'open_graph_reader/parser'
13
+ require 'open_graph_reader/version'
14
+
15
+ # @todo quirks mode where invalid attributes don't raise?
16
+ # @todo 1.1 compatibility mode?
17
+ # This module provides the main entry to the library. Please see the
18
+ # {file:README.md} for usage examples.
19
+ module OpenGraphReader
20
+ # Fetch the OpenGraph object at the given URL. Raise if there are any issues.
21
+ #
22
+ # @param [URI,#to_s] url The URL of the OpenGraph object to retrieve.
23
+ # @return [Base] The base object from which you can obtain the root objects.
24
+ # @raise [NoOpenGraphDataError] {include:NoOpenGraphDataError}
25
+ # @raise [InvalidObjectError] {include:InvalidObjectError}
26
+ def self.fetch! url
27
+ case url
28
+ when URI
29
+ target = Fetcher.new(url)
30
+ raise NoOpenGraphDataError, "#{url} doesn't contain any HTML" unless target.html?
31
+ parse! target.body, target.url
32
+ else
33
+ fetch! URI.parse(url.to_s)
34
+ end
35
+ end
36
+
37
+ # Parse the OpenGraph object in the given HTML document. Raise if there are any issues.
38
+ #
39
+ # @param [#to_s, Nokogiri::XML::Node] html A HTML document that contains an OpenGraph object.
40
+ # @param [#to_s] origin The source from where the given document was fetched.
41
+ # @return [Base] The base object from which you can obtain the root objects.
42
+ # @raise [NoOpenGraphDataError] {include:NoOpenGraphDataError}
43
+ # @raise [InvalidObjectError] {include:InvalidObjectError}
44
+ def self.parse! html, origin=nil
45
+ parser = Parser.new html
46
+ raise NoOpenGraphDataError, "#{origin || html} does not contain any OpenGraph tags" unless parser.has_tags?
47
+ Builder.new(parser.graph, parser.additional_namespaces).base.tap {|base|
48
+ base.origin = origin.to_s if origin
49
+ }
50
+ end
51
+
52
+ # Convenience wrapper around {OpenGraphReader.fetch!} that swallows the esceptions
53
+ # and returns nil instead.
54
+ #
55
+ # @param [URI,#to_s] url The URL of the OpenGraph object to retrieve.
56
+ # @return [Base, nil] The base object from which you can obtain the root objects.
57
+ # @see OpenGraphReader.fetch!
58
+ def self.fetch url
59
+ fetch! url
60
+ rescue NoOpenGraphDataError, InvalidObjectError
61
+ end
62
+
63
+ # Convenience wrapper around {OpenGraphReader.parse!} that swallows the esceptions
64
+ # and returns nil instead.
65
+ #
66
+ # @param [#to_s] html A HTML document that contains an OpenGraph object.
67
+ # @param [#to_s] origin The source from where the given document was fetched.
68
+ # @return [Base, nil] The base object from which you can obtain the root objects.
69
+ # @see OpenGraphReader.parse!
70
+ def self.parse html, origin=nil
71
+ parse! html, origin
72
+ rescue NoOpenGraphDataError, InvalidObjectError
73
+ end
74
+
75
+ # The target couldn't be fetched, didn't contain any HTML or
76
+ # any OpenGraph tags.
77
+ class NoOpenGraphDataError < StandardError
78
+ end
79
+
80
+ # The target did contain OpenGraph tags, but they're not valid.
81
+ class InvalidObjectError < StandardError
82
+ end
83
+ end
@@ -0,0 +1,57 @@
1
+ require 'forwardable'
2
+
3
+ module OpenGraphReader
4
+ # You get an instance of this class as result of your quest to obtain
5
+ # an OpenGraph object. It simply contains and returns the root objects,
6
+ # most commonly <tt>og</tt>.
7
+ class Base
8
+ extend Forwardable
9
+
10
+ # @!method [](name)
11
+ # Get a root object by name.
12
+ #
13
+ # @param [String] name The name of the root namespace.
14
+ # @return [Object, nil] The corresponding root object if available.
15
+ # @api private
16
+ # @!method []=(name, object)
17
+ # Make a new root object available on this base.
18
+ #
19
+ # @param [String] name The name of the root namespace.
20
+ # @param [Object] object The corresponding root object.
21
+ # @api private
22
+ def_delegators :@bases, :[], :[]=
23
+
24
+ # If available, contains the source location of the document the
25
+ # available objects were parsed from.
26
+ #
27
+ # @return [String, nil]
28
+ attr_reader :origin
29
+
30
+ # Set origin.
31
+ #
32
+ # @api private
33
+ # @see #origin
34
+ attr_writer :origin
35
+
36
+ # @api private
37
+ def initialize
38
+ @bases = {}
39
+ end
40
+
41
+ # @private
42
+ def respond_to_missing?(method, include_private=false)
43
+ @bases.has_key? method.to_s
44
+ end
45
+
46
+ # Makes the found root objects available.
47
+ # @return [Object]
48
+ def method_missing(method, *args, &block)
49
+ name = method.to_s
50
+ if respond_to_missing? name
51
+ @bases[name]
52
+ else
53
+ super(method, *args, &block)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,100 @@
1
+ module OpenGraphReader
2
+ # Convert a {Parser::Graph} into the right hierarchy of {Object}s attached
3
+ # to a {Base}.
4
+ #
5
+ # @todo validate required, verticals
6
+ # @api private
7
+ class Builder
8
+ # Well-known types from
9
+ #
10
+ # @see http://ogp.me
11
+ KNOWN_TYPES = %w(
12
+ website
13
+ music.song
14
+ music.album
15
+ music.playlist
16
+ music.radio_station
17
+ video.movie
18
+ video.episode
19
+ video.tv_show
20
+ video.other
21
+ article
22
+ book
23
+ profile
24
+ ).freeze
25
+
26
+ # Create a new builder.
27
+ #
28
+ # @param [Parser::Graph] graph
29
+ # @param [Array<String>] additional_namespaces Namespaces found in the
30
+ # prefix attribute of the head tag of the HTML document
31
+ # @see Parser#graph
32
+ # @see Parser#additional_namespaces
33
+ def initialize graph, additional_namespaces=[]
34
+ @graph = graph
35
+ @additional_namespaces = additional_namespaces
36
+ end
37
+
38
+ # Build and return the base.
39
+ #
40
+ # @return [Base]
41
+ def base
42
+ base = Base.new
43
+
44
+ type = @graph.fetch 'og:type', 'website'
45
+
46
+ validate_type type
47
+
48
+ @graph.each do |property|
49
+ root, *path, name = property.path
50
+ base[root] ||= Object::Registry[root].new
51
+ object = resolve base[root], root, path
52
+
53
+ if object.respond_to? "#{name}s" # Collection # TODO
54
+ collection = object.public_send "#{name}s" #TODO
55
+ if Object::Registry.registered? property.fullname # of subobjects
56
+ object = Object::Registry[property.fullname].new
57
+ collection << object
58
+ object.content = property.content
59
+ else # of type
60
+ collection << property.content
61
+ end
62
+ elsif Object::Registry.registered? property.fullname # Subobject
63
+ object[name] ||= Object::Registry[property.fullname].new
64
+ object[name].content = property.content
65
+ else # Direct attribute
66
+ object[name] = property.content
67
+ end
68
+ end
69
+
70
+ base
71
+ end
72
+
73
+ private
74
+
75
+ def resolve object, last_namespace, path
76
+ return object if path.empty?
77
+
78
+ next_name = path.shift
79
+ if object.respond_to? "#{next_name}s" # collection # TODO: do not respond_to? with user data
80
+ collection = object.public_send("#{next_name}s") # TODO: do not public_send with user data
81
+ next_object = collection.last
82
+ if next_object.nil? #|| path.empty? # Final namespace or missing previous declaration, create a new collection item
83
+ next_object = Object::Registry[[*last_namespace, next_name].join(':')].new
84
+ collection << next_object
85
+ end
86
+ else
87
+ next_object = object[next_name]
88
+ next_object ||= Object::Registry[[*last_namespace, next_name].join(':')].new
89
+ end
90
+
91
+ next_object
92
+ end
93
+
94
+ def validate_type type
95
+ unless KNOWN_TYPES.include?(type) || @additional_namespaces.include?(type)
96
+ raise InvalidObjectError, "Undefined type #{type}"
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,333 @@
1
+ require 'open_graph_reader/object'
2
+
3
+ module OpenGraphReader
4
+ # @see http://ogp.me/#metadata
5
+ class Og
6
+ include Object
7
+
8
+ namespace :og
9
+
10
+ # @!macro property
11
+ # @return [String]
12
+ string :type, required: true, default: 'website'
13
+
14
+ # @!macro property
15
+ # @return [String]
16
+ string :title, required: true
17
+
18
+ # @!attribute [r] images
19
+ # @return [Array<Image>]
20
+ # @!macro property
21
+ # @return [Image]
22
+ url :image, required: true, collection: true
23
+
24
+ # @!macro property
25
+ # @return [String, nil]
26
+ url :url
27
+
28
+ # @!macro property
29
+ # @return [Audio, nil]
30
+ url :audio
31
+
32
+ # @!macro property
33
+ # @return [String, nil]
34
+ string :description
35
+
36
+ # @!macro property
37
+ # @return [String]
38
+ enum :determiner, ['', 'a', 'an', 'the', 'auto'], default: ''
39
+
40
+ # @!macro property
41
+ # @return [Locale, nil]
42
+ string :locale
43
+
44
+ # @!macro property
45
+ # @return [String, nil]
46
+ string :site_name
47
+
48
+ # @!macro property
49
+ # @return [Video, nil]
50
+ url :video
51
+
52
+ # @see http://ogp.me/#structured
53
+ class Image
54
+ include Object
55
+
56
+ namespace :og, :image
57
+ content :url
58
+
59
+ url :url
60
+
61
+ # @!macro property
62
+ # @return [String, nil]
63
+ url :secure_url
64
+
65
+ # @!macro property
66
+ # @return [String, nil]
67
+ string :type
68
+
69
+ # @!macro property
70
+ # @return [Integer, nil]
71
+ integer :width
72
+
73
+ # @!macro property
74
+ # @return [Integer, nil]
75
+ integer :height
76
+
77
+ # @return [String, nil]
78
+ def url
79
+ secure_url || properties[:url] || content
80
+ end
81
+ end
82
+
83
+ # @see http://ogp.me/#structured
84
+ class Audio
85
+ include Object
86
+
87
+ namespace :og, :audio
88
+ content :url
89
+
90
+ # This property is not listed on http://ogp.me, but commonly found
91
+ url :url
92
+
93
+ # @!macro property
94
+ # @return [String, nil]
95
+ url :secure_url
96
+
97
+ # @!macro property
98
+ # @return [String, nil]
99
+ string :type
100
+
101
+ # @return [String, nil]
102
+ def url
103
+ secure_url || properties[:url] || content
104
+ end
105
+ end
106
+
107
+ # @see http://ogp.me/#metadata
108
+ class Locale
109
+ include Object
110
+
111
+ namespace :og, :locale
112
+ content :string
113
+
114
+ # @!attribute [r] alternates
115
+ # @return [Array<String>]
116
+ # @!macro property
117
+ # @return [String, nil]
118
+ string :alternate, collection: true
119
+ end
120
+
121
+ # @see http://ogp.me/#structured
122
+ class Video
123
+ include Object
124
+
125
+ namespace :og, :video
126
+ content :url
127
+
128
+ # @!macro property
129
+ # @return [String, nil]
130
+ url :secure_url
131
+
132
+ # @!macro property
133
+ # @return [String, nil]
134
+ string :type
135
+
136
+ # @!macro property
137
+ # @return [Integer, nil]
138
+ integer :width
139
+
140
+ # @!macro property
141
+ # @return [Integer, nil]
142
+ integer :height
143
+
144
+ # @return [String, nil]
145
+ def url
146
+ secure_url || content
147
+ end
148
+ end
149
+ end
150
+
151
+ # @see http://ogp.me/#type_profile
152
+ class Profile
153
+ include Object
154
+
155
+ namespace :profile
156
+ content :url
157
+
158
+ # @!macro property
159
+ # @return [String, nil]
160
+ string :first_name
161
+
162
+ # @!macro property
163
+ # @return [String, nil]
164
+ string :last_name
165
+
166
+ # @!macro property
167
+ # @return [String, nil]
168
+ string :username
169
+
170
+ # @!macro property
171
+ # @return [String, nil]
172
+ enum :gender, %w(male female)
173
+
174
+ # @!macro property
175
+ # @return [String, nil]
176
+ # This one only exists because video had to define a video:actor:role,
177
+ # yay for designing a protocol with implementations in mind
178
+ string :role
179
+ end
180
+
181
+ # @see http://ogp.me/#type_article
182
+ class Article
183
+ include Object
184
+
185
+ namespace :article
186
+
187
+ # @!macro property
188
+ # @return [DateTime, nil]
189
+ datetime :published_time
190
+
191
+ # @!macro property
192
+ # @return [DateTime, nil]
193
+ datetime :modified_time
194
+
195
+ # @!macro property
196
+ # @return [DateTime, nil]
197
+ datetime :expiration_time
198
+
199
+ # @todo This one is a reference to another OpenGraph object. Support fetching it?
200
+ # @!attribute [r] authors
201
+ # @return [Array<Profile>]
202
+ # @!macro property
203
+ # @return [Profile, nil]
204
+ url :author, collection: true, to: Profile
205
+
206
+ # @!macro property
207
+ # @return [String, nil]
208
+ string :section
209
+
210
+ # @!attribute [r] tags
211
+ # @return [Array<String>]
212
+ # @!macro property
213
+ # @return [String, nil]
214
+ string :tag, collection: true
215
+ end
216
+
217
+ # @see http://ogp.me/#type_video
218
+ class Video
219
+ include Object
220
+
221
+ namespace :video
222
+
223
+ # @!attribute [r] actors
224
+ # @return [Array<Profile>]
225
+ # @!macro property
226
+ # @return [Profile, nil]
227
+ url :actor, to: Profile, verticals: %w(movie episode tv_show other), collection: true
228
+
229
+ # @!attribute [r] directors
230
+ # @return [Array<Profile>]
231
+ # @!macro property
232
+ # @return [Profile, nil]
233
+ url :director, to: Profile, verticals: %w(movie episode tv_show other), collection: true
234
+
235
+ # @!attribute [r] writers
236
+ # @return [Array<Profile>]
237
+ # @!macro property
238
+ # @return [Profile, nil]
239
+ url :writer, to: Profile, verticals: %w(movie episode tv_show other), collection: true
240
+
241
+ # @!macro property
242
+ # @return [Integer, nil]
243
+ integer :duration, verticals: %w(movie episode tv_show other)
244
+
245
+ # @!macro property
246
+ # @return [DateTime, nil]
247
+ datetime :release_date, verticals: %w(movie episode tv_show other)
248
+
249
+ # @!attribute [r] tags
250
+ # @return [Array<String>]
251
+ # @!macro property
252
+ # @return [String, nil]
253
+ string :tag, verticals: %w(movie episode tv_show other), collection: true
254
+
255
+ # @todo validate that target vertical is video.tv_show ?
256
+ # @!macro property
257
+ # @return [Sring, nil]
258
+ url :series, to: Video, verticals: %w(episode)
259
+ end
260
+
261
+ # @see http://ogp.me/#type_book
262
+ class Book
263
+ include Object
264
+
265
+ namespace :book
266
+
267
+ # @todo This one is a reference to another OpenGraph object. Support fetching it?
268
+ # @!attribute [r] authors
269
+ # @return [Array<Profile>]
270
+ # @!macro property
271
+ # @return [Profile, nil]
272
+ url :author, collection: true, to: Profile
273
+
274
+ # @!macro property
275
+ # @return [Sring, nil]
276
+ string :isbn
277
+
278
+ # @!macro property
279
+ # @return [DateTime, nil]
280
+ datetime :release_date
281
+
282
+ # @!attribute [r] tags
283
+ # @return [Array<String>]
284
+ # @!macro property
285
+ # @return [String, nil]
286
+ string :tag, collection: true
287
+ end
288
+
289
+ # @see http://ogp.me/#type_music
290
+ class Music
291
+ include Object
292
+
293
+ namespace :music
294
+
295
+ # @!macro property
296
+ # @return [Integer, nil]
297
+ integer :duration, verticals: %w(song)
298
+
299
+
300
+ # @todo validate that target vertical is music.album/music.song ?
301
+ # @!attribute [r] albums
302
+ # @return [Array<Music>]
303
+ # @macro property
304
+ # @return [Music, nil]
305
+ url :album, to: Music, verticals: %w(song), collection: true
306
+
307
+ # @macro property
308
+ # @return [Integer, nil]
309
+ integer :disc, verticals: %w(song album playlist)
310
+
311
+ # @macro property
312
+ # @return [Integer, nil]
313
+ integer :track, verticals: %w(song album playlist)
314
+
315
+ # @!attribute [r] musicians
316
+ # @return [Array<Profile>]
317
+ # @!macro property
318
+ # @return [Profile, nil]
319
+ url :musician, to: Profile, verticals: %w(song album), collection: true
320
+
321
+ # @macro property
322
+ # @return [Music, nil]
323
+ url :song, to: Music, verticals: %w(album playlist)
324
+
325
+ # @macro property
326
+ # @return [DateTime, nil]
327
+ datetime :release_date, verticals: %w(album)
328
+
329
+ # @macro property
330
+ # @return [Profile, nil]
331
+ url :creator, to: Profile, verticals: %w(playlist radio_station)
332
+ end
333
+ end