linkscape 0.2.6

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.
@@ -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