serpscan 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +82 -0
  6. data/Rakefile +1 -0
  7. data/lib/serpscan.rb +16 -0
  8. data/lib/serpscan/account.rb +8 -0
  9. data/lib/serpscan/api.rb +48 -0
  10. data/lib/serpscan/errors.rb +3 -0
  11. data/lib/serpscan/keyword.rb +10 -0
  12. data/lib/serpscan/search_engine.rb +6 -0
  13. data/lib/serpscan/search_engine_country.rb +21 -0
  14. data/lib/serpscan/serpscan_child_object.rb +23 -0
  15. data/lib/serpscan/serpscan_object.rb +68 -0
  16. data/lib/serpscan/utilities.rb +18 -0
  17. data/lib/serpscan/version.rb +3 -0
  18. data/lib/serpscan/website.rb +16 -0
  19. data/serpscan.gemspec +30 -0
  20. data/spec/fixtures/vcr_cassettes/Serpscan_Keyword/it_should_behave_like_a_creatable_object/should_return_object_after_creation.yml +52 -0
  21. data/spec/fixtures/vcr_cassettes/Serpscan_Keyword/it_should_behave_like_a_deletable_object/should_return_true_after_delete.yml +86 -0
  22. data/spec/fixtures/vcr_cassettes/Serpscan_Keyword/it_should_behave_like_a_findable_object/should_return_a_value_for_alltime_change.yml +46 -0
  23. data/spec/fixtures/vcr_cassettes/Serpscan_Keyword/it_should_behave_like_a_findable_object/should_return_a_value_for_current_rank.yml +46 -0
  24. data/spec/fixtures/vcr_cassettes/Serpscan_Keyword/it_should_behave_like_a_findable_object/should_return_a_value_for_day_change.yml +46 -0
  25. data/spec/fixtures/vcr_cassettes/Serpscan_Keyword/it_should_behave_like_a_findable_object/should_return_a_value_for_id.yml +46 -0
  26. data/spec/fixtures/vcr_cassettes/Serpscan_Keyword/it_should_behave_like_a_findable_object/should_return_a_value_for_initial_rank.yml +46 -0
  27. data/spec/fixtures/vcr_cassettes/Serpscan_Keyword/it_should_behave_like_a_findable_object/should_return_a_value_for_phrase.yml +46 -0
  28. data/spec/fixtures/vcr_cassettes/Serpscan_Keyword/it_should_behave_like_a_findable_object/should_return_a_value_for_search_engine_country_id.yml +46 -0
  29. data/spec/fixtures/vcr_cassettes/Serpscan_Keyword/it_should_behave_like_a_findable_object/should_return_a_value_for_search_volume.yml +46 -0
  30. data/spec/fixtures/vcr_cassettes/Serpscan_Keyword/it_should_behave_like_a_findable_object/should_return_a_value_for_week_change.yml +46 -0
  31. data/spec/fixtures/vcr_cassettes/Serpscan_SearchEngine/it_should_behave_like_a_findable_object/should_return_a_value_for_countries.yml +46 -0
  32. data/spec/fixtures/vcr_cassettes/Serpscan_SearchEngine/it_should_behave_like_a_findable_object/should_return_a_value_for_id.yml +46 -0
  33. data/spec/fixtures/vcr_cassettes/Serpscan_SearchEngine/it_should_behave_like_a_findable_object/should_return_a_value_for_title.yml +46 -0
  34. data/spec/fixtures/vcr_cassettes/Serpscan_SearchEngine/it_should_behave_like_a_listable_object/should_return_an_array_of_Serpscan_SearchEngine_objects.yml +46 -0
  35. data/spec/fixtures/vcr_cassettes/Serpscan_SearchEngine/it_should_behave_like_a_listable_object/should_return_an_array_of_objects.yml +46 -0
  36. data/spec/fixtures/vcr_cassettes/Serpscan_Website/create_keyword/should_return_a_keyword_object.yml +96 -0
  37. data/spec/fixtures/vcr_cassettes/Serpscan_Website/it_should_behave_like_a_creatable_object/should_return_object_after_creation.yml +51 -0
  38. data/spec/fixtures/vcr_cassettes/Serpscan_Website/it_should_behave_like_a_deletable_object/should_return_true_after_delete.yml +86 -0
  39. data/spec/fixtures/vcr_cassettes/Serpscan_Website/it_should_behave_like_a_findable_object/should_return_a_value_for_id.yml +46 -0
  40. data/spec/fixtures/vcr_cassettes/Serpscan_Website/it_should_behave_like_a_findable_object/should_return_a_value_for_url.yml +46 -0
  41. data/spec/fixtures/vcr_cassettes/Serpscan_Website/it_should_behave_like_a_listable_object/should_return_an_array_of_Serpscan_Website_objects.yml +46 -0
  42. data/spec/fixtures/vcr_cassettes/Serpscan_Website/it_should_behave_like_a_listable_object/should_return_an_array_of_objects.yml +46 -0
  43. data/spec/fixtures/vcr_cassettes/Serpscan_Website/keywords/should_return_a_paginated_array_of_keyword_objects.yml +663 -0
  44. data/spec/fixtures/vcr_cassettes/Serpscan_Website/keywords/should_return_an_array_of_keyword_objects.yml +90 -0
  45. data/spec/lib/serpscan/keyword_spec.rb +8 -0
  46. data/spec/lib/serpscan/search_engine_spec.rb +6 -0
  47. data/spec/lib/serpscan/website_spec.rb +32 -0
  48. data/spec/spec_helper.rb +24 -0
  49. data/spec/support/shared_examples/creatable.rb +7 -0
  50. data/spec/support/shared_examples/deletable.rb +7 -0
  51. data/spec/support/shared_examples/findable.rb +9 -0
  52. data/spec/support/shared_examples/listable.rb +7 -0
  53. metadata +240 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bef7e95dba139b3f0ded2f2e0b72c322a705c246
