open_graph_reader 0.4.0 → 0.5.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 +4 -4
- data/lib/open_graph_reader.rb +10 -10
- data/lib/open_graph_reader/base.rb +9 -2
- data/lib/open_graph_reader/builder.rb +96 -44
- data/lib/open_graph_reader/configuration.rb +36 -10
- data/lib/open_graph_reader/definitions.rb +32 -33
- data/lib/open_graph_reader/fetcher.rb +13 -18
- data/lib/open_graph_reader/object.rb +7 -9
- data/lib/open_graph_reader/object/dsl.rb +58 -43
- data/lib/open_graph_reader/object/dsl/types.rb +51 -35
- data/lib/open_graph_reader/object/registry.rb +3 -3
- data/lib/open_graph_reader/parser.rb +30 -29
- data/lib/open_graph_reader/parser/graph.rb +18 -6
- data/lib/open_graph_reader/version.rb +1 -1
- data/spec/fixtures/real_world/invalid_article_author.html +299 -0
- data/spec/fixtures/real_world/invalid_datetime.html +301 -0
- data/spec/fixtures/real_world/url_path.html +1871 -0
- data/spec/integration/invalid_examples_spec.rb +21 -21
- data/spec/integration/real_world_spec.rb +335 -72
- data/spec/integration/valid_examples_spec.rb +7 -6
- data/spec/open_graph_reader_spec.rb +6 -6
- data/spec/spec_helper.rb +5 -8
- metadata +9 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 63380e971993a2baaa81d6d1189155a434fcf0fb
|
4
|
+
data.tar.gz: 84f9aa844498759914175ae984c86bb597a78669
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9747784896105a3b56891157c861a193dbb192ca8ebecee9152f908cac3f3d79ce2013179883022bcd3a40c95daf9ad2cc23d78656f92979af7f23c0acf008c4
|
7
|
+
data.tar.gz: b68f4cf4b9a1a9956b4dfe1003666f27ed736c00559716a6b0f9562b1f86137c7dc3ea6e3e66b698b8a5e5272919ce7d7cbcf2c5bc358ba52762623c9c961d77
|
data/lib/open_graph_reader.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
|
-
require
|
1
|
+
require "uri"
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
10
|
-
require
|
3
|
+
require "open_graph_reader/base"
|
4
|
+
require "open_graph_reader/builder"
|
5
|
+
require "open_graph_reader/configuration"
|
6
|
+
require "open_graph_reader/definitions"
|
7
|
+
require "open_graph_reader/fetcher"
|
8
|
+
require "open_graph_reader/object"
|
9
|
+
require "open_graph_reader/parser"
|
10
|
+
require "open_graph_reader/version"
|
11
11
|
|
12
12
|
# @todo 1.1 compatibility mode?
|
13
13
|
# This module provides the main entry to the library. Please see the
|
@@ -40,7 +40,7 @@ module OpenGraphReader
|
|
40
40
|
def self.parse! html, origin=nil
|
41
41
|
self.current_origin = origin
|
42
42
|
parser = Parser.new html
|
43
|
-
raise NoOpenGraphDataError, "#{origin || html} does not contain any OpenGraph tags" unless parser.
|
43
|
+
raise NoOpenGraphDataError, "#{origin || html} does not contain any OpenGraph tags" unless parser.any_tags?
|
44
44
|
Builder.new(parser).base.tap {|base|
|
45
45
|
base.origin = origin.to_s if origin
|
46
46
|
self.current_origin = nil
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require
|
1
|
+
require "forwardable"
|
2
2
|
|
3
3
|
module OpenGraphReader
|
4
4
|
# You get an instance of this class as result of your quest to obtain
|
@@ -39,13 +39,20 @@ module OpenGraphReader
|
|
39
39
|
# @see #origin
|
40
40
|
attr_writer :origin
|
41
41
|
|
42
|
+
# Return the stored root objects as a hash.
|
43
|
+
#
|
44
|
+
# @api private
|
45
|
+
# @return [String => Object]
|
46
|
+
attr_reader :bases
|
47
|
+
alias_method :children, :bases
|
48
|
+
|
42
49
|
# @api private
|
43
50
|
def initialize
|
44
51
|
@bases = {}
|
45
52
|
end
|
46
53
|
|
47
54
|
# @private
|
48
|
-
def respond_to_missing?(method,
|
55
|
+
def respond_to_missing?(method, _include_private=false)
|
49
56
|
@bases.has_key? method.to_s
|
50
57
|
end
|
51
58
|
|
@@ -24,7 +24,7 @@ module OpenGraphReader
|
|
24
24
|
def base
|
25
25
|
base = Base.new
|
26
26
|
|
27
|
-
type = @parser.graph.fetch(
|
27
|
+
type = @parser.graph.fetch("og:type", "website").downcase
|
28
28
|
|
29
29
|
validate_type type
|
30
30
|
|
@@ -33,6 +33,7 @@ module OpenGraphReader
|
|
33
33
|
end
|
34
34
|
|
35
35
|
synthesize_required_properties base
|
36
|
+
drop_empty_children base
|
36
37
|
validate base
|
37
38
|
|
38
39
|
base
|
@@ -41,62 +42,110 @@ module OpenGraphReader
|
|
41
42
|
private
|
42
43
|
|
43
44
|
def build_property base, property
|
44
|
-
|
45
|
-
base[root] ||= Object::Registry[root].new
|
46
|
-
object = resolve base[root], root, path
|
45
|
+
object, name = object_and_name base, property
|
47
46
|
|
48
|
-
if
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
collection << object
|
53
|
-
object.content = property.content
|
54
|
-
else # of type
|
55
|
-
collection << property.content
|
56
|
-
end
|
57
|
-
elsif Object::Registry.registered? property.fullname # Subobject
|
58
|
-
object[name] ||= Object::Registry[property.fullname].new
|
59
|
-
object[name].content = property.content
|
47
|
+
if collection? object, name
|
48
|
+
build_collection object, property, name
|
49
|
+
elsif subobject? property
|
50
|
+
build_subobject object, property, name
|
60
51
|
else # Direct attribute
|
61
|
-
object
|
52
|
+
build_single object, property, name
|
62
53
|
end
|
63
54
|
rescue UnknownNamespaceError, UndefinedPropertyError => e
|
64
55
|
raise InvalidObjectError, e.message if OpenGraphReader.config.strict
|
65
56
|
end
|
66
57
|
|
58
|
+
def object_and_name base, property
|
59
|
+
root, *path, name = property.path
|
60
|
+
base[root] ||= Object::Registry[root].new
|
61
|
+
object = resolve base[root], root, path
|
62
|
+
|
63
|
+
[object, name]
|
64
|
+
end
|
65
|
+
|
66
|
+
def collection? object, name
|
67
|
+
object.property?(name) && object.respond_to?("#{name}s")
|
68
|
+
end
|
69
|
+
|
70
|
+
def build_collection object, property, name
|
71
|
+
collection = object.public_send "#{name}s"
|
72
|
+
if Object::Registry.registered? property.fullname # of subobjects
|
73
|
+
object = Object::Registry[property.fullname].new
|
74
|
+
collection << object
|
75
|
+
object.content = property.content
|
76
|
+
else # of type
|
77
|
+
collection << property.content
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def subobject? property
|
82
|
+
Object::Registry.registered? property.fullname
|
83
|
+
end
|
84
|
+
|
85
|
+
def build_subobject object, property, name
|
86
|
+
object[name] ||= Object::Registry[property.fullname].new
|
87
|
+
object[name].content = property.content
|
88
|
+
end
|
89
|
+
|
90
|
+
def build_single object, property, name
|
91
|
+
object[name] = property.content
|
92
|
+
end
|
93
|
+
|
67
94
|
def resolve object, last_namespace, path
|
68
95
|
return object if path.empty?
|
69
96
|
|
70
97
|
next_name = path.shift
|
71
|
-
if object.
|
72
|
-
|
73
|
-
next_object = collection.last
|
74
|
-
if next_object.nil? # Final namespace or missing previous declaration, create a new collection item
|
75
|
-
next_object = Object::Registry[[*last_namespace, next_name].join(':')].new
|
76
|
-
collection << next_object
|
77
|
-
end
|
98
|
+
if object.property?(next_name) && object.respond_to?("#{next_name}s") # collection
|
99
|
+
resolve_collection object, last_namespace, next_name
|
78
100
|
else
|
79
|
-
|
80
|
-
|
101
|
+
resolve_property object, last_namespace, next_name
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def resolve_collection object, last_namespace, next_name
|
106
|
+
collection = object.public_send("#{next_name}s")
|
107
|
+
next_object = collection.last
|
108
|
+
# Final namespace or missing previous declaration, create a new collection item
|
109
|
+
if next_object.nil?
|
110
|
+
next_object = Object::Registry[[*last_namespace, next_name].join(":")].new
|
111
|
+
collection << next_object
|
81
112
|
end
|
82
113
|
|
83
114
|
next_object
|
84
115
|
end
|
85
116
|
|
117
|
+
def resolve_property object, last_namespace, next_name
|
118
|
+
next_object = object[next_name]
|
119
|
+
next_object || Object::Registry[[*last_namespace, next_name].join(":")].new
|
120
|
+
end
|
121
|
+
|
86
122
|
def synthesize_required_properties base
|
87
|
-
|
88
|
-
|
123
|
+
return unless OpenGraphReader.config.synthesize_title
|
124
|
+
return if base.og.title
|
125
|
+
|
126
|
+
base.og["title"] = @parser.title
|
127
|
+
end
|
128
|
+
|
129
|
+
def drop_empty_children base
|
130
|
+
base = base.children
|
131
|
+
base.each do |key, object|
|
132
|
+
[*object].each do |object|
|
133
|
+
if object.is_a? Object
|
134
|
+
drop_empty_children object
|
135
|
+
base.delete(key) if object.content.nil? && object.children.empty? && object.properties.empty?
|
136
|
+
end
|
137
|
+
end
|
89
138
|
end
|
90
139
|
end
|
91
140
|
|
92
141
|
def validate_type type
|
93
142
|
return unless OpenGraphReader.config.strict
|
94
143
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
144
|
+
return if KNOWN_TYPES.include?(type)
|
145
|
+
return if @parser.additional_namespaces.include?(type)
|
146
|
+
return if Object::Registry.verticals.include?(type)
|
147
|
+
|
148
|
+
raise InvalidObjectError, "Undefined type #{type}"
|
100
149
|
end
|
101
150
|
|
102
151
|
def validate base
|
@@ -115,18 +164,21 @@ module OpenGraphReader
|
|
115
164
|
end
|
116
165
|
|
117
166
|
def validate_verticals object, type
|
118
|
-
return unless type.include?
|
167
|
+
return unless type.include? "."
|
168
|
+
|
119
169
|
verticals = object.class.verticals
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
170
|
+
return unless verticals.has_key? type
|
171
|
+
return if extra_properties(object, type, verticals).empty?
|
172
|
+
|
173
|
+
raise InvalidObjectError, "Set invalid property #{extra_properties.first} for #{type} " \
|
174
|
+
"in #{object.inspect}, valid properties are #{valid_properties.inspect}"
|
175
|
+
end
|
176
|
+
|
177
|
+
def extra_properties object, type, verticals
|
178
|
+
valid_properties = verticals[type]
|
179
|
+
set_properties = object.class.available_properties.select {|property| object[property] }
|
180
|
+
|
181
|
+
set_properties - valid_properties
|
130
182
|
end
|
131
183
|
end
|
132
184
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require
|
1
|
+
require "singleton"
|
2
2
|
|
3
3
|
module OpenGraphReader
|
4
4
|
# The behavior of this library can be tweaked with some parameters.
|
@@ -38,6 +38,14 @@ module OpenGraphReader
|
|
38
38
|
# @return [Bool]
|
39
39
|
attr_accessor :validate_references
|
40
40
|
|
41
|
+
# Discard invalid optional properties (default: <tt>false</tt>).
|
42
|
+
#
|
43
|
+
# Instead of rendering the entire object invalid, discard
|
44
|
+
# invalid optional properties.
|
45
|
+
#
|
46
|
+
# @return [Bool]
|
47
|
+
attr_accessor :discard_invalid_optional_properties
|
48
|
+
|
41
49
|
# Fallback to the title tag if og:title is missing (default: <tt>false</tt>).
|
42
50
|
#
|
43
51
|
# The standard makes defining og:title required, but it's
|
@@ -47,18 +55,33 @@ module OpenGraphReader
|
|
47
55
|
# @return [Bool]
|
48
56
|
attr_accessor :synthesize_title
|
49
57
|
|
50
|
-
|
51
|
-
# Guess image URL when it looks like a path (default: <tt>false</tt>).
|
58
|
+
# Guess object URL when it looks like a path (default: <tt>false</tt>).
|
52
59
|
#
|
53
|
-
# The standard requires
|
54
|
-
#
|
60
|
+
# The standard requires the url type to point to a full http or https URL.
|
61
|
+
# However it's common practice to put a path relative
|
55
62
|
# to the domain URL. When enabled, the library tries to guess the full
|
56
63
|
# URL from such a path. Note the object can still turn invalid if it fails
|
57
64
|
# to do so.
|
58
65
|
#
|
59
66
|
# @return [Bool]
|
67
|
+
attr_accessor :synthesize_url
|
68
|
+
|
69
|
+
# Guess image URL when it looks like a path (default: <tt>false</tt>).
|
70
|
+
#
|
71
|
+
# See {#synthesize_url}
|
72
|
+
#
|
73
|
+
# @return [Bool]
|
60
74
|
attr_accessor :synthesize_image_url
|
61
75
|
|
76
|
+
# Parse non ISO8601 datetimes (default: <tt>false</tt>).
|
77
|
+
#
|
78
|
+
# The standard clearly requires ISO8601 as format for
|
79
|
+
# datetime properties. However other formats are seen in the wild.
|
80
|
+
# With this setting enabled, the format is guessed.
|
81
|
+
#
|
82
|
+
# @return [Bool]
|
83
|
+
attr_accessor :guess_datetime_format
|
84
|
+
|
62
85
|
# @private
|
63
86
|
def initialize
|
64
87
|
reset_to_defaults!
|
@@ -66,11 +89,14 @@ module OpenGraphReader
|
|
66
89
|
|
67
90
|
# Reset configuration to their defaults
|
68
91
|
def reset_to_defaults!
|
69
|
-
@strict
|
70
|
-
@validate_required
|
71
|
-
@validate_references
|
72
|
-
@
|
73
|
-
@
|
92
|
+
@strict = false
|
93
|
+
@validate_required = true
|
94
|
+
@validate_references = true
|
95
|
+
@discard_invalid_optional_properties = false
|
96
|
+
@synthesize_title = false
|
97
|
+
@synthesize_url = false
|
98
|
+
@synthesize_image_url = false
|
99
|
+
@guess_datetime_format = false
|
74
100
|
end
|
75
101
|
end
|
76
102
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require
|
1
|
+
require "open_graph_reader/object"
|
2
2
|
|
3
3
|
module OpenGraphReader
|
4
4
|
# @see http://ogp.me/#metadata
|
@@ -9,7 +9,7 @@ module OpenGraphReader
|
|
9
9
|
|
10
10
|
# @!macro property
|
11
11
|
# @return [String]
|
12
|
-
string :type, required: true, downcase: true, default:
|
12
|
+
string :type, required: true, downcase: true, default: "website"
|
13
13
|
|
14
14
|
# @!macro property
|
15
15
|
# @return [String]
|
@@ -19,15 +19,15 @@ module OpenGraphReader
|
|
19
19
|
# @return [Array<Image>]
|
20
20
|
# @!macro property
|
21
21
|
# @return [Image]
|
22
|
-
url
|
22
|
+
url :image, required: true, collection: true
|
23
23
|
|
24
24
|
# @!macro property
|
25
25
|
# @return [String, nil]
|
26
|
-
url
|
26
|
+
url :url
|
27
27
|
|
28
28
|
# @!macro property
|
29
29
|
# @return [Audio, nil]
|
30
|
-
url
|
30
|
+
url :audio
|
31
31
|
|
32
32
|
# @!macro property
|
33
33
|
# @return [String, nil]
|
@@ -35,7 +35,7 @@ module OpenGraphReader
|
|
35
35
|
|
36
36
|
# @!macro property
|
37
37
|
# @return [String]
|
38
|
-
enum
|
38
|
+
enum :determiner, ["", "a", "an", "the", "auto"], default: ""
|
39
39
|
|
40
40
|
# @!macro property
|
41
41
|
# @return [Locale, nil]
|
@@ -47,7 +47,7 @@ module OpenGraphReader
|
|
47
47
|
|
48
48
|
# @!macro property
|
49
49
|
# @return [Video, nil]
|
50
|
-
url
|
50
|
+
url :video
|
51
51
|
|
52
52
|
# @see http://ogp.me/#structured
|
53
53
|
class Image
|
@@ -56,15 +56,15 @@ module OpenGraphReader
|
|
56
56
|
namespace :og, :image
|
57
57
|
content :url, image: true
|
58
58
|
|
59
|
-
url
|
59
|
+
url :url
|
60
60
|
|
61
61
|
# @!macro property
|
62
62
|
# @return [String, nil]
|
63
|
-
url
|
63
|
+
url :secure_url
|
64
64
|
|
65
65
|
# @!macro property
|
66
66
|
# @return [String, nil]
|
67
|
-
string
|
67
|
+
string :type
|
68
68
|
|
69
69
|
# @!macro property
|
70
70
|
# @return [Integer, nil]
|
@@ -88,11 +88,11 @@ module OpenGraphReader
|
|
88
88
|
content :url
|
89
89
|
|
90
90
|
# This property is not listed on http://ogp.me, but commonly found
|
91
|
-
url
|
91
|
+
url :url
|
92
92
|
|
93
93
|
# @!macro property
|
94
94
|
# @return [String, nil]
|
95
|
-
url
|
95
|
+
url :secure_url
|
96
96
|
|
97
97
|
# @!macro property
|
98
98
|
# @return [String, nil]
|
@@ -127,11 +127,11 @@ module OpenGraphReader
|
|
127
127
|
|
128
128
|
# @!macro property
|
129
129
|
# @return [String, nil]
|
130
|
-
url
|
130
|
+
url :secure_url
|
131
131
|
|
132
132
|
# @!macro property
|
133
133
|
# @return [String, nil]
|
134
|
-
string
|
134
|
+
string :type
|
135
135
|
|
136
136
|
# @!macro property
|
137
137
|
# @return [Integer, nil]
|
@@ -169,7 +169,7 @@ module OpenGraphReader
|
|
169
169
|
|
170
170
|
# @!macro property
|
171
171
|
# @return [String, nil]
|
172
|
-
enum
|
172
|
+
enum :gender, %w(male female)
|
173
173
|
|
174
174
|
# @!macro property
|
175
175
|
# @return [String, nil]
|
@@ -201,17 +201,17 @@ module OpenGraphReader
|
|
201
201
|
# @return [Array<Profile>]
|
202
202
|
# @!macro property
|
203
203
|
# @return [Profile, nil]
|
204
|
-
url
|
204
|
+
url :author, collection: true, to: Profile
|
205
205
|
|
206
206
|
# @!macro property
|
207
207
|
# @return [String, nil]
|
208
|
-
string
|
208
|
+
string :section
|
209
209
|
|
210
210
|
# @!attribute [r] tags
|
211
211
|
# @return [Array<String>]
|
212
212
|
# @!macro property
|
213
213
|
# @return [String, nil]
|
214
|
-
string
|
214
|
+
string :tag, collection: true
|
215
215
|
end
|
216
216
|
|
217
217
|
# @see http://ogp.me/#type_video
|
@@ -224,38 +224,38 @@ module OpenGraphReader
|
|
224
224
|
# @return [Array<Profile>]
|
225
225
|
# @!macro property
|
226
226
|
# @return [Profile, nil]
|
227
|
-
url
|
227
|
+
url :actor, to: Profile, verticals: %w(movie episode tv_show other), collection: true
|
228
228
|
|
229
229
|
# @!attribute [r] directors
|
230
230
|
# @return [Array<Profile>]
|
231
231
|
# @!macro property
|
232
232
|
# @return [Profile, nil]
|
233
|
-
url
|
233
|
+
url :director, to: Profile, verticals: %w(movie episode tv_show other), collection: true
|
234
234
|
|
235
235
|
# @!attribute [r] writers
|
236
236
|
# @return [Array<Profile>]
|
237
237
|
# @!macro property
|
238
238
|
# @return [Profile, nil]
|
239
|
-
url
|
239
|
+
url :writer, to: Profile, verticals: %w(movie episode tv_show other), collection: true
|
240
240
|
|
241
241
|
# @!macro property
|
242
242
|
# @return [Integer, nil]
|
243
|
-
integer
|
243
|
+
integer :duration, verticals: %w(movie episode tv_show other)
|
244
244
|
|
245
245
|
# @!macro property
|
246
246
|
# @return [DateTime, nil]
|
247
|
-
datetime :release_date,
|
247
|
+
datetime :release_date, verticals: %w(movie episode tv_show other)
|
248
248
|
|
249
249
|
# @!attribute [r] tags
|
250
250
|
# @return [Array<String>]
|
251
251
|
# @!macro property
|
252
252
|
# @return [String, nil]
|
253
|
-
string
|
253
|
+
string :tag, verticals: %w(movie episode tv_show other), collection: true
|
254
254
|
|
255
255
|
# @todo validate that target vertical is video.tv_show ?
|
256
256
|
# @!macro property
|
257
257
|
# @return [Sring, nil]
|
258
|
-
url
|
258
|
+
url :series, to: Video, verticals: %w(episode)
|
259
259
|
end
|
260
260
|
|
261
261
|
# @see http://ogp.me/#type_book
|
@@ -296,38 +296,37 @@ module OpenGraphReader
|
|
296
296
|
# @return [Integer, nil]
|
297
297
|
integer :duration, verticals: %w(song)
|
298
298
|
|
299
|
-
|
300
299
|
# @todo validate that target vertical is music.album/music.song ?
|
301
300
|
# @!attribute [r] albums
|
302
301
|
# @return [Array<Music>]
|
303
302
|
# @macro property
|
304
303
|
# @return [Music, nil]
|
305
|
-
url
|
304
|
+
url :album, to: Music, verticals: %w(song), collection: true
|
306
305
|
|
307
306
|
# @macro property
|
308
307
|
# @return [Integer, nil]
|
309
|
-
integer
|
308
|
+
integer :disc, verticals: %w(song album playlist)
|
310
309
|
|
311
310
|
# @macro property
|
312
311
|
# @return [Integer, nil]
|
313
|
-
integer
|
312
|
+
integer :track, verticals: %w(song album playlist)
|
314
313
|
|
315
314
|
# @!attribute [r] musicians
|
316
315
|
# @return [Array<Profile>]
|
317
316
|
# @!macro property
|
318
317
|
# @return [Profile, nil]
|
319
|
-
url
|
318
|
+
url :musician, to: Profile, verticals: %w(song album), collection: true
|
320
319
|
|
321
320
|
# @macro property
|
322
321
|
# @return [Music, nil]
|
323
|
-
url
|
322
|
+
url :song, to: Music, verticals: %w(album playlist)
|
324
323
|
|
325
324
|
# @macro property
|
326
325
|
# @return [DateTime, nil]
|
327
|
-
datetime :release_date,
|
326
|
+
datetime :release_date, verticals: %w(album)
|
328
327
|
|
329
328
|
# @macro property
|
330
329
|
# @return [Profile, nil]
|
331
|
-
url
|
330
|
+
url :creator, to: Profile, verticals: %w(playlist radio_station)
|
332
331
|
end
|
333
332
|
end
|