fieldview 0.0.0

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: c291877633d7f4126668164ee7cc60c7c5c67bac
4
+ data.tar.gz: df16497405345a46fab2c65066eec659ffc074b7
5
+ SHA512:
6
+ metadata.gz: 883b9f63fcaf3af60958bc8407660a76608afca697fa043afae906240d6c7f2f36ed3116dea2de6c76630c3e4ba82effe08d6d11ab3e349c4cb03b962eae052d
7
+ data.tar.gz: 8d027e67cdabf34fd66a7284196c34533d82b2f002df8826b2d91b8005ae4246fb8632225f5b67041dd518c465a4826244f5e5eabc9e7a2d6927acb91d63808d
data/.gitignore ADDED
@@ -0,0 +1,52 @@
1
+ *agribotix*
2
+ *.gem
3
+ *.rbc
4
+ *.byebug*
5
+ /.config
6
+ /coverage/
7
+ /InstalledFiles
8
+ /pkg/
9
+ /spec/reports/
10
+ /spec/examples.txt
11
+ /test/tmp/
12
+ /test/version_tmp/
13
+ /tmp/
14
+
15
+ # Used by dotenv library to load environment variables.
16
+ # .env
17
+
18
+ ## Specific to RubyMotion:
19
+ .dat*
20
+ .repl_history
21
+ build/
22
+ *.bridgesupport
23
+ build-iPhoneOS/
24
+ build-iPhoneSimulator/
25
+
26
+ ## Specific to RubyMotion (use of CocoaPods):
27
+ #
28
+ # We recommend against adding the Pods directory to your .gitignore. However
29
+ # you should judge for yourself, the pros and cons are mentioned at:
30
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
31
+ #
32
+ # vendor/Pods/
33
+
34
+ ## Documentation cache and generated files:
35
+ /.yardoc/
36
+ /_yardoc/
37
+ /doc/
38
+ /rdoc/
39
+
40
+ ## Environment normalization:
41
+ /.bundle/
42
+ /vendor/bundle
43
+ /lib/bundler/man/
44
+
45
+ # for a library or gem, you might want to ignore these files since the code is
46
+ # intended to run in multiple environments; otherwise, check them in:
47
+ # Gemfile.lock
48
+ # .ruby-version
49
+ # .ruby-gemset
50
+
51
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
52
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ group :development do
6
+ gem 'minitest'
7
+ gem 'webmock'
8
+ gem 'rake'
9
+ platforms :mri do
10
+ # to avoid problems, bring Byebug in on just versions of Ruby under which
11
+ # it's known to work well
12
+ if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.0.0')
13
+ gem 'byebug'
14
+ end
15
+ end
16
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,39 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ fieldview (0.0.0)
5
+ faraday (~> 0.9)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ addressable (2.5.1)
11
+ public_suffix (~> 2.0, >= 2.0.2)
12
+ byebug (9.0.6)
13
+ crack (0.4.3)
14
+ safe_yaml (~> 1.0.0)
15
+ faraday (0.12.0.1)
16
+ multipart-post (>= 1.2, < 3)
17
+ hashdiff (0.3.2)
18
+ minitest (5.8.4)
19
+ multipart-post (2.0.0)
20
+ public_suffix (2.0.5)
21
+ rake (11.3.0)
22
+ safe_yaml (1.0.4)
23
+ webmock (3.0.1)
24
+ addressable (>= 2.3.6)
25
+ crack (>= 0.3.2)
26
+ hashdiff
27
+
28
+ PLATFORMS
29
+ ruby
30
+
31
+ DEPENDENCIES
32
+ byebug
33
+ fieldview!
34
+ minitest
35
+ rake
36
+ webmock
37
+
38
+ BUNDLED WITH
39
+ 1.13.2
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Paul Susmarski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # FieldView API Library
2
+
3
+ The FieldView Ruby library provides convenient access to the FieldView API from applications written in the Ruby language. It includes a pre-defined set of classes for API resources that are available currently from the API. You will need to get access from a Climate Corporation representative and the interface utilize OAUTH 2.0.
4
+
5
+ ## Usage
6
+
7
+ The library needs to be configured with your FieldView account's client secret,
8
+ client id, and x-api-key which would be provided to you by a Climate
9
+ representative. Set these according to the their values
10
+
11
+ ``` ruby
12
+ require "fieldview"
13
+
14
+ FieldView.client_id = "fake-client"
15
+ FieldView.client_secret = "fake-client-..."
16
+ FieldView.x_api_key = "..."
17
+
18
+ # Required if acquiring a new auth token without refresh tokens
19
+ FieldView.redirect_uri = "http://my.local.host/path/that/users/are/directed"
20
+
21
+
22
+ # Get an Auth token
23
+ auth_token = FieldView::AuthToken.new_auth_token_with_code_from_redirect_code(<CODE>)
24
+
25
+ # Or initialize with previous information
26
+ auth_token = FieldView::AuthToken.new(
27
+ access_token: <ATOKEN>,
28
+ expiration_at: <DATETIME>,
29
+ refresh_token: <RTOKEN>,
30
+ refresh_token_expiration_at: <DATETIME>)
31
+
32
+ # Or with just auth_token (assuming it hasn't expired)
33
+ auth_token = FieldView::AuthToken.new(access_token: <ATOKEN>)
34
+
35
+ # refresh token and a new access/refresh token will be associated with the object
36
+ auth_token = FieldView::AuthToken.new(refresh_token: <RTOKEN>)
37
+
38
+
39
+ ```
40
+
41
+ ## Development
42
+
43
+ Run all tests:
44
+
45
+ bundle exec rake
46
+
47
+ Run a single test suite:
48
+
49
+ bundle exec ruby -Ilib/ test/field_view_test.rb
50
+
51
+ Run a single test:
52
+
53
+ bundle exec ruby -Ilib/ test/field_view_test.rb -n /client.id/
54
+
55
+ ## Disclaimer
56
+
57
+ This Gem is in no way associated with The Climate Corporation, and they are in no way associated with it's support, maintenance, or updates.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.pattern = './test/**/test_*.rb'
5
+ end
6
+
7
+ desc "Run tests"
8
+ task :default => :test
data/fieldview.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), 'lib'))
2
+
3
+ spec = Gem::Specification.new do |s|
4
+ s.name = 'fieldview'
5
+ s.version = "0.0.0"
6
+ s.required_ruby_version = '>= 1.9.3'
7
+ s.summary = 'Ruby bindings for the FieldView API'
8
+ s.description = ' FieldView is used make data-driven decisions to maximize your return on every acre.'
9
+ s.author = 'Paul Susmarski'
10
+ s.email = 'paul@susmarski.com'
11
+ s.homepage = 'http://rubygems.org/gems/fielview'
12
+ s.license = 'MIT'
13
+
14
+ s.add_dependency('faraday', '~> 0.9')
15
+
16
+ s.files = Dir['lib/**/*.rb']
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- test/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ['lib']
21
+ end
@@ -0,0 +1,111 @@
1
+ module FieldView
2
+ class AuthToken
3
+ attr_accessor :access_token, :access_token_expiration_at, :refresh_token, :refresh_token_expiration_at
4
+ attr_accessor :last_request
5
+ def initialize(params)
6
+ # Assume now was 5 seconds ago
7
+ now = FieldView.get_now_for_auth_token - 5
8
+
9
+ # Used on every request
10
+ self.access_token = params[:access_token]
11
+
12
+ # When the access token expires we'll need to refresh,
13
+ # can be specified as part of the object, 14399 is what is being
14
+ # returned at the time of writing this (2017-04-11)
15
+ self.access_token_expiration_at = params[:access_token_expiration_at] || (now + (params[:expires_in]||14399) - 10)
16
+
17
+ # Refresh token isn't required, but can be initialized with this
18
+ self.refresh_token = params[:refresh_token]
19
+
20
+ # Refresh token technically expires in 30 days, but
21
+ # we'll subtract one day for safety
22
+ self.refresh_token_expiration_at = params[:access_token_expiration_at] || (now + (30-1)*24*60*60)
23
+ end
24
+
25
+ def access_token_expired?
26
+ return !!(self.access_token.nil? || self.access_token_expiration_at <= Time.now)
27
+ end
28
+
29
+ def refresh_token_expired?
30
+ return !!(self.refresh_token.nil? || self.refresh_token_expiration_at <= Time.now)
31
+ end
32
+
33
+ def refresh_access_token!()
34
+ http, request = self.class.build_token_request([
35
+ ["grant_type", "refresh_token"],
36
+ ["refresh_token", self.refresh_token]
37
+ ])
38
+ response = http.request(request)
39
+ if response.code != "200" then
40
+ # An error has occurred, log to roll bar and notify them
41
+ raise RefreshTokenError.new("Failed to refresh FieldView token, response body: #{response.body}")
42
+ else
43
+ json = JSON.parse(response.body, symbolize_names: true)
44
+ initialize(json)
45
+ end
46
+ end
47
+
48
+ def execute_request!(method, path, headers: {}, params: {})
49
+ self.last_request = path
50
+ if access_token_expired? && refresh_token_expired? then
51
+ raise AllTokensExpiredError.new("All of your tokens have expired. " \
52
+ "You'll need to re-log into FieldView.")
53
+ end
54
+
55
+ if access_token_expired? then
56
+ refresh_access_token!()
57
+ end
58
+ uri = URI.parse("#{FieldView.api_base}#{FieldView.api_version}/#{path}")
59
+
60
+ # TODO: Handle parameters
61
+ http = Net::HTTP.new(uri.host, uri.port)
62
+ http.use_ssl = FieldView.api_base.start_with?("https")
63
+ request = Net::HTTP.class_eval(method.to_s.capitalize).new(uri.request_uri)
64
+ request["Accept"] = "*/*"
65
+ request["Authorization"] = "Bearer #{self.access_token}"
66
+ request["X-Api-Key"] = FieldView.x_api_key
67
+ request["Content-Type"] = "application/json"
68
+
69
+ headers.each do |header,value|
70
+ request[header] = value.to_s
71
+ end
72
+
73
+ response = http.request(request)
74
+ FieldView.handle_response_error_codes(response)
75
+ return FieldViewResponse.new(response)
76
+ end
77
+
78
+ def self.build_token_request(url_params)
79
+ uri = URI.parse(FieldView.oauth_token_base)
80
+ new_query_ar = URI.decode_www_form(uri.query || '')
81
+ url_params.each do |param|
82
+ new_query_ar << param
83
+ end
84
+ uri.query = URI.encode_www_form(new_query_ar)
85
+ http = Net::HTTP.new(uri.host, uri.port)
86
+ http.use_ssl = true
87
+ request = Net::HTTP::Post.new(uri.request_uri)
88
+ request["Accept"] = "*/*"
89
+ request["Authorization"] = "Basic #{Base64.encode64("#{FieldView.client_id}:#{FieldView.client_secret}").strip}"
90
+ request["Content-Type"] = "application/x-www-form-urlencoded"
91
+ return http, request
92
+ end
93
+
94
+ # Code will be collected from the redirect to your server
95
+ def self.new_auth_token_with_code_from_redirect_code(code)
96
+ http, request = build_token_request([
97
+ ["grant_type", "authorization_code"],
98
+ ["redirect_uri", FieldView.redirect_uri],
99
+ ["code", code]
100
+ ])
101
+ response = http.request(request)
102
+ if response.code != "200" then
103
+ raise AuthenticationError.new("Was unable to get a new auth token using the code provided. " \
104
+ "See response body: #{response.body}")
105
+ else
106
+ json = JSON.parse(response.body, symbolize_names: true)
107
+ return AuthToken.new(json)
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,28 @@
1
+ module FieldView
2
+ class Feature
3
+ attr_accessor :type, :coordinates
4
+ def initialize(json_feature_object)
5
+ # Use RGEO??
6
+ self.type = json_feature_object[:type]
7
+ self.coordinates = json_feature_object[:coordinates]
8
+ end
9
+
10
+ def point?()
11
+ return !!(self.type =~ /\Apoint\z/i)
12
+ end
13
+
14
+ def multi_polygon?()
15
+ return !!(self.type =~ /\Amultipolygon\z/i)
16
+ end
17
+ end
18
+ class Boundary
19
+ attr_accessor :id, :units, :area, :centroid, :geometry
20
+ def initialize(json_object)
21
+ self.id = json_object[:id]
22
+ self.area = json_object[:area][:q]
23
+ self.units = json_object[:area][:u]
24
+ self.centroid = Feature.new(json_object[:centroid])
25
+ self.geometry = Feature.new(json_object[:geometry])
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,64 @@
1
+ module FieldView
2
+ # FieldViewError is the base error from which all other more specific FieldView
3
+ # errors derive.
4
+ class FieldViewError < StandardError
5
+ attr_reader :message
6
+
7
+ # These fields are now available as part of #response and that usage should
8
+ # be preferred.
9
+ attr_reader :http_body
10
+ attr_reader :http_headers
11
+ attr_reader :http_status
12
+ attr_reader :request_id
13
+
14
+ attr_accessor :response
15
+ # Initializes a FieldViewError.
16
+ def initialize(message=nil, http_status: nil, http_body: nil,
17
+ http_headers: nil)
18
+ @message = message
19
+ @http_status = http_status
20
+ @http_body = http_body
21
+ @http_headers = http_headers || {}
22
+ @request_id = @http_headers[FieldView::REQUEST_ID_HEADER_KEY]
23
+ end
24
+
25
+ def to_s
26
+ status_string = @http_status.nil? ? "" : "(Status #{@http_status}) "
27
+ id_string = @request_id.nil? ? "" : "(Request #{@request_id}) "
28
+ "#{status_string}#{id_string}#{@message}"
29
+ end
30
+ end
31
+
32
+ # AuthenticationError is raised when invalid credentials are used to connect
33
+ # to FieldView's servers.
34
+ class AuthenticationError < FieldViewError
35
+ end
36
+
37
+ # Raised when all the tokens expired and nothing can be done.
38
+ class AllTokensExpiredError < FieldViewError
39
+ end
40
+
41
+ # Raised when access resources you don't have access to
42
+ class PermissionError < FieldViewError
43
+ end
44
+
45
+ # Raised when a refresh token is attempted to be used but it can't be
46
+ class RefreshTokenError < FieldViewError
47
+ end
48
+
49
+ # Raised when too many requests are being made
50
+ class RateLimitError < FieldViewError
51
+ end
52
+
53
+ # Raised when accessing a non-existent resource
54
+ class InvalidRequestError < FieldViewError
55
+ end
56
+
57
+ # Raised when something goes wrong with FieldView
58
+ class InternalServerError < FieldViewError
59
+ end
60
+
61
+ # Raised when the server is busy
62
+ class ServerBusyError < FieldViewError
63
+ end
64
+ end
@@ -0,0 +1,22 @@
1
+ module FieldView
2
+ class Field
3
+ attr_accessor :id
4
+ attr_accessor :name
5
+ attr_accessor :boundary_id
6
+ attr_accessor :auth_token
7
+ def initialize(json_object, auth_token = nil)
8
+ self.id = json_object[:id]
9
+ self.name = json_object[:name]
10
+ self.boundary_id = json_object[:boundaryId]
11
+ self.auth_token = auth_token
12
+ end
13
+
14
+ def boundary
15
+ @boundary ||= nil
16
+ if @boundary.nil?
17
+ @boundary = Boundary.new(self.auth_token.execute_request!(:get, "boundaries/#{self.boundary_id}").data)
18
+ end
19
+ return @boundary
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,34 @@
1
+ module FieldView
2
+ class Fields
3
+ PATH = "fields"
4
+ def self.list(auth_token, limit: nil, next_token: nil)
5
+ limit ||= FieldView.default_page_limit
6
+ response = auth_token.execute_request!(:get, PATH,
7
+ headers: {
8
+ FieldView::NEXT_TOKEN_HEADER_KEY => next_token,
9
+ FieldView::PAGE_LIMIT_HEADER_KEY => limit
10
+ })
11
+ next_token = response.http_headers[FieldView::NEXT_TOKEN_HEADER_KEY]
12
+
13
+ if (response.http_status == 200 || response.http_status == 206) then
14
+ # 206: Partial result, will have more data
15
+ # 200: When all the results were in the list
16
+ return_data = response.data[:results]
17
+ elsif (response.http_status == 304)
18
+ # 304: Nothing modified since last request
19
+ return_data = []
20
+ else
21
+ # This should never happen
22
+ return_data = nil
23
+ end
24
+
25
+ return ListObject.new(
26
+ self,
27
+ auth_token,
28
+ return_data.collect { |i| Field.new(i, auth_token) },
29
+ response.http_status,
30
+ next_token: next_token,
31
+ limit: limit)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,24 @@
1
+ module FieldView
2
+ class FieldViewResponse
3
+ attr_accessor :request_id
4
+ attr_accessor :http_body
5
+ attr_accessor :http_headers
6
+ attr_accessor :http_status
7
+ attr_accessor :data
8
+ def initialize(response)
9
+ self.http_headers = {}
10
+ response.each_capitalized_name do |n|
11
+ self.http_headers[n] = response[n]
12
+ end
13
+
14
+ self.http_body = response.body
15
+ begin
16
+ self.data = JSON.parse(response.body, symbolize_names: true)
17
+ rescue JSON::ParserError
18
+ self.data = nil
19
+ end
20
+ self.http_status = response.code.to_i
21
+ self.request_id = self.http_headers[FieldView::REQUEST_ID_HEADER_KEY]
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,43 @@
1
+ module FieldView
2
+ class ListObject
3
+ attr_accessor :limit
4
+ attr_reader :auth_token, :data, :last_http_status, :next_token, :listable
5
+ include Enumerable
6
+
7
+ def initialize(listable, auth_token, data, http_status, next_token: nil, limit: 100)
8
+ @listable = listable
9
+ @auth_token = auth_token
10
+ @data = data
11
+ @last_http_status = http_status
12
+ @next_token = next_token
13
+ @limit = 100
14
+ end
15
+
16
+ def each(&blk)
17
+ self.data.each(&blk)
18
+ end
19
+
20
+ def next_page!()
21
+ return if !self.more_pages?()
22
+ new_list = @listable.list(auth_token, limit: self.limit, next_token: self.next_token)
23
+ @data = new_list.data
24
+ @last_http_status = new_list.last_http_status
25
+ @auth_token = new_list.auth_token
26
+ end
27
+
28
+ # alias for more_pages
29
+ def has_more?()
30
+ return self.more_pages?()
31
+ end
32
+
33
+ def more_pages?()
34
+ return Util.http_status_is_more_in_list?(self.last_http_status)
35
+ end
36
+
37
+ def restart!()
38
+ @last_http_status = nil
39
+ @next_token = nil
40
+ next_page!()
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,7 @@
1
+ module FieldView
2
+ class Util
3
+ def self.http_status_is_more_in_list?(http_status)
4
+ return http_status.to_i == 206
5
+ end
6
+ end
7
+ end
data/lib/fieldview.rb ADDED
@@ -0,0 +1,130 @@
1
+ require 'net/http'
2
+ require 'json'
3
+ require 'rbconfig'
4
+ require 'base64'
5
+
6
+ # API Support Classes
7
+ require 'fieldview/errors'
8
+ require 'fieldview/auth_token'
9
+ require 'fieldview/fields'
10
+ require 'fieldview/util'
11
+ require 'fieldview/fieldview_response'
12
+ require 'fieldview/field'
13
+ require 'fieldview/list_object'
14
+ require 'fieldview/boundary'
15
+
16
+ module FieldView
17
+ @oauth_token_base = "https://api.climate.com/api/oauth/token"
18
+ @api_base = "https://platform.climate.com/v"
19
+ @max_network_retries = 0
20
+ @api_version = 4
21
+ @default_page_limit = 100
22
+ NEXT_TOKEN_HEADER_KEY = "X-Next-Token"
23
+ REQUEST_ID_HEADER_KEY = "X-Http-Request-Id"
24
+ PAGE_LIMIT_HEADER_KEY = "X-Limit"
25
+
26
+ class << self
27
+ attr_accessor :oauth_token_base, :api_base, :max_network_retries, :api_version, :now,
28
+ :default_page_limit
29
+
30
+ def get_now_for_auth_token
31
+ now || Time.now
32
+ end
33
+
34
+ def x_api_key
35
+ @_x_api_key ||= nil
36
+ unless @_x_api_key
37
+ raise AuthenticationError.new('No x-api-key provided. ' \
38
+ 'Set your x-api-key using "FieldView.x_api_key = <X-API-KEY>". ' \
39
+ 'This should have been provided to you by a Climate representative. ' \
40
+ 'This takes the form of "my-client"')
41
+ end
42
+ return @_x_api_key
43
+ end
44
+ def x_api_key=(value)
45
+ @_x_api_key = value
46
+ end
47
+
48
+ def redirect_uri
49
+ @_redirect_uri ||= nil
50
+ unless @_redirect_uri
51
+ raise AuthenticationError.new("You must set the redirect uri to your proper server " \
52
+ "to get new auth tokens if you haven't set one. " \
53
+ "Set your redirect uri using FieldView.redirect_uri = <REDIRECT_URI>")
54
+ end
55
+ return @_redirect_uri
56
+ end
57
+ def redirect_uri=(value)
58
+ @_redirect_uri = value
59
+ end
60
+
61
+ def client_id
62
+ @_client_id ||= nil
63
+ unless @_client_id
64
+ raise AuthenticationError.new('No client id provided. ' \
65
+ 'Set your client id using "FieldView.client_id = <CLIENT-ID>". ' \
66
+ 'This should have been provided to you by a Climate representative. ' \
67
+ 'This takes the form of "my-client"')
68
+ end
69
+ return @_client_id
70
+ end
71
+ def client_id=(value)
72
+ @_client_id = value
73
+ end
74
+
75
+ def client_secret
76
+ @_client_secret ||= nil
77
+ unless @_client_secret
78
+ raise AuthenticationError.new('No client_secret provided. ' \
79
+ 'Set your client secret using "FieldView.client_secret = <CLIENT-SECRET>". ' \
80
+ 'This should have been provided to you by a Climate representative. ' \
81
+ 'This takes the form of "my-client-fz9900x98-x98908j-jslx"')
82
+ end
83
+ return @_client_secret
84
+ end
85
+ def client_secret=(value)
86
+ @_client_secret = value
87
+ end
88
+
89
+ def handle_response_error_codes(response)
90
+ headers = response.to_hash
91
+ code = response.code.to_i
92
+ body = response.body
93
+ error = nil
94
+ case code
95
+ when 503
96
+ error = ServerBusyError.new(
97
+ "Server Busy", http_status: code, http_body: body,
98
+ http_headers: headers)
99
+ when 400, 404
100
+ error = InvalidRequestError.new(
101
+ "Bad input", http_status: code, http_body: body,
102
+ http_headers: headers)
103
+ when 401
104
+ error = AuthenticationError.new(
105
+ "Unauthorized", http_status: code, http_body: body,
106
+ http_headers: headers)
107
+ when 403
108
+ error = PermissionError.new(
109
+ "Forbidden", http_status: code, http_body: body,
110
+ http_headers: headers)
111
+ when 429
112
+ # Retry-After will be in the headers
113
+ error = RateLimitError.new(
114
+ "Too many requests", http_status: code, http_body: body,
115
+ http_headers: headers)
116
+ when 500
117
+ error = InternalServerError.new(
118
+ "Internal server error", http_status: code, http_body: body,
119
+ http_headers: headers)
120
+ end
121
+
122
+ if error.nil? then
123
+ return
124
+ else
125
+ error.response = response
126
+ raise error
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,141 @@
1
+ {
2
+ "boundary_one": {
3
+ "id": "e694b43e-86eb-4773-86d0-3c4f546178dc",
4
+ "area": {
5
+ "q": 58.923473472390256,
6
+ "u": "ac"
7
+ },
8
+ "centroid": {
9
+ "type": "Point",
10
+ "coordinates": [
11
+ -105.223479062131,
12
+ 40.0579906147381
13
+ ]
14
+ },
15
+ "geometry": {
16
+ "type": "MultiPolygon",
17
+ "coordinates": [
18
+ [
19
+ [
20
+ [
21
+ -105.2255165,
22
+ 40.0437726
23
+ ],
24
+ [
25
+ -105.2208496,
26
+ 40.0438021
27
+ ],
28
+ [
29
+ -105.2208915,
30
+ 40.0402151
31
+ ],
32
+ [
33
+ -105.2255049,
34
+ 40.0402473
35
+ ],
36
+ [
37
+ -105.2255165,
38
+ 40.0437726
39
+ ]
40
+ ]
41
+ ],
42
+ [
43
+ [
44
+ [
45
+ -105.2257098,
46
+ 40.087157
47
+ ],
48
+ [
49
+ -105.2257173,
50
+ 40.0903676
51
+ ],
52
+ [
53
+ -105.2249648,
54
+ 40.0898429
55
+ ],
56
+ [
57
+ -105.2232786,
58
+ 40.0894078
59
+ ],
60
+ [
61
+ -105.2226989,
62
+ 40.0886577
63
+ ],
64
+ [
65
+ -105.2218189,
66
+ 40.0883161
67
+ ],
68
+ [
69
+ -105.2218299,
70
+ 40.0879038
71
+ ],
72
+ [
73
+ -105.2217414,
74
+ 40.0874009
75
+ ],
76
+ [
77
+ -105.2214516,
78
+ 40.0871603
79
+ ],
80
+ [
81
+ -105.2257098,
82
+ 40.087157
83
+ ]
84
+ ]
85
+ ]
86
+ ]
87
+ }
88
+ },
89
+ "single_field_list": {
90
+ "results": [
91
+ {
92
+ "id": "530dd6e4-170b-4254-b10c-ef07b9533f74",
93
+ "name": "NE of Airport",
94
+ "boundaryId": "e694b43e-86eb-4773-86d0-3c4f546178dc"
95
+ }
96
+ ]
97
+ },
98
+ "field_two_list": {
99
+ "results": [
100
+ {
101
+ "id": "687c5ad6-7969-4112-8234-f8b5474a7389",
102
+ "name": "SE of Airport",
103
+ "boundaryId": "0a34b7bc-3f6b-4ace-9e6d-f97a7c6b66b9"
104
+ }
105
+ ]
106
+ },
107
+ "authorization": {
108
+ "access_token": "b2a58121-e02e-4c56-9036-ac23eb295ddc",
109
+ "token_type": "bearer",
110
+ "refresh_token": "23bba1b4-4e61-4e41-83fb-620061158239",
111
+ "expires_in": 14399,
112
+ "scope": "openid partnerapis platform",
113
+ "id_token": "8s9nxjx908uoij12oik2j4.1j4kjs90ivcoixkjmxrw3um493u8924u4yuemnshfjdsen450q.dG9rZW4iLCJhdWQiOiJwYXJ0bmVyLWFncmlib3RpeCIsImV4cCI6MTQ5MTk0NTUzNSwiaWF0IjoxNDkxOTQ0OTM1LCJhdXRoX3RpbWUiOjE0OTE5NDQ5MzUsInVzZXJfaWQiOiIxODYzNzUiLCJzdWIiOiIxODYzNzUiLCJlbWFpbCI6ImNvbXB1dGVAYWdyaWJvdGl4LmNvbSIsImFkZHJlc3MiOnt9LCJyb2xlcyI6WyJjbGllbnQiXSwicGhvbmVfbnVtYmVyIjoiMzAzNzE3ODk3OSJ9.TjSPzpdIUUQMD3wh_-PCdB2o0M8n1tpWAeicQ-Wz-AwXsbmxesDnZroppsN-0_H_8ULOhSKwlGVGEuuD6QfYElhlGZuviSZcYkwYDNtXNN9oVa4MgfWK76txLV59oaGpFuHlqKGMskL4uV5Ao_H9_0jN1THqluav-WUh4WwxB-LclIu4i7gv1g5Bw5zpzzccQAzErc04u_Gf1gwi1IZtFYJJncSnnm1hp5XhBZgLZD0twiNOYzxML3nFbM37WEf1G3iqSLiTH1bIKc_MO7dG3o8oq3SoYjDmjRWl7iL7WD3jZHWiAuvVBa0rDqOOeVpCPUYPk62n9HJ6JNtMnSp3eUvnR8nfaOvm7P1ZehOAVnJVq1yWfjOtxEvjLBmD7wRs9I5N2OANiLgqvnvJVy_BuAqQXDqmgusrU9FotGRP2obSRfZvaPzxXurCYY9OfS8uxxHBlzVe06dTNRezuOzy_op7y33dOEOzFfoBNyFcTtlptcICs6uIsGC_k239auWTiMKIGgAdtbgoAvVLK49-s7IdX9gfUv9ugaZgduPDq6hzxWQh-Uahyf1LkpLfb9wTHPdq9upW8Dt5iSBQKcngCBdLjR3WrGIRi0M0m2beRSB-acUiIWreeoCOY78UErEd5ebYwfBaAFh1Igy5EfvfYyaNjhv1-cYX8q3fc2JBPPU",
114
+ "user": {
115
+ "tosacceptedat": null,
116
+ "email": "testing@test.com",
117
+ "firstviews": 0,
118
+ "phone": "5555555555",
119
+ "roles": [
120
+ "Client"
121
+ ],
122
+ "city": null,
123
+ "firstname": "Testing",
124
+ "source": "user-service-default",
125
+ "address1": null,
126
+ "mailchimp": false,
127
+ "address2": null,
128
+ "zip": null,
129
+ "id": 186375,
130
+ "state": null,
131
+ "lastname": "Lastname",
132
+ "uuid": "ca9cdad5-c759-44b5-9442-371338f301a9",
133
+ "enabled": true,
134
+ "country": "United States",
135
+ "asOf": 1491944935932
136
+ },
137
+ "site": {
138
+ "siteCode": "fccd95c45c048f7c8c9a60c8"
139
+ }
140
+ }
141
+ }
@@ -0,0 +1,26 @@
1
+ class APIFixtures
2
+ def initialize
3
+ @fixtures = ::JSON.parse(File.read("#{PROJECT_ROOT}/spec/fixtures.json"),
4
+ symbolize_names: true)
5
+ freeze_recursively(@fixtures)
6
+ end
7
+
8
+ def [](name)
9
+ @fixtures[name]
10
+ end
11
+
12
+ def fetch(*args)
13
+ @fixtures.fetch(*args)
14
+ end
15
+
16
+ private
17
+
18
+ def freeze_recursively(data)
19
+ data.each do |k, v|
20
+ if v.is_a?(Hash)
21
+ freeze_recursively(v)
22
+ end
23
+ end
24
+ data.freeze
25
+ end
26
+ end
@@ -0,0 +1,91 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ class TestAuthToken < Minitest::Test
4
+ FIXTURE = API_FIXTURES.fetch(:authorization)
5
+ def auth_uri_stub
6
+ "#{prefix_for_all_requests}#{FieldView.oauth_token_base[8..-1]}"
7
+ end
8
+
9
+ def check_token_matches_fixture(token)
10
+ assert_equal FIXTURE[:access_token], token.access_token
11
+ assert_equal FIXTURE[:refresh_token], token.refresh_token
12
+ assert_equal FieldView.now + FIXTURE[:expires_in] - 15, token.access_token_expiration_at
13
+ assert_equal FieldView.now - 5 + 29*24*60*60, token.refresh_token_expiration_at, "Should be 29 days in the future"
14
+ end
15
+
16
+ def setup
17
+ setup_for_api_requests
18
+ end
19
+
20
+ def teardown
21
+ teardown_for_api_request
22
+ end
23
+
24
+ def test_build_token_request
25
+ http, request = FieldView::AuthToken.build_token_request([["dont", "care"]])
26
+ assert_equal "/api/oauth/token?dont=care", request.path
27
+
28
+ headers = request.to_hash
29
+ assert_equal ["*/*"], headers["accept"]
30
+ assert_equal ["Basic #{Base64.encode64("#{FieldView.client_id}:#{FieldView.client_secret}").strip}"], headers["authorization"]
31
+ assert_equal ["application/x-www-form-urlencoded"], headers["content-type"]
32
+
33
+ assert_equal Net::HTTP::Post, request.class
34
+ assert_equal "api.climate.com", http.address
35
+ end
36
+
37
+ def test_new_auth_token_with_code_from_redirect_code_bad
38
+ stub_request(:post, "#{auth_uri_stub}?" \
39
+ "code&grant_type=authorization_code&redirect_uri=#{FieldView.redirect_uri}").
40
+ to_return(status: 401, body: JSON.generate({}))
41
+ assert_raises FieldView::AuthenticationError do
42
+ FieldView::AuthToken.new_auth_token_with_code_from_redirect_code(nil)
43
+ end
44
+ end
45
+
46
+ def test_new_auth_token_with_code_from_redirect_code_good
47
+ # now a good token
48
+ code = "code"
49
+ FieldView.now = Time.now
50
+ stub_request(:post, "#{auth_uri_stub}?" \
51
+ "code=#{code}&grant_type=authorization_code&redirect_uri=#{FieldView.redirect_uri}").
52
+ to_return(status: 200, body: JSON.generate(FIXTURE))
53
+ token = FieldView::AuthToken.new_auth_token_with_code_from_redirect_code(code)
54
+ check_token_matches_fixture(token)
55
+ end
56
+
57
+ def test_create_with_just_access_token
58
+ token = FieldView::AuthToken.new(access_token: "yyy")
59
+ assert !token.access_token_expired?
60
+ assert token.refresh_token_expired?
61
+ end
62
+
63
+ def test_create_with_just_refresh_token
64
+ FieldView.now = Time.now
65
+ token = FieldView::AuthToken.new(refresh_token: "xxx")
66
+ assert token.access_token_expired?
67
+ assert !token.refresh_token_expired?
68
+
69
+ stub_request(:post, "#{auth_uri_stub}?" \
70
+ "grant_type=refresh_token&refresh_token=#{token.refresh_token}").
71
+ to_return(status: 200, body: JSON.generate(FIXTURE))
72
+ token.refresh_access_token!
73
+ check_token_matches_fixture(token)
74
+ end
75
+
76
+ def test_completely_expired_token
77
+ token = FieldView::AuthToken.new(
78
+ access_token: "yyy",
79
+ refresh_token: "xxx",
80
+ refresh_token_expiration_at: Time.now - 20,
81
+ access_token_expiration_at: Time.now - 20
82
+ )
83
+
84
+ assert token.access_token_expired?
85
+ assert token.refresh_token_expired?
86
+
87
+ assert_raises FieldView::AllTokensExpiredError do
88
+ token.execute_request!(:get, "dont/care")
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,17 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ class TestBoundary < Minitest::Test
4
+ FIXTURE = API_FIXTURES.fetch(:boundary_one)
5
+ def test_initialization
6
+ boundary = FieldView::Boundary.new(FIXTURE)
7
+
8
+ assert_equal FIXTURE[:id], boundary.id
9
+ assert_equal 58.923473472390256, boundary.area
10
+ assert_equal "ac", boundary.units
11
+ assert_equal 2, boundary.centroid.coordinates.length
12
+ assert boundary.centroid.point?
13
+ assert_equal 2, boundary.geometry.coordinates.length
14
+ assert boundary.geometry.multi_polygon?
15
+ assert boundary.geometry.multi_polygon? != boundary.centroid.multi_polygon?, "make sure they are different types"
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ class TestField < Minitest::Test
4
+ FIXTURE = API_FIXTURES.fetch(:single_field_list)[:results][0]
5
+ def test_initialization
6
+ field = FieldView::Field.new(FIXTURE, new_auth_token)
7
+
8
+ assert_equal FIXTURE[:id], field.id
9
+ assert_equal FIXTURE[:name], field.name
10
+ assert_equal FIXTURE[:boundaryId], field.boundary_id
11
+ assert_equal new_auth_token.access_token, field.auth_token.access_token
12
+ end
13
+
14
+ def test_get_boundary
15
+ field = FieldView::Field.new(FIXTURE, new_auth_token)
16
+
17
+ stub_request(:get, /boundaries\/#{API_FIXTURES[:boundary_one][:id]}/).
18
+ to_return(status: 200, body: API_FIXTURES[:boundary_one].to_json())
19
+
20
+ api_requests() do
21
+ field.boundary
22
+ end
23
+
24
+ assert_equal FIXTURE[:boundaryId], field.boundary.id
25
+ end
26
+ end
@@ -0,0 +1,70 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ class TestFields < Minitest::Test
4
+ FIXTURE = API_FIXTURES.fetch(:single_field_list)
5
+
6
+ def setup
7
+ setup_for_api_requests
8
+ end
9
+
10
+ def teardown
11
+ teardown_for_api_request
12
+ end
13
+
14
+ def test_list_with_one_page()
15
+ next_token = "AZXJKLA123"
16
+ stub_request(:get, /fields/).
17
+ with(headers: { FieldView::PAGE_LIMIT_HEADER_KEY => FieldView.default_page_limit.to_s}).
18
+ to_return(status: 200, body: FIXTURE.to_json(),
19
+ headers: next_token_headers)
20
+ fields = FieldView::Fields.list(new_auth_token)
21
+
22
+ assert_equal FIXTURE[:results].length, fields.data.length
23
+ assert_equal FIXTURE[:results][0][:id], fields.data[0].id
24
+ assert_equal next_token, fields.next_token
25
+ assert_equal FieldView::Fields, fields.listable
26
+ end
27
+
28
+ def test_list_with_no_more_data
29
+ stub_request(:get, /fields/).
30
+ with(headers: { FieldView::PAGE_LIMIT_HEADER_KEY => FieldView.default_page_limit.to_s}).
31
+ to_return(status: 304, body: {}.to_json(),
32
+ headers: next_token_headers)
33
+ fields = FieldView::Fields.list(new_auth_token)
34
+
35
+ assert_equal 0, fields.data.length
36
+ end
37
+
38
+ def test_with_more_data
39
+ next_token = "AZXJKLA123"
40
+ stub_request(:get, /fields/).
41
+ with(headers: { FieldView::PAGE_LIMIT_HEADER_KEY => FieldView.default_page_limit.to_s}).
42
+ to_return(status: 200, body: FIXTURE.to_json(),
43
+ headers: next_token_headers)
44
+ fields = FieldView::Fields.list(new_auth_token)
45
+
46
+ assert_equal FIXTURE[:results].length, fields.data.length
47
+ assert_equal FIXTURE[:results][0][:id], fields.data[0].id
48
+ assert_equal next_token, fields.next_token
49
+ end
50
+
51
+ def test_with_limit
52
+ stub_request(:get, /fields/).
53
+ with(headers: { FieldView::PAGE_LIMIT_HEADER_KEY => 1}).
54
+ to_return(status: 200, body: FIXTURE.to_json(),
55
+ headers: next_token_headers)
56
+ fields = FieldView::Fields.list(new_auth_token, limit: 1)
57
+
58
+ assert_equal 1, fields.data.length
59
+ end
60
+
61
+ def test_with_next_token
62
+ next_token = "AZXJKLA123"
63
+ stub_request(:get, /fields/).
64
+ with(headers: { FieldView::NEXT_TOKEN_HEADER_KEY => next_token }).
65
+ to_return(status: 200, body: FIXTURE.to_json())
66
+
67
+ fields = FieldView::Fields.list(new_auth_token, next_token: next_token)
68
+ assert_equal 1, fields.data.length
69
+ end
70
+ end
@@ -0,0 +1,46 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ class TestFieldView < Minitest::Test
4
+ def test_now
5
+ assert (Time.now - FieldView.get_now_for_auth_token) <= 5, "should be close"
6
+ expected_time = Time.now - 20
7
+ FieldView.now = expected_time
8
+ assert_equal expected_time, FieldView.get_now_for_auth_token
9
+ end
10
+
11
+ def test_requires_client_id
12
+ FieldView.client_id = nil
13
+ assert_raises FieldView::AuthenticationError do
14
+ FieldView.client_id
15
+ end
16
+ FieldView.client_id = "test"
17
+ assert FieldView.client_id
18
+ end
19
+
20
+ def test_requires_client_secret
21
+ FieldView.client_secret = nil
22
+ assert_raises FieldView::AuthenticationError do
23
+ FieldView.client_secret
24
+ end
25
+ FieldView.client_secret = "test"
26
+ assert FieldView.client_secret
27
+ end
28
+
29
+ def test_requires_redirect_uri
30
+ FieldView.redirect_uri = nil
31
+ assert_raises FieldView::AuthenticationError do
32
+ FieldView.redirect_uri
33
+ end
34
+ FieldView.redirect_uri = "test"
35
+ assert FieldView.redirect_uri
36
+ end
37
+
38
+ def test_requires_x_api_key
39
+ FieldView.x_api_key = nil
40
+ assert_raises FieldView::AuthenticationError do
41
+ FieldView.x_api_key
42
+ end
43
+ FieldView.x_api_key = "test"
44
+ assert FieldView.x_api_key
45
+ end
46
+ end
@@ -0,0 +1,52 @@
1
+ require 'minitest/autorun'
2
+ require 'fieldview'
3
+ require 'byebug'
4
+ require 'webmock/minitest'
5
+
6
+ PROJECT_ROOT = File.expand_path("../../", __FILE__)
7
+ require File.expand_path('../api_fixtures', __FILE__)
8
+
9
+ class Minitest::Test
10
+ # Fixtures are available in tests using something like:
11
+ #
12
+ # API_FIXTURES[:fields][:id]
13
+ #
14
+ API_FIXTURES = APIFixtures.new
15
+
16
+
17
+ def next_token_headers(next_token = "AZXJKLA123")
18
+ return {FieldView::NEXT_TOKEN_HEADER_KEY => next_token}
19
+ end
20
+
21
+ def new_auth_token
22
+ FieldView::AuthToken.new(
23
+ access_token: "yyy",
24
+ refresh_token: "xxx"
25
+ )
26
+ end
27
+
28
+ def api_requests()
29
+ setup_for_api_requests
30
+ yield
31
+ teardown_for_api_request
32
+ end
33
+
34
+ def setup_for_api_requests()
35
+ FieldView.redirect_uri = "http://fake.test/"
36
+ FieldView.client_id = "fake-client"
37
+ FieldView.client_secret = "fake-client-abcdef12-3456-7890-abcd-ef0123456789"
38
+ FieldView.x_api_key = "ixj98c98njkn109su9jxkjks61"
39
+ end
40
+
41
+ def teardown_for_api_request()
42
+ FieldView.redirect_uri = nil
43
+ FieldView.client_id = nil
44
+ FieldView.client_secret = nil
45
+ FieldView.x_api_key = nil
46
+ FieldView.now = nil
47
+ end
48
+
49
+ def prefix_for_all_requests
50
+ "https://"
51
+ end
52
+ end
@@ -0,0 +1,32 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ class TestListObject < Minitest::Test
4
+ FIXTURE = API_FIXTURES.fetch(:single_field_list)
5
+
6
+ def test_each_loop
7
+ data = ["x","y","z"]
8
+ list = FieldView::ListObject.new(FieldView::Fields, new_auth_token, data, 200, next_token: nil)
9
+
10
+ list.each_with_index do |x, i|
11
+ assert_equal data[i], x
12
+ end
13
+ end
14
+
15
+ def test_get_next_page
16
+ next_token = "JZIOJKLJ"
17
+ list = FieldView::ListObject.new(FieldView::Fields, new_auth_token,
18
+ ["dont","care"], 206, next_token: next_token)
19
+ stub_request(:get, /fields/).
20
+ with(headers: next_token_headers(next_token)).
21
+ to_return(status: 200, body: API_FIXTURES[:field_two_list].to_json(),
22
+ headers: next_token_headers())
23
+
24
+ api_requests() do
25
+ list.next_page!()
26
+ end
27
+
28
+ assert_equal 1, list.data.length
29
+ assert_equal API_FIXTURES[:field_two_list][:results][0][:id], list.data[0].id
30
+ assert !list.more_pages?()
31
+ end
32
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fieldview
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Paul Susmarski
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-04-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '0.9'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '0.9'
27
+ description: ' FieldView is used make data-driven decisions to maximize your return
28
+ on every acre.'
29
+ email: paul@susmarski.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - .gitignore
35
+ - Gemfile
36
+ - Gemfile.lock
37
+ - LICENSE
38
+ - README.md
39
+ - Rakefile
40
+ - fieldview.gemspec
41
+ - lib/fieldview.rb
42
+ - lib/fieldview/auth_token.rb
43
+ - lib/fieldview/boundary.rb
44
+ - lib/fieldview/errors.rb
45
+ - lib/fieldview/field.rb
46
+ - lib/fieldview/fields.rb
47
+ - lib/fieldview/fieldview_response.rb
48
+ - lib/fieldview/list_object.rb
49
+ - lib/fieldview/util.rb
50
+ - spec/fixtures.json
51
+ - test/api_fixtures.rb
52
+ - test/test_auth_token.rb
53
+ - test/test_boundary.rb
54
+ - test/test_field.rb
55
+ - test/test_fields.rb
56
+ - test/test_fieldview.rb
57
+ - test/test_helper.rb
58
+ - test/test_list_object.rb
59
+ homepage: http://rubygems.org/gems/fielview
60
+ licenses:
61
+ - MIT
62
+ metadata: {}
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - '>='
70
+ - !ruby/object:Gem::Version
71
+ version: 1.9.3
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubyforge_project:
79
+ rubygems_version: 2.0.14.1
80
+ signing_key:
81
+ specification_version: 4
82
+ summary: Ruby bindings for the FieldView API
83
+ test_files:
84
+ - test/api_fixtures.rb
85
+ - test/test_auth_token.rb
86
+ - test/test_boundary.rb
87
+ - test/test_field.rb
88
+ - test/test_fields.rb
89
+ - test/test_fieldview.rb
90
+ - test/test_helper.rb
91
+ - test/test_list_object.rb