open_graph_reader 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fb82d8a9867ad4c1cf0258ef4e97f993cccdc6aa
4
- data.tar.gz: ef67382716657161c2098380bf00c6abc48e6a4e
3
+ metadata.gz: bea1c38c6567749ce9bd8b369867d5e7c0b7a2b3
4
+ data.tar.gz: b0805bdd3e5e1efddc7f73c59dd40ad7315b99c3
5
5
  SHA512:
6
- metadata.gz: a60f2d67610ab9d1cd0587c58b92452f07fff77da489eec68c7906af650247bd42e26506a5d323f9d6ebe4b3bb247b7b26aba2dac5389634c660d80801b6f32b
7
- data.tar.gz: 423fb2fe90278c64bd089579c5165c9ee468663c3d9eeb0661c1dfa0a5adc74f12a8772f7f5bdeff0d15ecb52493887d822e13a68dae31de98f9056636e21666
6
+ metadata.gz: 6d3d1edbaa5b0ac800300c79631d5494e6eebf7cc362b618621ca6006530d303c7de679fa761ae5d6edd3b826fa069f1dde3ff6f76dbd295b3781e68fa4eb29e
7
+ data.tar.gz: f9ba7d6080e164c75491abbd297994627f2812e907c230c5edb5c4f42681657da27c7e730b83b42df19fc96ddcb9fd9a3cd44b3121f933ac45d8db1635b040c6
@@ -1,18 +1,14 @@
1
1
  require 'uri'
2
2
 
3
- begin
4
- require 'faraday_middleware/response/follow_redirects'
5
- rescue LoadError; end
6
-
7
3
  require 'open_graph_reader/base'
8
4
  require 'open_graph_reader/builder'
5
+ require 'open_graph_reader/configuration'
9
6
  require 'open_graph_reader/definitions'
10
7
  require 'open_graph_reader/fetcher'
11
8
  require 'open_graph_reader/object'
12
9
  require 'open_graph_reader/parser'
13
10
  require 'open_graph_reader/version'
14
11
 
15
- # @todo quirks mode where invalid attributes don't raise?
16
12
  # @todo 1.1 compatibility mode?
17
13
  # This module provides the main entry to the library. Please see the
18
14
  # {file:README.md} for usage examples.
@@ -72,6 +68,24 @@ module OpenGraphReader
72
68
  rescue NoOpenGraphDataError, InvalidObjectError
73
69
  end
74
70
 
71
+ # Configure the library, see {Configuration} for the list of available
72
+ # options and their defaults. Changing configuration at runtime is not
73
+ # thread safe.
74
+ #
75
+ # @yieldparam [Configuration] the configuration object
76
+ # @see Configuration
77
+ def self.configure
78
+ yield config
79
+ end
80
+
81
+ # Get the current {Configuration} instance
82
+ #
83
+ # @api private
84
+ # @return [Configuration]
85
+ def self.config
86
+ Configuration.instance
87
+ end
88
+
75
89
  # The target couldn't be fetched, didn't contain any HTML or
76
90
  # any OpenGraph tags.
77
91
  class NoOpenGraphDataError < StandardError
@@ -80,4 +94,10 @@ module OpenGraphReader
80
94
  # The target did contain OpenGraph tags, but they're not valid.
81
95
  class InvalidObjectError < StandardError
82
96
  end
97
+
98
+ # The target defines a namespace we have no definition for
99
+ #
100
+ # @api private
101
+ class UnknownNamespaceError < StandardError
102
+ end
83
103
  end
@@ -19,7 +19,13 @@ module OpenGraphReader
19
19
  # @param [String] name The name of the root namespace.
20
20
  # @param [Object] object The corresponding root object.
21
21
  # @api private
22
- def_delegators :@bases, :[], :[]=
22
+ # @!method each
23
+ # Traverse the available objects
24
+ #
25
+ # @yield [Object]
26
+ # @api private
27
+ def_delegators :@bases, :[], :[]=, :each_value
28
+ alias_method :each, :each_value
23
29
 
24
30
  # If available, contains the source location of the document the
25
31
  # available objects were parsed from.
@@ -1,27 +1,13 @@
1
1
  module OpenGraphReader
2
2
  # Convert a {Parser::Graph} into the right hierarchy of {Object}s attached
