mocrata 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6ea5004761040d9a269f778003915705495aeb80
4
+ data.tar.gz: 29f11a520caf96642656e6e0268b643f3d284cae
5
+ SHA512:
6
+ metadata.gz: d887cafd67ad5669aa6560c169eba34464db314df0763ebbc5dd697b22bf2b32d5f0312dc192d8323e9630cea1d1b03c420f33934fa08a855dab19792639820f
7
+ data.tar.gz: 6ac06af00a4d45b148edd57adabf239f2d26e653f1ded892f9b36acd82ad99d94631858f3d7a6d6d20e6593880ff83120b1f27d2a917dcac8e5322b4169b1bc3
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --markup=markdown
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mocrata.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Heather Rivers
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.
data/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # Mocrata
2
+
3
+ Mocrata is a [SODA](http://dev.socrata.com/) (Socrata Open Data API) client
4
+ developed by [Mode Analytics](https://modeanalytics.com).
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem 'mocrata'
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install mocrata
19
+
20
+ ## Usage
21
+
22
+ ### Setup
23
+
24
+ ```
25
+ Mocrata.configure do |config|
26
+ config.app_token = 'yourtoken' # optional Socrata application token
27
+ end
28
+ ```
29
+
30
+ ### Accessing data
31
+
32
+ ```
33
+ dataset = Mocrata::Dataset.new('http://soda.demo.socrata.com/resource/6xzm-fzcu')
34
+
35
+ dataset.csv
36
+ => [["Sally", 10], ["Earl", 2]]
37
+
38
+ dataset.json
39
+ => [{"name"=>"Sally", "age"=>10}, {"name"=>"Earl", "age"=>2}]
40
+
41
+ dataset.fields
42
+ => {"name"=>"text", "age"=>"number"}
43
+ ```
44
+
45
+ ### Iterating through rows
46
+
47
+ ```
48
+ dataset.each_row(:csv) do |row|
49
+ # do something with the row
50
+ end
51
+
52
+ dataset.each_row(:json) { |row| ... }
53
+ ```
54
+
55
+ ## Contributing
56
+
57
+ 1. Fork it
58
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
59
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
60
+ 4. Push to the branch (`git push origin my-new-feature`)
61
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,37 @@
1
+ # encoding: utf-8
2
+ #
3
+ module Mocrata
4
+ # @attr [String] app_token A Socrata application token
5
+ #
6
+ class Configuration
7
+ # The maximum number of rows allowed per request by Socrata
8
+ MAX_PER_PAGE = 1000
9
+
10
+ attr_accessor :app_token
11
+
12
+ # @return [Integer] the value of the `per_page` configuration option
13
+ #
14
+ def per_page
15
+ @per_page ||= MAX_PER_PAGE
16
+ end
17
+
18
+ # Sets the value of the `per_page` configuration option
19
+ #
20
+ # @param value [Integer] the number of results per page {http://dev.socrata.com SODA} resource url
21
+ #
22
+ # @return [Integer] the value
23
+ #
24
+ # @raise [Mocrata::Configuration::ConfigurationError] if the value is invalid
25
+ #
26
+ def per_page=(value)
27
+ if value > MAX_PER_PAGE
28
+ message = "Per page #{value} exceeds maximum value of #{MAX_PER_PAGE}"
29
+ raise ConfigurationError.new(message)
30
+ end
31
+
32
+ @per_page = value
33
+ end
34
+
35
+ class ConfigurationError < StandardError; end
36
+ end
37
+ end
@@ -0,0 +1,131 @@
1
+ # encoding: utf-8
2
+ #
3
+ module Mocrata
4
+ # A Mocrata::Dataset instance represents a SODA dataset and provides
5
+ # interfaces for reading its metadata and contents in supported formats.
6
+ #
7
+ class Dataset
8
+ # Construct a new Dataset instance
9
+ #
10
+ # @param url [String] valid {http://dev.socrata.com SODA} resource url
11
+ #
12
+ # @return [Mocrata::Dataset] the instance
13
+ #
14
+ # @example
15
+ # dataset = Mocrata::Dataset.new('http://data.sfgov.org/resource/funx-qxxn')
16
+ #
17
+ def initialize(url)
18
+ @url = url
19
+ end
20
+
21
+ # Iterate through each row of the dataset
22
+ #
23
+ # @param format [Symbol, String] the format, `:json` or `:csv`
24
+ #
25
+ # @yield [Array<Array>] row of values
26
+ #
27
+ # @example
28
+ # dataset.each_row(:json) do |row|
29
+ # # do something with the row
30
+ # end
31
+ #
32
+ def each_row(format, &block)
33
+ each_page(format) do |page|
34
+ page.each(&block)
35
+ end
36
+ end
37
+
38
+ # Iterate through each page of the dataset
39
+ #
40
+ # @param format [Symbol, String] the format, `:json` or `:csv`
41
+ # @param per_page [optional, Integer] the number of rows to return for each page
42
+ #
43
+ # @yield [Array<Array>] page of rows
44
+ #
45
+ # @example
46
+ # dataset.each_page(:csv) do |page|
47
+ # # do something with the page
48
+ # end
49
+ #
50
+ def each_page(format, per_page = nil, &block)
51
+ page = 1
52
+ per_page ||= Mocrata.config.per_page
53
+
54
+ while true
55
+ rows = send(format, :page => page, :per_page => per_page)
56
+ yield rows
57
+ break if rows.size < per_page
58
+ page += 1
59
+ end
60
+ end
61
+
62
+ # The contents of the dataset in CSV format
63
+ #
64
+ # @param params [optional, Hash] hash of options to pass along to the HTTP request
65
+ #
66
+ # @option params [Integer] :page the page to request
67
+ # @option params [Integer] :per_page the number of rows to return for each page
68
+ #
69
+ # @return [Array<Array>] the array of rows
70
+ #
71
+ # @example
72
+ # dataset.csv(:page => 2, :per_page => 10)
73
+ #
74
+ def csv(params = {})
75
+ get(:csv, params).body
76
+ end
77
+
78
+ # The contents of the dataset in JSON format
79
+ #
80
+ # @param params [optional, Hash] hash of options to pass along to the HTTP request
81
+ #
82
+ # @option params [Integer] :page the page to request
83
+ # @option params [Integer] :per_page the number of rows to return for each page
84
+ #
85
+ # @return [Array<Hash>] the array of rows
86
+ #
87
+ # @example
88
+ # dataset.json(:page => 2, :per_page => 10)
89
+ #
90
+ def json(params = {})
91
+ get(:json, params).body
92
+ end
93
+
94
+ # Get the headers associated with the dataset
95
+ #
96
+ # @return [Hash] a hash of headers
97
+ #
98
+ def headers
99
+ # SODA doesn't support HEAD requests, unfortunately
100
+ @headers ||= get(:json, :per_page => 0).headers
101
+ end
102
+
103
+ # A hash of field names and types from headers
104
+ #
105
+ # @return [Hash] a hash of field names and types
106
+ #
107
+ def fields
108
+ Hash[field_names.zip(field_types)]
109
+ end
110
+
111
+ private
112
+
113
+ attr_reader :url
114
+
115
+ def get(format, params = {})
116
+ Mocrata::Request.new(base_url, format, params).response
117
+ end
118
+
119
+ def base_url
120
+ @base_url ||= Mocrata::DatasetUrl.new(url).normalize
121
+ end
122
+
123
+ def field_names
124
+ headers.fetch('x-soda2-fields', [])
125
+ end
126
+
127
+ def field_types
128
+ headers.fetch('x-soda2-types', [])
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,84 @@
1
+ # encoding: utf-8
2
+ #
3
+ module Mocrata
4
+ # @attr_reader [String] original the original Socrata dataset URL
5
+ #
6
+ class DatasetUrl
7
+ attr_reader :original
8
+
9
+ # Construct a new DatasetUrl instance
10
+ #
11
+ # @param original [String] the original Socrata dataset URL
12
+ #
13
+ # @return [Mocrata::DatasetUrl] the instance
14
+ #
15
+ # @example
16
+ # url = Mocrata::DatasetUrl.new('http://data.sfgov.org/resource/funx-qxxn')
17
+ #
18
+ def initialize(original)
19
+ @original = original
20
+ end
21
+
22
+ # Normalize a Socrata dataset URL. Ensures https protocol. Removes query
23
+ # string and fragment, if any.
24
+ #
25
+ # @return [String] the normalized URL
26
+ #
27
+ def normalize
28
+ uri = URI(self.class.ensure_protocol(original))
29
+
30
+ uri.scheme = 'https'
31
+ uri.fragment = nil
32
+ uri.query = nil
33
+
34
+ self.class.strip_format(uri.to_s)
35
+ end
36
+
37
+ # Validate the original URL against the expected Socrata dataset URL
38
+ # pattern
39
+ #
40
+ # @raise [Mocrata::DatasetUrl::InvalidError] if the URL is invalid
41
+ #
42
+ def validate!
43
+ unless original =~ VALID_PATTERN
44
+ raise InvalidError.new("Invalid URL: #{original.inspect}")
45
+ end
46
+
47
+ true
48
+ end
49
+
50
+ class << self
51
+ # Ensure that a URL has a valid protocol
52
+ #
53
+ # @param url [String] the url with or without protocol
54
+ #
55
+ # @return [String] the url with protocol
56
+ #
57
+ def ensure_protocol(url)
58
+ if url =~ /\A\/\//
59
+ url = "https:#{url}"
60
+ elsif url !~ /\Ahttps?:\/\//
61
+ url = "https://#{url}"
62
+ end
63
+
64
+ url
65
+ end
66
+
67
+ # Strip explicit format from a given URL if present
68
+ #
69
+ # @param url [String] the url with or without format
70
+ #
71
+ # @return [String] the url without format
72
+ #
73
+ def strip_format(url)
74
+ url.gsub(/\.[a-zA-Z]+\Z/, '')
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ VALID_PATTERN = /\/resource\//
81
+
82
+ class InvalidError < StandardError; end
83
+ end
84
+ end
@@ -0,0 +1,94 @@
1
+ # encoding: utf-8
2
+ #
3
+ require 'cgi'
4
+ require 'csv'
5
+ require 'json'
6
+ require 'net/https'
7
+
8
+ module Mocrata
9
+ # @attr_reader [String] url the request URL
10
+ # @attr_reader [Symbol] format the request format, `:json` or `:csv`
11
+ # @attr_reader [Hash] params the requst params
12
+ #
13
+ class Request
14
+ attr_reader :url, :format, :params
15
+
16
+ # Construct a new Request instance
17
+ #
18
+ # @param url [String] the request URL
19
+ # @param format [Symbol] the request format, `:json` or `:csv`
20
+ # @param params [Hash] the requst params
21
+ #
22
+ # @return [Mocrata::Request] the instance
23
+ #
24
+ def initialize(url, format, params = {})
25
+ @url = url
26
+ @format = format
27
+ @params = params
28
+ end
29
+
30
+ # Perform the HTTP GET request
31
+ #
32
+ # @return [Mocrata::Response] the validated response
33
+ #
34
+ def response
35
+ request = Net::HTTP::Get.new(uri.request_uri)
36
+
37
+ request.add_field('Accept', content_type)
38
+ request.add_field('X-App-Token', Mocrata.config.app_token)
39
+
40
+ response = http.request(request)
41
+
42
+ Mocrata::Response.new(response).tap(&:validate!)
43
+ end
44
+
45
+ # @return [String] the content type for the specified format
46
+ #
47
+ # @raise [Mocrata::Request::RequestError] if the format is not supported
48
+ #
49
+ def content_type
50
+ Mocrata::CONTENT_TYPES.fetch(format, nil).tap do |type|
51
+ raise RequestError.new("Invalid format: #{format}") unless type
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def http
58
+ @http ||= Net::HTTP.new(uri.host, uri.port).tap do |http|
59
+ http.use_ssl = true
60
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
61
+ end
62
+ end
63
+
64
+ def soda_params
65
+ @soda_params ||= {}.tap do |soda|
66
+ limit = params.fetch(:per_page, Mocrata.config.per_page)
67
+ page = params.fetch(:page, 1)
68
+
69
+ soda[:$limit] = limit
70
+ soda[:$offset] = (page - 1) * limit
71
+ end
72
+ end
73
+
74
+ def uri
75
+ @uri ||= URI(url).dup.tap do |uri|
76
+ uri.query = self.class.query_string(soda_params)
77
+ end
78
+ end
79
+
80
+ class << self
81
+ # Construct a query string from a hash
82
+ #
83
+ # @param hash [Hash] the hash of parmas
84
+ #
85
+ # @return [String] the query string
86
+ #
87
+ def query_string(hash)
88
+ hash.map { |k, v| "#{k}=#{CGI::escape(v.to_s)}" }.join('&')
89
+ end
90
+ end
91
+
92
+ class RequestError < StandardError; end
93
+ end
94
+ end
@@ -0,0 +1,84 @@
1
+ # encoding: utf-8
2
+ #
3
+ require 'json'
4
+ require 'csv'
5
+
6
+ module Mocrata
7
+ class Response
8
+ # Construct a new Response instance
9
+ #
10
+ # @param http_response [Net::HTTPResponse] the http response
11
+ #
12
+ # @return [Mocrata::Response] the instance
13
+ #
14
+ def initialize(http_response)
15
+ @http_response = http_response
16
+ end
17
+
18
+ # Perform certain checks against the HTTP response and raise an exception
19
+ # if necessary
20
+ #
21
+ # @return [true]
22
+ #
23
+ # @raise [Mocrata::Response::ResponseError] if the response is invalid
24
+ #
25
+ def validate!
26
+ if content_type == :json
27
+ if body.respond_to?(:has_key?) && body.has_key?('error')
28
+ raise ResponseError.new("API error: #{body['message']}")
29
+ end
30
+ end
31
+
32
+ true
33
+ end
34
+
35
+ # HTTP headers with certain values parsed as JSON
36
+ #
37
+ # @return [Hash] the header keys and values
38
+ #
39
+ def headers
40
+ @headers ||= {}.tap do |result|
41
+ http_response.each_header do |key, value|
42
+ value = JSON.parse(value) if JSON_HEADERS.include?(key)
43
+
44
+ result[key] = value
45
+ end
46
+ end
47
+ end
48
+
49
+ # The HTTP response body, processed according to content type
50
+ #
51
+ # @return [Array] the parsed body
52
+ #
53
+ def body
54
+ send(content_type)
55
+ end
56
+
57
+ private
58
+
59
+ # SODA headers that are always encoded as JSON
60
+ JSON_HEADERS = %w(x-soda2-fields x-soda2-types)
61
+
62
+ attr_reader :http_response
63
+
64
+ def content_type
65
+ type = headers['content-type']
66
+
67
+ CONTENT_TYPES.each do |key, value|
68
+ return key if type && type.start_with?(value)
69
+ end
70
+
71
+ raise ResponseError.new("Unexpected content type: #{type}")
72
+ end
73
+
74
+ def csv
75
+ CSV.parse(http_response.body)[1..-1] # exclude header
76
+ end
77
+
78
+ def json
79
+ JSON.parse(http_response.body)
80
+ end
81
+
82
+ class ResponseError < StandardError; end
83
+ end
84
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: utf-8
2
+ #
3
+ module Mocrata
4
+ VERSION = "0.0.1"
5
+ end
data/lib/mocrata.rb ADDED
@@ -0,0 +1,56 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Mocrata is a [SODA](http://dev.socrata.com/) (Socrata Open Data API) client
4
+ # developed by [Mode Analytics](https://modeanalytics.com).
5
+ #
6
+ module Mocrata
7
+ # Supported Socrata content types
8
+ #
9
+ # @see http://dev.socrata.com/docs/formats/ Socrata format documentation
10
+ #
11
+ # @todo Add support for application/rdf+xml
12
+ #
13
+ CONTENT_TYPES = {
14
+ :json => 'application/json',
15
+ :csv => 'text/csv'
16
+ }
17
+
18
+ class << self
19
+ # Set Mocrata configuration values
20
+ #
21
+ # @yield [Mocrata::Configuration] the configuration instance
22
+ #
23
+ # @example
24
+ # Mocrata.configure do |config|
25
+ # config.app_token = 'yourtoken'
26
+ # config.per_page = 1000
27
+ # end
28
+ #
29
+ def configure(&block)
30
+ yield config
31
+ end
32
+
33
+ # The Mocrata configuration instance
34
+ #
35
+ # @return [Mocrata::Configuration] the configuration instance
36
+ #
37
+ def config
38
+ @config ||= Mocrata::Configuration.new
39
+ end
40
+
41
+ # Remove Mocrata configuration instance variable
42
+ #
43
+ def reset
44
+ if instance_variable_defined?(:@config)
45
+ remove_instance_variable(:@config)
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ require 'mocrata/configuration'
52
+ require 'mocrata/dataset'
53
+ require 'mocrata/dataset_url'
54
+ require 'mocrata/request'
55
+ require 'mocrata/response'
56
+ require 'mocrata/version'
data/mocrata.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mocrata/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mocrata"
8
+ spec.version = Mocrata::VERSION
9
+ spec.authors = ["Heather Rivers"]
10
+ spec.email = ["heather@modeanalytics.com"]
11
+ spec.description = %q{Mode's SODA client}
12
+ spec.summary = %q{Mocrata is a SODA (Socrata Open Data API) client developed by Mode Analytics}
13
+ spec.homepage = "https://github.com/mode/mocrata"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
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.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec"
24
+ spec.add_development_dependency "simplecov"
25
+ spec.add_development_dependency "rdoc"
26
+
27
+ spec.required_ruby_version = "~> 2.0"
28
+ end
@@ -0,0 +1,28 @@
1
+ # encoding: utf-8
2
+ #
3
+ require 'spec_helper'
4
+
5
+ describe Mocrata::Configuration do
6
+ let :config do
7
+ Mocrata::Configuration.new
8
+ end
9
+
10
+ describe '#per_page' do
11
+ it 'has default value' do
12
+ expect(config.per_page).to eq(1000)
13
+ end
14
+ end
15
+
16
+ describe '#per_page=' do
17
+ it 'overrides default value' do
18
+ expect(config.per_page).to eq(1000)
19
+ config.per_page = 50
20
+ expect(config.per_page).to eq(50)
21
+ end
22
+
23
+ it 'raises exception if max value is exceeded' do
24
+ expect { config.per_page = 1001 }.to raise_error(
25
+ Mocrata::Configuration::ConfigurationError)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,134 @@
1
+ # encoding: utf-8
2
+ #
3
+ require 'spec_helper'
4
+
5
+ describe Mocrata::Dataset do
6
+ let :dataset do
7
+ Mocrata::Dataset.new('')
8
+ end
9
+
10
+ let :rows do
11
+ (0..5).map do |i|
12
+ { "key_#{i}" => "value_#{i}" }
13
+ end
14
+ end
15
+
16
+ let :pages do
17
+ [rows[0..3], rows[4..5]]
18
+ end
19
+
20
+ describe '#each_row' do
21
+ it 'yields rows' do
22
+ expect(dataset).to receive(:each_page)
23
+ .and_yield(pages[0])
24
+ .and_yield(pages[1])
25
+
26
+ expect { |b|
27
+ dataset.each_row(:json, &b)
28
+ }.to yield_successive_args(*rows)
29
+ end
30
+ end
31
+
32
+ describe '#each_page' do
33
+ it 'yields pages' do
34
+ expect(dataset).to receive(:json).and_return(*pages)
35
+
36
+ expect { |b|
37
+ dataset.each_page(:json, 4, &b)
38
+ }.to yield_successive_args(*pages)
39
+ end
40
+ end
41
+
42
+ describe '#get' do
43
+ it 'returns response' do
44
+ dataset = Mocrata::Dataset.new(
45
+ 'https://data.sfgov.org/resource/funx-qxxn')
46
+
47
+ response = Mocrata::Response.new(true)
48
+
49
+ expect_any_instance_of(Mocrata::Request).to receive(
50
+ :response).and_return(response)
51
+
52
+ expect(dataset.send(:get, :csv)).to eq(response)
53
+ end
54
+ end
55
+
56
+ describe '#csv' do
57
+ it 'returns csv body' do
58
+ response = Mocrata::Response.new(true)
59
+
60
+ expect(response).to receive(:body).and_return([])
61
+ expect(dataset).to receive(:get).and_return(response)
62
+
63
+ expect(dataset.csv).to eq([])
64
+ end
65
+ end
66
+
67
+ describe '#json' do
68
+ it 'returns json body' do
69
+ response = Mocrata::Response.new(true)
70
+
71
+ expect(response).to receive(:body).and_return([])
72
+ expect(dataset).to receive(:get).and_return(response)
73
+
74
+ expect(dataset.json).to eq([])
75
+ end
76
+ end
77
+
78
+ describe '#headers' do
79
+ it 'builds headers' do
80
+ response = Mocrata::Response.new(true)
81
+
82
+ expect(response).to receive(:headers).and_return('foo' => 'bar')
83
+ expect(dataset).to receive(:get).and_return(response)
84
+
85
+ expect(dataset.headers).to eq('foo' => 'bar')
86
+ end
87
+ end
88
+
89
+ describe '#fields' do
90
+ it 'builds empty map' do
91
+ expect(dataset).to receive(:field_names).and_return([])
92
+ expect(dataset).to receive(:field_types).and_return([])
93
+
94
+ expect(dataset.fields).to eq({})
95
+ end
96
+
97
+ it 'builds map' do
98
+ expect(dataset).to receive(:field_names).and_return(['key1', 'key2'])
99
+ expect(dataset).to receive(:field_types).and_return(['val1', 'val2'])
100
+
101
+ expect(dataset.fields).to eq('key1' => 'val1', 'key2' => 'val2')
102
+ end
103
+ end
104
+
105
+ describe '#field_names' do
106
+ it 'handles missing header' do
107
+ expect(dataset).to receive(:headers).and_return({})
108
+
109
+ expect(dataset.send(:field_names)).to eq([])
110
+ end
111
+
112
+ it 'returns header if present' do
113
+ expect(dataset).to receive(:headers).and_return(
114
+ 'x-soda2-fields' => ['name1', 'name2'])
115
+
116
+ expect(dataset.send(:field_names)).to eq(['name1', 'name2'])
117
+ end
118
+ end
119
+
120
+ describe '#field_types' do
121
+ it 'handles missing header' do
122
+ expect(dataset).to receive(:headers).and_return({})
123
+
124
+ expect(dataset.send(:field_types)).to eq([])
125
+ end
126
+
127
+ it 'returns header if present' do
128
+ expect(dataset).to receive(:headers).and_return(
129
+ 'x-soda2-types' => ['type1', 'type2'])
130
+
131
+ expect(dataset.send(:field_types)).to eq(['type1', 'type2'])
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,77 @@
1
+ # encoding: utf-8
2
+ #
3
+ require 'spec_helper'
4
+
5
+ describe Mocrata::DatasetUrl do
6
+ describe '#validate!' do
7
+ it 'returns true for valid url' do
8
+ url = Mocrata::DatasetUrl.new(
9
+ 'data.sfgov.org/resource/funx-qxxn.csv?limit=100#foo')
10
+
11
+ expect(url.validate!).to eq(true)
12
+ end
13
+
14
+ it 'raises exception for invalid url' do
15
+ url = Mocrata::DatasetUrl.new('data.sfgov.org/nope/funx-qxxn.csv')
16
+
17
+ expect {
18
+ url.validate!
19
+ }.to raise_error(Mocrata::DatasetUrl::InvalidError)
20
+ end
21
+ end
22
+
23
+ describe '#normalize' do
24
+ it 'normalizes original url' do
25
+ url = Mocrata::DatasetUrl.new(
26
+ 'data.sfgov.org/resource/funx-qxxn.csv?limit=100#foo')
27
+
28
+ expect(url.normalize).to eq('https://data.sfgov.org/resource/funx-qxxn')
29
+ end
30
+ end
31
+
32
+ describe '.ensure_protocol' do
33
+ it 'adds missing protocol' do
34
+ url = Mocrata::DatasetUrl.ensure_protocol('data.sfgov.org/')
35
+
36
+ expect(url).to eq('https://data.sfgov.org/')
37
+ end
38
+
39
+ it 'adds protocol to schemeless url' do
40
+ url = Mocrata::DatasetUrl.ensure_protocol('//data.sfgov.org/')
41
+
42
+ expect(url).to eq('https://data.sfgov.org/')
43
+ end
44
+
45
+ it 'preserves http protocol' do
46
+ url = Mocrata::DatasetUrl.ensure_protocol('http://data.sfgov.org/')
47
+
48
+ expect(url).to eq('http://data.sfgov.org/')
49
+ end
50
+
51
+ it 'preserves https protocol' do
52
+ url = Mocrata::DatasetUrl.ensure_protocol('https://data.sfgov.org/')
53
+
54
+ expect(url).to eq('https://data.sfgov.org/')
55
+ end
56
+ end
57
+
58
+ describe '.strip_format' do
59
+ it 'preserves url without format' do
60
+ url = Mocrata::DatasetUrl.strip_format('data.sfgov.org/resource/foo')
61
+
62
+ expect(url).to eq('data.sfgov.org/resource/foo')
63
+ end
64
+
65
+ it 'strips format at end of url' do
66
+ url = Mocrata::DatasetUrl.strip_format('data.sfgov.org/resource/foo.bar')
67
+
68
+ expect(url).to eq('data.sfgov.org/resource/foo')
69
+ end
70
+
71
+ it 'preserves format if not at end of url' do
72
+ url = Mocrata::DatasetUrl.strip_format('data.sfgov.org/resource/foo.bar/baz')
73
+
74
+ expect(url).to eq('data.sfgov.org/resource/foo.bar/baz')
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,125 @@
1
+ # encoding: utf-8
2
+ #
3
+ require 'spec_helper'
4
+
5
+ describe Mocrata::Request do
6
+ describe '#response' do
7
+ it 'forms response' do
8
+ request = Mocrata::Request.new(
9
+ 'https://data.sfgov.org/resource/funx-qxxn', :json)
10
+
11
+ expect_any_instance_of(Mocrata::Response).to receive(
12
+ :validate!).and_return(true)
13
+
14
+ expect(request.send(:http)).to receive(:request).and_return(true)
15
+
16
+ expect(request.response).to be_an_instance_of(Mocrata::Response)
17
+ end
18
+ end
19
+
20
+ describe '#content_type' do
21
+ it 'raises exception with null format' do
22
+ request = Mocrata::Request.new(
23
+ 'https://data.sfgov.org/resource/funx-qxxn', nil)
24
+
25
+ expect {
26
+ request.content_type
27
+ }.to raise_error(Mocrata::Request::RequestError)
28
+ end
29
+
30
+ it 'raises exception with invalid format' do
31
+ request = Mocrata::Request.new(
32
+ 'https://data.sfgov.org/resource/funx-qxxn', :nope)
33
+
34
+ expect {
35
+ request.content_type
36
+ }.to raise_error(Mocrata::Request::RequestError)
37
+ end
38
+
39
+ it 'returns valid content type' do
40
+ request = Mocrata::Request.new(
41
+ 'https://data.sfgov.org/resource/funx-qxxn', :csv)
42
+
43
+ expect(request.content_type).to eq('text/csv')
44
+ end
45
+ end
46
+
47
+ describe '#http' do
48
+ it 'uses ssl' do
49
+ request = Mocrata::Request.new(
50
+ 'https://data.sfgov.org/resource/funx-qxxn', nil)
51
+
52
+ expect(request.send(:http).use_ssl?).to be true
53
+ end
54
+ end
55
+
56
+ describe '#soda_params' do
57
+ it 'is formed with default params' do
58
+ request = Mocrata::Request.new('', nil)
59
+
60
+ expect(request.send(:soda_params)).to eq(:$limit => 1000, :$offset => 0)
61
+ end
62
+
63
+ it 'is formed with custom params' do
64
+ request = Mocrata::Request.new('', nil, :page => 5, :per_page => 100)
65
+
66
+ expect(request.send(:soda_params)).to eq(:$limit => 100, :$offset => 400)
67
+ end
68
+
69
+ it 'ignores custom params' do
70
+ request = Mocrata::Request.new('', nil, :wat => 'nope')
71
+
72
+ expect(request.send(:soda_params)).to eq(:$limit => 1000, :$offset => 0)
73
+ end
74
+ end
75
+
76
+ describe '#uri' do
77
+ before :each do
78
+ expect(Mocrata.config).to receive(:per_page).and_return(10)
79
+ end
80
+
81
+ it 'is formed with default parameters' do
82
+ request = Mocrata::Request.new(
83
+ 'https://data.sfgov.org/resource/funx-qxxn', nil)
84
+
85
+ result = request.send(:uri).to_s
86
+
87
+ expect(result).to eq(
88
+ 'https://data.sfgov.org/resource/funx-qxxn?$limit=10&$offset=0')
89
+ end
90
+
91
+ it 'is formed with custom parameters' do
92
+ request = Mocrata::Request.new(
93
+ 'https://data.sfgov.org/resource/funx-qxxn', nil,
94
+ :per_page => 5,
95
+ :page => 3)
96
+
97
+ result = request.send(:uri).to_s
98
+
99
+ expect(result).to eq(
100
+ 'https://data.sfgov.org/resource/funx-qxxn?$limit=5&$offset=10')
101
+ end
102
+ end
103
+
104
+ describe '.query_string' do
105
+ it 'forms empty query string' do
106
+ expect(Mocrata::Request.query_string({})).to eq('')
107
+ end
108
+
109
+ it 'forms simple query string' do
110
+ result = Mocrata::Request.query_string(:foo => 'bar')
111
+ expect(result).to eq('foo=bar')
112
+ end
113
+
114
+ it 'forms complex string' do
115
+ result = Mocrata::Request.query_string(:foo => 'bar', :bar => 'baz')
116
+ expect(result).to eq('foo=bar&bar=baz')
117
+ end
118
+
119
+ it 'escapes values' do
120
+ result = Mocrata::Request.query_string(:foo => '"\'Stop!\' said Fred"')
121
+
122
+ expect(result).to eq('foo=%22%27Stop%21%27+said+Fred%22')
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,132 @@
1
+ # encoding: utf-8
2
+ #
3
+ require 'spec_helper'
4
+
5
+ describe Mocrata::Response do
6
+ let :response do
7
+ Mocrata::Response.new(true)
8
+ end
9
+
10
+ describe '#validate!' do
11
+ it 'returns true without content type' do
12
+ expect(response).to receive(:content_type).and_return(nil)
13
+ expect(response.validate!).to be true
14
+ end
15
+
16
+ it 'returns true with csv content' do
17
+ expect(response).to receive(:content_type).and_return(:csv)
18
+ expect(response.validate!).to be true
19
+ end
20
+
21
+ it 'returns true with json array' do
22
+ expect(response).to receive(:content_type).and_return(:json)
23
+ expect(response).to receive(:body).and_return([])
24
+ expect(response.validate!).to be true
25
+ end
26
+
27
+ it 'returns true with json array' do
28
+ expect(response).to receive(:content_type).and_return(:json)
29
+ expect(response).to receive(:body).at_least(:once).and_return({})
30
+ expect(response.validate!).to be true
31
+ end
32
+
33
+ it 'raises exception with json error' do
34
+ expect(response).to receive(:content_type).and_return(:json)
35
+ expect(response).to receive(:body).at_least(:once).and_return(
36
+ 'error' => true,
37
+ 'message' => 'something went wrong')
38
+
39
+ expect { response.validate! }.to raise_error(
40
+ Mocrata::Response::ResponseError)
41
+ end
42
+ end
43
+
44
+ describe '#content_type' do
45
+ it 'detects csv' do
46
+ expect(response).to receive(:headers).and_return(
47
+ 'content-type' => 'text/csv')
48
+
49
+ expect(response.send(:content_type)).to eq(:csv)
50
+ end
51
+
52
+ it 'detects csv with junk at the end' do
53
+ expect(response).to receive(:headers).and_return(
54
+ 'content-type' => 'text/csv; charset=utf-8')
55
+
56
+ expect(response.send(:content_type)).to eq(:csv)
57
+ end
58
+
59
+ it 'detects json' do
60
+ expect(response).to receive(:headers).and_return(
61
+ 'content-type' => 'application/json')
62
+
63
+ expect(response.send(:content_type)).to eq(:json)
64
+ end
65
+
66
+ it 'raises exception for unrecognized content type' do
67
+ expect(response).to receive(:headers).and_return(
68
+ 'content-type' => 'text/html')
69
+
70
+ expect { response.send(:content_type) }.to raise_error(
71
+ Mocrata::Response::ResponseError)
72
+ end
73
+
74
+ it 'raises exception for absent content type' do
75
+ expect(response).to receive(:headers).and_return({})
76
+ expect { response.send(:content_type) }.to raise_error(
77
+ Mocrata::Response::ResponseError)
78
+ end
79
+ end
80
+
81
+ describe '#csv' do
82
+ it 'parses body and excludes header' do
83
+ csv = "\"header1\"\n\"row1\"\n\"row2\""
84
+ expect(response.send(:http_response)).to receive(:body).and_return(csv)
85
+ expect(response.send(:csv)).to eq([['row1'], ['row2']])
86
+ end
87
+ end
88
+
89
+ describe '#json' do
90
+ it 'parses body' do
91
+ json = '[{"key1":"val1"}]'
92
+ expect(response.send(:http_response)).to receive(:body).and_return(json)
93
+ expect(response.send(:json)).to eq([{'key1' => 'val1'}])
94
+ end
95
+ end
96
+
97
+ describe '#body' do
98
+ it 'returns json' do
99
+ expect(response).to receive(:content_type).and_return(:json)
100
+ expect(response).to receive(:json).and_return([])
101
+ expect(response.body).to eq([])
102
+ end
103
+
104
+ it 'returns csv' do
105
+ expect(response).to receive(:content_type).and_return(:csv)
106
+ expect(response).to receive(:csv).and_return([])
107
+ expect(response.body).to eq([])
108
+ end
109
+ end
110
+
111
+ describe '#headers' do
112
+ it 'preserves non json headers' do
113
+ expect(response.send(:http_response)).to receive(:each_header)
114
+ .and_yield('x-foo-header', 'foo')
115
+ .and_yield('x-bar-header', 'bar')
116
+
117
+ expect(response.headers).to eq(
118
+ 'x-foo-header' => 'foo',
119
+ 'x-bar-header' => 'bar')
120
+ end
121
+
122
+ it 'parses json headers' do
123
+ expect(response.send(:http_response)).to receive(:each_header)
124
+ .and_yield('x-foo-header', 'foo')
125
+ .and_yield('x-soda2-fields', '{"name":"value"}')
126
+
127
+ expect(response.headers).to eq(
128
+ 'x-foo-header' => 'foo',
129
+ 'x-soda2-fields' => { 'name' => 'value' })
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+ #
3
+ require 'spec_helper'
4
+
5
+ describe Mocrata do
6
+ after :each do
7
+ Mocrata.reset
8
+ end
9
+
10
+ describe '.configure' do
11
+ it 'sets configuration variables' do
12
+ expect_any_instance_of(Mocrata::Configuration).to receive(:setting=).once
13
+
14
+ Mocrata.configure do |config|
15
+ config.setting = 'value'
16
+ end
17
+ end
18
+ end
19
+
20
+ describe '.config' do
21
+ it 'instantiates and memoizes configuration instance' do
22
+ expect(Mocrata.instance_variable_get(:@config)).to be_nil
23
+
24
+ expect(Mocrata.config).to be_an_instance_of(Mocrata::Configuration)
25
+
26
+ config = Mocrata.instance_variable_get(:@config)
27
+
28
+ expect(config).to be_an_instance_of(Mocrata::Configuration)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,12 @@
1
+ # encoding: utf-8
2
+ #
3
+ require 'simplecov'
4
+ SimpleCov.start
5
+
6
+ require 'bundler/setup'
7
+ Bundler.setup
8
+
9
+ require 'mocrata'
10
+
11
+ RSpec.configure do |config|
12
+ end
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mocrata
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Heather Rivers
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-07-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: simplecov
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rdoc
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Mode's SODA client
84
+ email:
85
+ - heather@modeanalytics.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - .gitignore
91
+ - .yardopts
92
+ - Gemfile
93
+ - LICENSE.txt
94
+ - README.md
95
+ - Rakefile
96
+ - lib/mocrata.rb
97
+ - lib/mocrata/configuration.rb
98
+ - lib/mocrata/dataset.rb
99
+ - lib/mocrata/dataset_url.rb
100
+ - lib/mocrata/request.rb
101
+ - lib/mocrata/response.rb
102
+ - lib/mocrata/version.rb
103
+ - mocrata.gemspec
104
+ - spec/lib/mocrata/configuration_spec.rb
105
+ - spec/lib/mocrata/dataset_spec.rb
106
+ - spec/lib/mocrata/dataset_url_spec.rb
107
+ - spec/lib/mocrata/request_spec.rb
108
+ - spec/lib/mocrata/response_spec.rb
109
+ - spec/lib/mocrata_spec.rb
110
+ - spec/spec_helper.rb
111
+ homepage: https://github.com/mode/mocrata
112
+ licenses:
113
+ - MIT
114
+ metadata: {}
115
+ post_install_message:
116
+ rdoc_options: []
117
+ require_paths:
118
+ - lib
119
+ required_ruby_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ~>
122
+ - !ruby/object:Gem::Version
123
+ version: '2.0'
124
+ required_rubygems_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - '>='
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ requirements: []
130
+ rubyforge_project:
131
+ rubygems_version: 2.0.0
132
+ signing_key:
133
+ specification_version: 4
134
+ summary: Mocrata is a SODA (Socrata Open Data API) client developed by Mode Analytics
135
+ test_files:
136
+ - spec/lib/mocrata/configuration_spec.rb
137
+ - spec/lib/mocrata/dataset_spec.rb
138
+ - spec/lib/mocrata/dataset_url_spec.rb
139
+ - spec/lib/mocrata/request_spec.rb
140
+ - spec/lib/mocrata/response_spec.rb
141
+ - spec/lib/mocrata_spec.rb
142
+ - spec/spec_helper.rb
143
+ has_rdoc: