linkscape 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009-2010 SEOmoz
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,228 @@
1
+ = Linkscape Gem
2
+
3
+ This is the official Gem to interact with the SEOmoz API. For more information, please check out the {API page on SEOmoz}[http://www.seomoz.org/api] and the {SEOmoz API Wiki}[http://apiwiki.seomoz.org]
4
+
5
+ == Usage
6
+
7
+ First you must secure an accessID and secret key from SEOmoz. See the {API page}[http://www.seomoz.org/api] for details. Once you have your credentials, create an instance of your client:
8
+
9
+ client = Linkscape::Client.new(:accessID => "my_access_id", :secret => "$secretp@$$w0rd")
10
+
11
+ Then you should be able to make one of the predefined API calls
12
+
13
+ response = client.urlMetrics("http://gemcutter.org", :cols => :all)
14
+
15
+ At this point <tt>response</tt> should be a Linkscape::Response object. You can get at its data by calling
16
+
17
+ response.data
18
+
19
+ This returns a Linkscape::Response::ResponseData object, which is the container class for the response data.
20
+
21
+ For the urlMetrics and mozrank call, you can request specific data points, like:
22
+
23
+ response.data[:url]
24
+ response.data[:external_links]
25
+ response.data[:page_authority]
26
+
27
+ For all other calls, such as anchor text, top pages and link data sets, <tt>data</tt> is an array of ResponseData objects, with each one keyed on their respective types. For example:
28
+
29
+ response.data.first[:source][:mozrank] # :source just for topPages and link calls
30
+ response.data.first[:target][:page_authority] # :target just for link calls
31
+ response.data.first[:link][:text] # :link just for link calls
32
+ response.data.first[:anchor][:text] # :anchor just for link anchorMetrics calls
33
+
34
+ == Available API Calls
35
+
36
+ The Linkscape::Client object can make the following API calls:
37
+
38
+ === mozRank
39
+
40
+ Returns the mozRank of the supplied URI.
41
+
42
+ client.mozRank(uri)
43
+
44
+ === urlMetrics
45
+
46
+ Returns metrics about the supplied URI.
47
+
48
+ client.urlMetrics(uri, :cols => :all)
49
+
50
+ This call accepts the following options:
51
+
52
+ * <tt>:cols</tt> - An array of columns (see below) to return in the response, or the <tt>:all</tt> keyword, which returns all columns.
53
+
54
+ === topLinks
55
+
56
+ Returns a list of topLinks to the supplied URI and :type.
57
+
58
+ client.topLinks(uri, :page, :urlcols => :all, :linkcols => :all, :limit => 3))
59
+
60
+ The 2nd parameter of this method, <tt>:type</tt> can be either <tt>:page</tt>, <tt>:subdomain</tt> <tt>:domain</tt>. It specifies the type of links you are requesting (links to the supplied URI, Root Domain or the Subdomain of the supplied URI).
61
+
62
+ This call accepts the following options:
63
+
64
+ * <tt>:sourcecols</tt> - An array of data columns (see below) that should be returned for the link source.
65
+ * <tt>:targetcols</tt> - An array of data columns (see below) that should be returned for the link target.
66
+ * <tt>:linkcols</tt> - An array of data columns (see below) that should be returned for the link itself.
67
+ * <tt>:limit</tt> - The # of links (limit) you would like to return.
68
+ * <tt>:offset</tt> = The number of records to offset before returning the 1st record of results.
69
+
70
+ === allLinks
71
+
72
+ Returns all the links to a specific URI and :type.
73
+
74
+ client.allLinks(uri, :page, :urlcols => [:title, :url, :page_authority, :domain_authority], :linkcols => :all, :filters => :external, :limit => 3)
75
+
76
+ The 2nd parameter of this method, <tt>:type</tt> can be either <tt>:page</tt>, <tt>:subdomain</tt> <tt>:domain</tt>. It specifies the type of links you are requesting (links to the supplied URI, Root Domain or the Subdomain of the supplied URI).
77
+
78
+ This call accepts the following options:
79
+
80
+ * <tt>:sourcecols</tt> - An array of data columns (see below) that should be returned for the link source.
81
+ * <tt>:targetcols</tt> - An array of data columns (see below) that should be returned for the link target.
82
+ * <tt>:linkcols</tt> - An array of data columns (see below) that should be returned for the link itself.
83
+ * <tt>:filters</tt> - A String, Array or Symbol of filters (see below) that should be applied to the list of links. NOTE: Multiple filters may be combined, i.e. <tt>:filters => [:internal,:follow,:redir301]</tt>.
84
+ * <tt>:limit</tt> - The # of links (limit) you would like to return.
85
+ * <tt>:offset</tt> = The number of records to offset before returning the 1st record of results.
86
+ * <tt>:scope</tt> - A symbol representing the 'scope' of the links (see below).
87
+
88
+ === topPages
89
+
90
+ Returns a list of the top pages on the URI in question
91
+
92
+ client.topPages(uri, :page, :cols => :all, :limit => 3)
93
+
94
+ The 2nd parameter of this method, <tt>:type</tt> can be either <tt>:page</tt>, <tt>:subdomain</tt> <tt>:domain</tt>. It specifies the type of top pages you are requesting (top pages on the supplied URI, Root Domain or the Subdomain of the supplied URI).
95
+
96
+ This call accepts the following options:
97
+
98
+ * <tt>:cols</tt> - An array of data columns (see below) that should be returned for the link source.
99
+ * <tt>:limit</tt> - The # of links (limit) you would like to return.
100
+ * <tt>:offset</tt> = The number of records to offset before returning the 1st record of results.
101
+
102
+ === anchorMetrics
103
+
104
+ Returns anchor text metrics about the URI in question
105
+
106
+ client.anchorMetrics(uri, :cols => :all, :scope => "page_to_domain", :filters => :external, :sort => :domains_linking_page, :limit => 3, :scope => :phrase_to_page)
107
+
108
+ This call accepts the following options:
109
+
110
+ * <tt>:cols</tt> - An array of data columns (see below) that should be returned.
111
+ * <tt>:scope</tt> - A symbol representing the 'scope' of the anchor text data (see below).
112
+ * <tt>:sort</tt> - A symbol representing the 'sort order' of the anchor text data (see below).
113
+ * <tt>:filters</tt> - A symbol representing the 'filter' of the anchor text data (see below). NOTE: Only <tt>:external</tt> or <tt>internal</tt> filters may be used, separately.
114
+ * <tt>:limit</tt> - The # of links (limit) you would like to return.
115
+ * <tt>:offset</tt> = The number of records to offset before returning the 1st record of results.
116
+
117
+ == Requesting Data on Multiple URLs
118
+
119
+ For the API calls which support it, you may request data on multiple URLs by passing an
120
+ array of URLs to the API call.
121
+
122
+ urls = ["http://www.seomoz.org/blog/21-tactics-to-increase-blog-traffic", "http://www.seomoz.org/tools"]
123
+ response = client.urlMetrics(urls, :cols => :all)
124
+ response.data.first[:url]
125
+ >> "http://www.seomoz.org/blog/21-tactics-to-increase-blog-traffic"
126
+ response.data.first[:external_links]
127
+ >> 436
128
+
129
+ == Available Response Columns
130
+
131
+ Depending on the type of data point return, you may access certain data points inside the ResponseData object.
132
+
133
+ === Source/Target/URL Metrics
134
+
135
+ * <tt>:status</tt>
136
+ * <tt>:fq_domain_mozrank</tt>
137
+ * <tt>:pl_domain_links</tt>
138
+ * <tt>:pl_domain_external_links</tt>
139
+ * <tt>:fq_domain</tt>
140
+ * <tt>:fq_domain_moztrust</tt>
141
+ * <tt>:fq_domain_pl_domains_linking</tt>
142
+ * <tt>:pl_domain</tt>
143
+ * <tt>:url</tt>
144
+ * <tt>:page_authority_raw</tt>
145
+ * <tt>:pl_domain_external_mozrank_sum_raw</tt>
146
+ * <tt>:links</tt>
147
+ * <tt>:external_mozrank</tt>
148
+ * <tt>:pl_domain_mozrank</tt>
149
+ * <tt>:juice_links</tt>
150
+ * <tt>:title</tt>
151
+ * <tt>:fq_domains_linking</tt>
152
+ * <tt>:page_authority</tt>
153
+ * <tt>:fq_domain_external_mozrank_sum_raw</tt>
154
+ * <tt>:pl_domain_moztrust</tt>
155
+ * <tt>:fq_domain_external_links</tt>
156
+ * <tt>:domain_authority_raw</tt>
157
+ * <tt>:canonical_url</tt>
158
+ * <tt>:pl_domain_mozrank_sum_raw</tt>
159
+ * <tt>:fq_domain_links</tt>
160
+ * <tt>:all</tt>
161
+ * <tt>:mozrank</tt>
162
+ * <tt>:pl_domain_pl_domains_linking</tt>
163
+ * <tt>:external_links</tt>
164
+ * <tt>:fq_domain_fq_domains_linking</tt>
165
+ * <tt>:pl_domains_linking</tt>
166
+ * <tt>:domain_authority</tt>
167
+ * <tt>:fq_domain_mozrank_sum_raw</tt>
168
+ * <tt>:moztrust</tt>
169
+
170
+ === Link Metrics
171
+
172
+ * <tt>:text</tt>
173
+ * <tt>:mozrank</tt> (passed)
174
+
175
+ === Anchor Text Metrics
176
+
177
+ * <tt>:flags</tt>
178
+ * <tt>:internal_mozrank</tt>
179
+ * <tt>:internal_pages_linking</tt>
180
+ * <tt>:external_subdomains_linking</tt>
181
+ * <tt>:external_mozrank</tt>
182
+ * <tt>:text</tt>
183
+ * <tt>:internal_subdomains_linking</tt>
184
+ * <tt>:external_domains_linking</tt>
185
+ * <tt>:all</tt>
186
+ * <tt>:record_id</tt>
187
+ * <tt>:external_pages_linking</tt>
188
+
189
+ == Available Filters
190
+
191
+ Links may be filtered by any of the following. Anchor text may o
192
+
193
+ * <tt>:internal</tt>
194
+ * <tt>:external</tt>
195
+ * <tt>:redir301</tt>
196
+ * <tt>:follow</tt>
197
+ * <tt>:nofollow</tt>
198
+
199
+ == Anchor Text Scope
200
+
201
+ When requesting anchor text data, the following scopes may be used.
202
+
203
+ * <tt>:phrase_to_page</tt>
204
+ * <tt>:phrase_to_subdomain</tt>
205
+ * <tt>:phrase_to_domain</tt>
206
+ * <tt>:term_to_page</tt>
207
+ * <tt>:term_to_subdomain</tt>
208
+ * <tt>:term_to_domain</tt>
209
+
210
+ == Link Scope
211
+
212
+ When requesting links, the following scope may be used
213
+
214
+ * <tt>:page_to_page</tt>
215
+ * <tt>:page_to_subdomain</tt>
216
+ * <tt>:page_to_domain</tt>
217
+ * <tt>:domain_to_page</tt>
218
+ * <tt>:domain_to_subdomain</tt>
219
+ * <tt>:domain_to_domain</tt>
220
+
221
+ == Sort Orders
222
+
223
+ When sorting links, the following sort orders are available.
224
+
225
+ * <tt>:page_athority</tt>
226
+ * <tt>:domain_authority</tt>
227
+ * <tt>:domains_linking_page</tt>
228
+ * <tt>:domains_linking_domain</tt>
@@ -0,0 +1,53 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "linkscape"
8
+ gem.summary = %Q{Provides an interface to the SEOmoz API}
9
+ gem.description = %Q{Provides an interface to SEOmoz's suite of APIs, including the free and site intelligence APIs.}
10
+ gem.email = %q{api@seomoz.org}
11
+ gem.homepage = "http://www.seomoz.org/api"
12
+ gem.authors = ["Marty Smyth", "Jeff Pollard"]
13
+ gem.add_dependency "ruby-hmac", ">= 0"
14
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
+ end
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
19
+ end
20
+
21
+ require 'rake/testtask'
22
+ Rake::TestTask.new(:test) do |test|
23
+ test.libs << 'lib' << 'test'
24
+ test.pattern = 'test/**/test_*.rb'
25
+ test.verbose = true
26
+ end
27
+
28
+ begin
29
+ require 'rcov/rcovtask'
30
+ Rcov::RcovTask.new do |test|
31
+ test.libs << 'test'
32
+ test.pattern = 'test/**/test_*.rb'
33
+ test.verbose = true
34
+ end
35
+ rescue LoadError
36
+ task :rcov do
37
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
38
+ end
39
+ end
40
+
41
+ task :test => :check_dependencies
42
+
43
+ task :default => :test
44
+
45
+ require 'rake/rdoctask'
46
+ Rake::RDocTask.new do |rdoc|
47
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
48
+
49
+ rdoc.rdoc_dir = 'rdoc'
50
+ rdoc.title = "linkscape #{version}"
51
+ rdoc.rdoc_files.include('README*')
52
+ rdoc.rdoc_files.include('lib/**/*.rb')
53
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.6
@@ -0,0 +1,24 @@
1
+ # Copied from ActiveSupport, included if we don't already have it
2
+
3
+ class Hash
4
+ # Return a new hash with all keys converted to symbols.
5
+ def symbolize_keys
6
+ inject({}) do |options, (key, value)|
7
+ options[(key.downcase.to_sym rescue key) || key] = value
8
+ options
9
+ end
10
+ end
11
+ def symbolize_keys!
12
+ self.replace(self.symbolize_keys)
13
+ end
14
+ # Return a new hash with all keys converted to strings.
15
+ def stringify_keys
16
+ inject({}) do |options, (key, value)|
17
+ options[(key.to_s rescue key) || key] = value
18
+ options
19
+ end
20
+ end
21
+ def stringify_keys!
22
+ self.replace(self.stringify_keys)
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ require 'rubygems'
2
+ require 'cgi'
3
+ require 'base64'
4
+ require 'hmac-sha1'
5
+
6
+ directory = File.expand_path(File.dirname(__FILE__))
7
+
8
+ require File.join(directory, 'hash-ext') unless Hash.method_defined?(:symbolize_keys)
9
+ require File.join(directory, 'string-ext')
10
+
11
+ require File.join(directory, 'linkscape', 'signer')
12
+ require File.join(directory, 'linkscape', 'constants')
13
+
14
+ require File.join(directory, 'linkscape', 'client')
15
+ require File.join(directory, 'linkscape', 'request')
16
+ require File.join(directory, 'linkscape', 'response')
17
+ require File.join(directory, 'linkscape', 'errors')
@@ -0,0 +1,210 @@
1
+ module Linkscape
2
+ class Client
3
+ def initialize(*args)
4
+ options = Hash === args.last ? args.pop : {}
5
+ @accessID = args.first ? args.shift : (options[:id] || options[:ID] || options[:accessID])
6
+ @secretKey = args.first ? args.shift : (options[:secret] || options[:secretKey] || options[:key])
7
+
8
+ @options = {
9
+ :apiHost => 'lsapi.seomoz.com',
10
+ :apiRoot => 'linkscape',
11
+ :accessID => @accessID,
12
+ :secretKey => @secretKey
13
+ }.merge(options)
14
+
15
+ end
16
+
17
+
18
+ def mozRank(*args)
19
+ options = Hash === args.last ? args.pop.symbolize_keys : {}
20
+ url = args.first ? args.shift : options[:url]
21
+
22
+ raise MissingArgument, "mozRank requires a url." unless url
23
+
24
+ options[:url] = url
25
+ options[:api] = 'mozrank'
26
+
27
+ Linkscape::Request.run(@options.merge(options))
28
+ end
29
+
30
+
31
+ def urlMetrics(*args)
32
+ options = Hash === args.last ? args.pop.symbolize_keys : {}
33
+ url = args.first ? args.shift : options[:url]
34
+
35
+ raise MissingArgument, "urlMetrics requires a url." unless url
36
+
37
+ options[:url] = url
38
+ options[:api] = 'url-metrics'
39
+
40
+ options[:query] = {
41
+ 'Cols' => translateBitfield(options[:cols], options[:columns], options[:fields], :type => :url)
42
+ }
43
+
44
+ raise MissingArgument, "urlMetrics requires a list of columns to return." unless options[:query]['Cols'].nonzero?
45
+
46
+ Linkscape::Request.run(@options.merge(options))
47
+ end
48
+
49
+
50
+ def topLinks(*args)
51
+ options = Hash === args.last ? args.pop.symbolize_keys : {}
52
+ url = args.first ? args.shift : options[:url]
53
+ whichSet = (args.first ? args.shift : (options[:set] || 'none')).to_sym
54
+
55
+ raise MissingArgument, "topLinks requires a set (:page, :subdomain, :domain) and a url." unless whichSet and url
56
+
57
+ options[:url] = url
58
+
59
+ options[:api] = {
60
+ :page => 'page-linklist',
61
+ :subdomain => 'subdomain-linklist',
62
+ :domain => 'rootdomain-linklist',
63
+ }[whichSet]
64
+
65
+ raise InvalidArgument, "topLinks set must be one of :page, :subdomain, :domain" unless options[:api]
66
+
67
+ options[:query] = {
68
+ 'SourceCols' => translateBitfield(options[:sourcecols], options[:sourcecolumns], options[:urlcols], :type => :url),
69
+ 'TargetCols' => translateBitfield(options[:targetcols], options[:targetcolumns], options[:urlcols], :type => :url),
70
+ 'LinkCols' => translateBitfield(options[:cols], options[:columns], options[:linkcols], :type => :link),
71
+ }
72
+
73
+ raise MissingArgument, "topLinks requires a list of columns for Source, Target, and/or Link." unless options[:query]['SourceCols'].nonzero? or options[:query]['TargetCols'].nonzero? or options[:query]['LinkCols'].nonzero?
74
+
75
+ Linkscape::Request.run(@options.merge(options))
76
+ end
77
+
78
+
79
+ def allLinks(*args)
80
+ options = Hash === args.last ? args.pop.symbolize_keys : {}
81
+ url = args.first ? args.shift : options[:url]
82
+ scope = args.length >= 2 ? "#{args.shift}_to_#{args.shift}" : (args.first ? args.shift : options[:scope])
83
+ sortOrder = (args.first ? args.shift : (options[:sort] || options[:sortOrder] || 'domains_linking_page')).to_sym
84
+ filters = args.first ? args.shift : (options[:filters] || options[:filter] || '')
85
+
86
+ raise MissingArgument, "allLinks requires a scope ([page, domain] to [page, subdomain, domain]) and a url." unless scope and url
87
+ raise InvalidArgument, "allLinks scope must be valid ([page, domain] to [page, subdomain, domain])" unless scope.to_s =~ /^(page|domain)_to_(page|subdomain|domain)$/
88
+ raise InvalidArgument, "allLinks sort order must be valid (domain_authority, page_authority, domains_linking_page, domains_linking_domain)" unless [:domain_authority, :page_authority, :domains_linking_page, :domains_linking_domain].include? sortOrder
89
+
90
+ if String === filters
91
+ filters = filters.downcase.split(/[,\s\+]+/).sort.collect(&:to_sym)
92
+ elsif Hash === filters
93
+ filters = filters.keys.collect{|k|k.to_s.downcase}.sort.collect(&:to_sym)
94
+ elsif Array === filters
95
+ filters = filters.collect{|k|k.to_s.downcase}.sort.collect(&:to_sym)
96
+ elsif Symbol === filters
97
+ filters = [filters]
98
+ else
99
+ raise InvalidArgument, "allLinks filters must be a string, hash, or array"
100
+ end
101
+ raise InvalidArgument, "allLinks filters must be from the set (:internal, :external, :redir301, :follow, :nofollow)" unless (filters - [:internal, :external, :redir301, :follow, :nofollow]).empty?
102
+
103
+ options[:url] = url
104
+ options[:api] = 'links'
105
+
106
+ options[:query] = {
107
+ 'SourceCols' => translateBitfield(options[:sourcecols], options[:sourcecolumns], options[:urlcols], :type => :url),
108
+ 'TargetCols' => translateBitfield(options[:targetcols], options[:targetcolumns], options[:urlcols], :type => :url),
109
+ 'LinkCols' => translateBitfield(options[:cols], options[:columns], options[:linkcols], :type => :link),
110
+ 'Scope' => scope,
111
+ 'Filter' => filters.join(' ').gsub(/redir/, ''),
112
+ 'Sort' => sortOrder.to_s,
113
+ }
114
+
115
+ raise MissingArgument, "allLinks requires a list of columns for Source, Target, and/or Link." unless options[:query]['SourceCols'].nonzero? or options[:query]['TargetCols'].nonzero? or options[:query]['LinkCols'].nonzero?
116
+
117
+ Linkscape::Request.run(@options.merge(options))
118
+ end
119
+
120
+
121
+ def topPages(*args)
122
+ options = Hash === args.last ? args.pop.symbolize_keys : {}
123
+ url = args.first ? args.shift : options[:url]
124
+
125
+ options[:url] = url
126
+ options[:api] = 'top-pages'
127
+
128
+ options[:query] = {
129
+ 'Cols' => translateBitfield(options[:cols], options[:columns], options[:sourcecols], options[:sourcecolumns], options[:urlcols], :type => :url),
130
+ }
131
+
132
+ raise MissingArgument, "topPages requires a list of columns to return." unless options[:query]['Cols'].nonzero?
133
+
134
+ Linkscape::Request.run(@options.merge(options))
135
+ end
136
+
137
+
138
+ def anchorMetrics(*args)
139
+ options = Hash === args.last ? args.pop.symbolize_keys : {}
140
+ url = args.first ? args.shift : options[:url]
141
+ scope = args.length >= 2 ? "#{args.shift}_to_#{args.shift}" : (args.first ? args.shift : options[:scope])
142
+ filters = args.first ? args.shift : (options[:filters] || options[:filter] || '')
143
+ sortOrder = (args.first ? args.shift : (options[:sort] || options[:sortOrder] || 'domains_linking_page')).to_sym
144
+
145
+ raise InvalidArgument, "anchorMetrics scope must be valid ([phrase, term] to [page, subdomain, domain])" unless scope.to_s =~ /^(phrase|term)_to_(page|subdomain|domain)$/
146
+ raise InvalidArgument, "anchorMetrics sort order must be valid (domain_authority, page_authority, domains_linking_page, domains_linking_domain)" unless [:domain_authority, :page_authority, :domains_linking_page, :domains_linking_domain].include? sortOrder
147
+
148
+ if String === filters
149
+ filters = filters.downcase.split(/[,\s\+]+/).sort.collect(&:to_sym)
150
+ elsif Hash === filters
151
+ filters = filters.keys.collect{|k|k.to_s.downcase}.sort.collect(&:to_sym)
152
+ elsif Array === filters
153
+ filters = filters.collect{|k|k.to_s.downcase}.sort.collect(&:to_sym)
154
+ elsif Symbol === filters
155
+ filters = [filters]
156
+ else
157
+ raise InvalidArgument, "anchorMetrics filters must be a string, hash, or array"
158
+ end
159
+ raise InvalidArgument, "anchorMetrics filters must be from the set (:internal, :external)" unless (filters - [:internal, :external]).empty?
160
+
161
+ options[:url] = url
162
+ options[:api] = 'anchor-text'
163
+
164
+ options[:query] = {
165
+ 'Cols' => translateBitfield(options[:cols], options[:columns], options[:anchorcols], :type => :anchors),
166
+ 'Scope' => scope,
167
+ 'Filter' => filters.join(' ').gsub(/redir/, ''),
168
+ 'Sort' => sortOrder.to_s,
169
+ }
170
+
171
+ # raise MissingArgument, "anchorMetrics requires a list of columns to return." unless options[:query]['Cols'].nonzero?
172
+
173
+ Linkscape::Request.run(@options.merge(options))
174
+ end
175
+
176
+
177
+
178
+
179
+ def inspect
180
+ %Q[#<#{self.class}:#{"0x%x" % self.object_id} api="#{@options[:apiHost]}/#{@options[:apiRoot]}" accessID="#{@options[:accessID]}">]
181
+ end
182
+
183
+ private
184
+
185
+ def translateBitfield *columns
186
+ options = Hash === columns.last ? columns.pop : {}
187
+
188
+ columns.flatten!
189
+ columns.compact!
190
+
191
+ bits = columns.inject(0) do |bitfield, key|
192
+ requestBit =
193
+ if options[:type] == :url
194
+ Linkscape::Constants::URLMetrics::RequestBits[key]
195
+ elsif options[:type] == :link
196
+ Linkscape::Constants::LinkMetrics::RequestBits[key]
197
+ elsif options[:type] == :anchors
198
+ Linkscape::Constants::AnchorMetrics::RequestBits[key]
199
+ else
200
+ Linkscape::Constants::URLMetrics::RequestBits[key] || Linkscape::Constants::LinkMetrics::RequestBits[key]
201
+ end
202
+ raise InvalidArgument, "Unknown #{options[:type]} flag '#{key.inspect}'".gsub(/ +/, ' ') unless requestBit
203
+ bitfield |= requestBit[:flag]
204
+ end
205
+
206
+ bits
207
+ end
208
+
209
+ end
210
+ end