4
+ data.tar.gz: 5fddfcf2957118ef0a3de69486a0a06918008405
5
+ SHA512:
6
+ metadata.gz: 9d30ff1ae77e6c9aa5768339e5e4c0c832759e94c1d71a7ba7d4825fa268a4f128686d40d34ee30016b1916398cb590116f63d4dac05d859dd79c1ca85e2c359
7
+ data.tar.gz: d50dc61fc0ea9b32161ca636a4c3d00aa8630856ffee9408277258b233e1b12966cd03e2324ede9ca5d59267a45ba77f2a38d4265a737e274132275a188b7ef3
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in serpscan.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Dylan Montgomery
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,82 @@
1
+ # SERP Scan API
2
+
3
+ ##### SERP Scan tracks your website's search engine position for the keywords that matter to you.
4
+
5
+ The serpscan gem is a ruby wrapper for interacting with the SERP Scan API. Before you can use the gem you'll need a SERP Scan account (https://serpscan.com), and an api key which can be found on the accounts page.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'serpscan'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install serpscan
22
+
23
+ ## Usage
24
+
25
+ Set your API key. If you're using Rails you may want to place the code below in a file at config/initializers/serpscan.rb.
26
+
27
+ ```ruby
28
+ Serpscan.api_key = 'YOUR API KEY'
29
+ ```
30
+
31
+ To create a website:
32
+
33
+ ```ruby
34
+ Serpscan::Website.create(url: 'example.com')
35
+ ```
36
+
37
+ To get a list of the websites currently in your account:
38
+
39
+
40
+ ```ruby
41
+ Serpscan::Website.all
42
+ ```
43
+
44
+ To get a particular website:
45
+
46
+ ```ruby
47
+ Serpscan::Website.find(id)
48
+ ```
49
+
50
+ To get a list of keywords for a website:
51
+
52
+ ```ruby
53
+ website = Serpscan::Website.find(1)
54
+ website.keywords
55
+ ```
56
+
57
+ To create a keyword:
58
+
59
+ ```ruby
60
+ website = Serpscan::Website.find(1)
61
+ website.create_keyword(phrase: 'example keyword')
62
+ ```
63
+
64
+ ## Attributes
65
+
66
+ Each of these attributes can be called directly on the object. Example:
67
+ ```
68
+ website = Serpscan::Website.find(1)
69
+ website.id # => 1
70
+ ```
71
+
72
+ Websites:
73
+
74
+ ```Website
75
+ [:id, :url]
76
+ ```
77
+
78
+ Keywords:
79
+
80
+ ```ruby
81
+ [:id, :phrase, :current_rank, :initial_rank, :day_change, :week_change, :alltime_change, :search_volume, :search_engine_country_id]
82
+ ```
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,16 @@
1
+ require 'serpscan/version'
2
+ require 'serpscan/api'
3
+ require 'serpscan/utilities'
4
+ require 'serpscan/serpscan_object'
5
+ require 'serpscan/serpscan_child_object'
6
+ require 'serpscan/keyword'
7
+ require 'serpscan/search_engine'
8
+ require 'serpscan/website'
9
+ require 'serpscan/errors'
10
+ require 'queryparams'
11
+
12
+ module Serpscan
13
+ class << self
14
+ attr_accessor :api_key
15
+ end
16
+ end
@@ -0,0 +1,8 @@
1
+ require 'rest_client'
2
+
3
+ module Serpscan
4
+ class Account < SerpscanObject
5
+ ATTRIBUTES = []
6
+ attr_accessor *ATTRIBUTES
7
+ end
8
+ end
@@ -0,0 +1,48 @@
1
+ require 'rest_client'
2
+
3
+ module Serpscan
4
+ class API
5
+ BASE_URL = 'https://serpscan.com/api/v1'
6
+ class << self
7
+ def get(path, options = {})
8
+ url = api_url(path)
9
+ if options[:params]
10
+ url += "&#{querify(options[:params])}"
11
+ end
12
+
13
+ normalize_response RestClient.get(url)
14
+ end
15
+
16
+ def delete(path)
17
+ RestClient.delete(api_url(path))
18
+ end
19
+
20
+ def post(path, options = {})
21
+ normalize_response RestClient.post(api_url(path), options[:params].to_json)
22
+ end
23
+
24
+ def put(path, options = {})
25
+ normalize_response RestClient.put(api_url(path), options[:params].to_json, content_type: :json)
26
+ end
27
+
28
+ private
29
+
30
+ def normalize_response(response)
31
+ JSON.parse(response)
32
+ end
33
+
34
+ def api_url(path)
35
+ "#{BASE_URL}#{tokenize(path)}"
36
+ end
37
+
38
+ def querify(hash)
39
+ hash.map { |k, v| "#{k}=#{v}".to_s }.join('&')
40
+ end
41
+
42
+ def tokenize(path)
43
+ param_separator = path.include?('?') ? '&' : '?'
44
+ path += "#{param_separator}token=#{Serpscan.api_key}"
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,3 @@
1
+ module Serpscan
2
+ class ParentError < StandardError; end
3
+ end
@@ -0,0 +1,10 @@
1
+ module Serpscan
2
+ class Keyword < SerpscanChildObject
3
+ ATTRIBUTES = [:id, :phrase, :current_rank, :initial_rank, :day_change, :week_change, :alltime_change, :search_volume, :search_engine_country_id]
4
+ attr_accessor *ATTRIBUTES
5
+
6
+ def website
7
+ parent
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,6 @@
1
+ module Serpscan
2
+ class SearchEngine < SerpscanObject
3
+ ATTRIBUTES = [:id, :title, :countries]
4
+ attr_accessor *ATTRIBUTES
5
+ end
6
+ end
@@ -0,0 +1,21 @@
1
+ module Serpscan
2
+ class SearchEngineCountry < SerpscanChildObject
3
+ ATTRIBUTES = [:id, :location, :search_engine_id]
4
+ attr_accessor *ATTRIBUTES
5
+
6
+ def search_engine
7
+ parent
8
+ end
9
+
10
+ def save(params = {})
11
+ super(params)
12
+ end
13
+
14
+ class << self
15
+ def create
16
+ params = { Serpscan::Utilities.object_to_string(self) => params }
17
+ API.post("#{api_path}?#{QueryParams.encode(params)}", params: params, json: true)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ module Serpscan
2
+ class SerpscanChildObject < SerpscanObject
3
+ attr_accessor :parent
4
+
5
+ def initialize(options = {})
6
+ @parent = options[:parent]
7
+ super
8
+ end
9
+
10
+ class << self
11
+ def all(params = {})
12
+ params.merge!(parent: params[:parent], child: self)
13
+ super
14
+ end
15
+
16
+ def create(params = {})
17
+ object = new(params)
18
+ params.merge!("#{object.parent.object_name}_id" => object.parent.id) if object.parent
19
+ super
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,68 @@
1
+ module Serpscan
2
+ class SerpscanObject
3
+ def initialize(options = {})
4
+ assign_attributes(options)
5
+ end
6
+
7
+ def api_path
8
+ SerpscanObject.api_path(self)
9
+ end
10
+
11
+ def object_name
12
+ Serpscan::Utilities.object_to_string(self)
13
+ end
14
+
15
+ def delete
16
+ Serpscan::API.delete("#{api_path}/#{id}")
17
+ true
18
+ end
19
+
20
+ class << self
21
+ def api_path(object = self)
22
+ "/#{Serpscan::Utilities.object_to_string(object)}s"
23
+ end
24
+
25
+ def all(options = {})
26
+ parent = options[:parent]
27
+ child = options[:child]
28
+ objects = options.fetch(:objects, [])
29
+
30
+ url = options[:parent] ? "#{parent.api_path}/#{parent.id}#{child.api_path}" : api_path
31
+ results = Serpscan::API.get(url, params: options[:params])
32
+
33
+ results['results'].map do |object|
34
+ objects << new(object.merge(parent: options[:parent]))
35
+ end
36
+
37
+ consider_pagination(results, options, objects)
38
+ objects
39
+ end
40
+
41
+ def find(id)
42
+ results = Serpscan::API.get("#{api_path}/#{id}")
43
+ new(results)
44
+ end
45
+
46
+ def create(params)
47
+ parent_object = params.delete(:parent)
48
+ params = { Serpscan::Utilities.object_to_string(self) => params }
49
+ results = Serpscan::API.post("#{api_path}?#{QueryParams.encode(params)}", params: params)
50
+ new(results.merge(parent: parent_object))
51
+ end
52
+
53
+ def consider_pagination(results, options, objects)
54
+ if results['page'] && results['page'].to_i < results['total_pages']
55
+ (options[:child]).all(options.merge(objects: objects, params: { page: results['page'].to_i + 1 }))
56
+ end
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def assign_attributes(options = {})
63
+ self.class::ATTRIBUTES.each do |attribute|
64
+ send("#{attribute}=", options[attribute.to_s])
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,18 @@
1
+ module Serpscan
2
+ class Utilities
3
+ class << self
4
+ def object_to_string(object)
5
+ object_name = object.is_a?(Class) ? object.name : object.class
6
+ underscore(object_name.to_s.split('::').last).downcase
7
+ end
8
+
9
+ def underscore(string)
10
+ string.gsub(/::/, '/')
11
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
12
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
13
+ .tr('-', '_')
14
+ .downcase
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ module Serpscan
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,16 @@
1
+ require 'rest_client'
2
+
3
+ module Serpscan
4
+ class Website < SerpscanObject
5
+ ATTRIBUTES = [:id, :url]
6
+ attr_accessor *ATTRIBUTES
7
+
8
+ def create_keyword(phrase)
9
+ keyword = Serpscan::Keyword.create(parent: self, phrase: phrase)
10
+ end
11
+
12
+ def keywords
13
+ @keywords ||= Serpscan::Keyword.all(parent: self)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'serpscan/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'serpscan'
8
+ spec.version = Serpscan::VERSION
9
+ spec.authors = ['Dylan Montgomery']
10
+ spec.email = ['mail@citizensinspace.com']
11
+ spec.summary = 'API client for SERP Scan rank tracker'
12
+ spec.description = spec.summary
13
+ spec.homepage = ''
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.7'
22
+ spec.add_development_dependency 'rake', '~> 10.0'
23
+ spec.add_development_dependency 'rspec'
24
+ spec.add_development_dependency 'vcr'
25
+ spec.add_development_dependency 'webmock'
26
+
27
+ spec.add_dependency 'rest-client'
28
+ spec.add_dependency 'json'
29
+ spec.add_dependency 'queryparams'
30
+ end
@@ -0,0 +1,52 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: post
5
+ uri: http://localhost:5055/api/v1/keywords?keyword%5Bkeyword%5D=example%20keyword&keyword%5Bsearch_engine_country_id%5D=1&keyword%5Bwebsite_id%5D=1&token=123
6
+ body:
7
+ encoding: UTF-8
8
+ string: '{"keyword":{"keyword":"example keyword","website_id":1,"search_engine_country_id":1}}'
9
+ headers:
10
+ Accept:
11
+ - "*/*; q=0.5, application/xml"
12
+ Accept-Encoding:
13
+ - gzip, deflate
14
+ Content-Length:
15
+ - '85'
16
+ User-Agent:
17
+ - Ruby
18
+ response:
19
+ status:
20
+ code: 201
21
+ message: 'Created '
22
+ headers:
23
+ Content-Type:
24
+ - application/json; charset=utf-8
25
+ X-Ua-Compatible:
26
+ - IE=Edge
27
+ Etag:
28
+ - '"b468971b71df0e30c02b082c707379cf"'
29
+ Cache-Control:
30
+ - max-age=0, private, must-revalidate
31
+ X-Request-Id:
32
+ - 7024f78e2d3309f8fd844a19208d4da7
33
+ X-Runtime:
34
+ - '0.069711'
35
+ Server:
36
+ - WEBrick/1.3.1 (Ruby/1.9.3/2013-01-15)
37
+ Date:
38
+ - Sun, 22 Feb 2015 20:43:19 GMT
39
+ Content-Length:
40
+ - '246'
41
+ Connection:
42
+ - Keep-Alive
43
+ Set-Cookie:
44
+ - _serps_session=BAh7BkkiD3Nlc3Npb25faWQGOgZFRkkiJTgxOTYyYTcxZGNkNzk1NjQ3MGFmZGUwNzQ0MTNjYThjBjsAVA%3D%3D--08f481b92b725e3339e9a3d8177e6c10e57ab1a2;
45
+ path=/; HttpOnly
46
+ body:
47
+ encoding: UTF-8
48
+ string: '{"website_id":null,"page":null,"total_pages":null,"results":[{"id":3,"phrase":"example
49
+ keyword","current_rank":null,"initial_rank":null,"day_change":null,"week_change":null,"alltime_change":"_","search_volume":null,"search_engine_country_id":1}]}'
50
+ http_version:
51
+ recorded_at: Sun, 22 Feb 2015 20:43:19 GMT
52
+ recorded_with: VCR 2.9.3