3
- # to a {Base}.
3
+ # to a {Base}, then validate it.
4
4
  #
5
- # @todo validate required, verticals
6
5
  # @api private
7
6
  class Builder
8
7
  # Well-known types from
9
8
  #
10
9
  # @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
10
+ KNOWN_TYPES = %w(website article book profile).freeze
25
11
 
26
12
  # Create a new builder.
27
13
  #
@@ -41,45 +27,53 @@ module OpenGraphReader
41
27
  def base
42
28
  base = Base.new
43
29
 
44
- type = @graph.fetch 'og:type', 'website'
30
+ type = @graph.fetch('og:type', 'website').downcase
45
31
 
46
32
  validate_type type
47
33
 
48
34
  @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
35
+ build_property base, property
68
36
  end
69
37
 
38
+ validate base
39
+
70
40
  base
71
41
  end
72
42
 
73
43
  private
74
44
 
45
+ def build_property base, property
46
+ root, *path, name = property.path
47
+ base[root] ||= Object::Registry[root].new
48
+ object = resolve base[root], root, path
49
+
50
+ if object.has_property?(name) && object.respond_to?("#{name}s") # Collection
51
+ collection = object.public_send "#{name}s"
52
+ if Object::Registry.registered? property.fullname # of subobjects
53
+ object = Object::Registry[property.fullname].new
54
+ collection << object
55
+ object.content = property.content
56
+ else # of type
57
+ collection << property.content
58
+ end
59
+ elsif Object::Registry.registered? property.fullname # Subobject
60
+ object[name] ||= Object::Registry[property.fullname].new
61
+ object[name].content = property.content
62
+ else # Direct attribute
63
+ object[name] = property.content
64
+ end
65
+ rescue UnknownNamespaceError => e
66
+ raise InvalidObjectError, e.message if OpenGraphReader.config.strict
67
+ end
68
+
75
69
  def resolve object, last_namespace, path
76
70
  return object if path.empty?
77
71
 
78
72
  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
73
+ if object.has_property?(next_name) && object.respond_to?("#{next_name}s") # collection
74
+ collection = object.public_send("#{next_name}s")
81
75
  next_object = collection.last
82
- if next_object.nil? #|| path.empty? # Final namespace or missing previous declaration, create a new collection item
76
+ if next_object.nil? # Final namespace or missing previous declaration, create a new collection item
83
77
  next_object = Object::Registry[[*last_namespace, next_name].join(':')].new
84
78
  collection << next_object
85
79
  end
@@ -92,9 +86,43 @@ module OpenGraphReader
92
86
  end
93
87
 
94
88
  def validate_type type
95
- unless KNOWN_TYPES.include?(type) || @additional_namespaces.include?(type)
89
+ return unless OpenGraphReader.config.strict
90
+
91
+ unless KNOWN_TYPES.include?(type) ||
92
+ @additional_namespaces.include?(type) ||
93
+ Object::Registry.verticals.include?(type)
96
94
  raise InvalidObjectError, "Undefined type #{type}"
97
95
  end
98
96
  end
97
+
98
+ def validate base
99
+ base.each do |object|
100
+ validate_required object if OpenGraphReader.config.validate_required
101
+ validate_verticals object, base.og.type
102
+ end
103
+ end
104
+
105
+ def validate_required object
106
+ object.class.required_properties.each do |property|
107
+ if object[property].nil?
108
+ raise InvalidObjectError, "Missing required property #{property} on #{object.inspect}"
109
+ end
110
+ end
111
+ end
112
+
113
+ def validate_verticals object, type
114
+ return unless type.include? '.'
115
+ verticals = object.class.verticals
116
+ if verticals.has_key? type
117
+ valid_properties = verticals[type]
118
+ set_properties = object.class.available_properties.select {|property| object[property] }
119
+ extra_properties = set_properties-valid_properties
120
+
121
+ unless extra_properties.empty?
122
+ raise InvalidObjectError, "Set invalid property #{extra_properties.first} for #{type} " \
123
+ "in #{object.inspect}, valid properties are #{valid_properties.inspect}"
124
+ end
125
+ end
126
+ end
99
127
  end
100
128
  end
