open_graph_reader 0.1.0

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