mocrata 0.0.1

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