@@ -0,0 +1,51 @@
1
+ require 'singleton'
2
+
3
+ module OpenGraphReader
4
+ # The behavior of this library can be tweaked with some parameters.
5
+ #
6
+ # @example
7
+ # OpenGraphReader.configure do |config|
8
+ # config.strict = true
9
+ # end
10
+ class Configuration
11
+ include Singleton
12
+
13
+ # Strict mode (default: <tt>false</tt>)
14
+ #
15
+ # In strict mode, if the fetched site defines an unknown type
16
+ # or property, {InvalidObjectError} is thrown instead of just ignoring
17
+ # those.
18
+ #
19
+ # @return [Bool]
20
+ attr_accessor :strict
21
+
22
+ # Validate required (default: <tt>true</tt>)
23
+ #
24
+ # Validate that required properties exist. If this is enabled and
25
+ # they do not, {InvalidObjectError} is thrown.
26
+ #
27
+ # @return [Bool]
28
+ attr_accessor :validate_required
29
+
30
+ # Validate references (default: <tt>true</tt>)
31
+ #
32
+ # If an object should be a reference to another object,
33
+ # validate that it contains an URL. Be careful in turning this off,
34
+ # an attacker could place things like <tt>javascript:</tt> links there.
35
+ #
36
+ # @return [Bool]
37
+ attr_accessor :validate_references
38
+
39
+ # @private
40
+ def initialize
41
+ reset_to_defaults!
42
+ end
43
+
44
+ # Reset configuration to their defaults
45
+ def reset_to_defaults!
46
+ @strict = false
47
+ @validate_required = true
48
+ @validate_references = true
49
+ end
50
+ end
51
+ end
@@ -9,7 +9,7 @@ module OpenGraphReader
9
9
 
10
10
  # @!macro property
11
11
  # @return [String]
12
- string :type, required: true, default: 'website'
12
+ string :type, required: true, downcase: true, default: 'website'
13
13
 
14
14
  # @!macro property
15
15
  # @return [String]
@@ -1,5 +1,13 @@
1
1
  require 'faraday'
2
2
 
3
+ begin
4
+ require 'faraday_middleware/response/follow_redirects'
5
+ rescue LoadError; end
6
+
7
+ begin
8
+ require 'faraday/cookie_jar'
9
+ rescue LoadError; end
10
+
3
11
  module OpenGraphReader
4
12
  # Fetch an URI to retrieve its HTML body, if available.
5
13
  #
@@ -14,9 +22,11 @@ module OpenGraphReader
14
22
  @connection = Faraday.default_connection.dup
15
23
 
16
24
  if defined? FaradayMiddleware
17
- unless @connection.builder.handlers.include? FaradayMiddleware::FollowRedirects
18
- @connection.builder.insert(0, FaradayMiddleware::FollowRedirects)
19
- end
25
+ prepend_middleware FaradayMiddleware::FollowRedirects
26
+ end
27
+
28
+ if defined? Faraday::CookieJar
29
+ prepend_middleware Faraday::CookieJar
20
30
  end
21
31
  end
22
32
 
@@ -78,5 +88,13 @@ module OpenGraphReader
78
88
  def fetched_headers?
79
89
  !@get_response.nil? || !@head_response.nil?
80
90
  end
91
+
92
+ private
93
+
94
+ def prepend_middleware middleware
95
+ unless @connection.builder.handlers.include? middleware
96
+ @connection.builder.insert(0, middleware)
97
+ end
98
+ end
81
99
  end
82
100
  end
@@ -71,7 +71,7 @@ module OpenGraphReader
71
71
  # @return [String, Object]
72
72
  def [] name
73
73
  raise InvalidObjectError, "Undefined property #{name} on #{inspect}" unless has_property? name
74
- properties[name.to_s]
74
+ public_send name.to_s #properties[name.to_s]
75
75
  end
76
76
 
77
77
  # Set the property to the given value.
@@ -81,8 +81,11 @@ module OpenGraphReader
81
81
  # @param [String, Object] value
82
82
  # @raise [InvalidObjectError] If the requested property is undefined.
83
83
  def []= name, value
84
- raise InvalidObjectError, "Undefined property #{name} on #{inspect}" unless has_property? name
85
- public_send "#{name}=", value
84
+ if has_property?(name)
85
+ public_send "#{name}=", value
86
+ elsif OpenGraphReader.config.strict
87
+ raise InvalidObjectError, "Undefined property #{name} on #{inspect}"
88
+ end
86
89
  end
