open_graph_reader 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|