87
90
 
88
91
  # Returns {#content} if available.
@@ -14,6 +14,7 @@ module OpenGraphReader
14
14
  # @option options [Class] :to This property maps to the given object (optional).
15
15
  # belongs to the given verticals of the object (optional).
16
16
  # @option options [Array<String>] :verticials This property
17
+ # @option options [Bool] :downcase (false) Normalize the contents case to lowercase.
17
18
  #
18
19
  # @!macro property
19
20
  # @!attribute [rw] $1
@@ -32,15 +33,18 @@ module OpenGraphReader
32
33
  processors[name] = processor
33
34
 
34
35
  define_method(name) do |name, *args|
35
- available_properties << name.to_s
36
36
  options = args.pop if args.last.is_a? Hash
37
37
  options ||= {}
38
38
 
39
+ available_properties << name.to_s
40
+ required_properties << name.to_s if options[:required]
39
41
  Registry.register [@namespace, name].join(':'), options[:to] if options[:to]
40
42
 
41
43
  if options[:verticals]
42
44
  options[:verticals].each do |vertical|
43
- verticals[[@namespace, vertical].join('.')] << name
45
+ vertical = [@namespace, vertical].join('.')
46
+ verticals[vertical] << name.to_s
47
+ Registry.verticals << vertical
44
48
  end
45
49
  end
46
50
 
@@ -50,21 +54,22 @@ module OpenGraphReader
50
54
  end
51
55
 
52
56
  define_method(name) do
53
- # TODO raise if required
54
57
  value = children[name.to_s].first
55
- # TODO: figure out a sane way to distinguish subobject properties
58
+ # @todo figure out a sane way to distinguish subobject properties
56
59
  value.content if value && value.is_a?(Object)
57
60
  value || options[:default]
58
61
  end
59
62
  else
60
63
  define_method(name) do
61
- # TODO raise if required
62
64
  properties[name.to_s] || options[:default]
63
65
  end
64
66
 
65
67
  define_method("#{name}=") do |value|
66
- # TODO: figure out a sane way to distinguish subobject properties
67
- value = processor.call(value, *args, options) unless value.is_a? Object
68
+ # @todo figure out a sane way to distinguish subobject properties
69
+ unless value.is_a? Object
70
+ value.downcase! if options[:downcase]
71
+ value = processor.call(value, *args, options)
72
+ end
68
73
  properties[name.to_s] = value
69
74
  end
70
75
  end
@@ -88,11 +93,23 @@ module OpenGraphReader
88
93
  Registry.register @namespace, self
89
94
  end
90
95
 
91
- # Set the type for the content attribute
96
+ # @overload content type, *args, options={}
92
97
  #
93
- # @param [Symbol] type one of the registered types.
94
- def content type
95
- @content_processor = DSL.processors[type]
98
+ # Set the type for the content attribute
99
+ #
100
+ # @param [Symbol] type one of the registered types.
101
+ # @param [Array<Object>] args Additional parameters for the type
102
+ # @param [Hash] options
103
+ # @option options [Bool] :downcase (false) Normalize the contents case to lowercase.
104
+ def content type, *args
105
+ options = args.pop if args.last.is_a? Hash
106
+ options ||= {}
107
+
108
+ @content_processor = proc {|value|
109
+ value.downcase! if options[:downcase]
110
+ options[:to] ||= self
111
+ DSL.processors[type].call(value, *args, options)
112
+ }
96
113
  end
97
114
 
98
115
  # The list of defined properties on this object.
@@ -102,6 +119,13 @@ module OpenGraphReader
102
119
  @available_properties ||= []
103
120
  end
104
121
 
122
+ # The list of required properties on this object.
123
+ #
124
+ # @return [Array<String]
125
+ def required_properties
126
+ @required_properties ||= []
127
+ end
128
+
105
129
  # A map from type names to processing blocks.
106
130
  #
107
131
  # @api private
@@ -115,7 +139,7 @@ module OpenGraphReader
115
139
  # @api private
116
140
  # @return [Proc]
117
141
  def content_processor
118
- @content_processor || proc {|value| value }
142
+ @content_processor
119
143
  end
120
144
 
121
145
  # A map from vertical names to attributes that belong to them.