diff --git a/.gitignore b/.gitignore index 94bb6bb..c3ebfa2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ # Ignore bundler config /.bundle Gemfile.lock + +spec/examples.txt + diff --git a/.rspec b/.rspec index b3eb8b4..c99d2e7 100644 --- a/.rspec +++ b/.rspec @@ -1,2 +1 @@ ---color ---format documentation \ No newline at end of file +--require spec_helper diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..ba36e77 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,31 @@ +inherit_from: .rubocop_todo.yml + +AllCops: + TargetRubyVersion: 2.2 + +# Keep diffs clean +Layout/TrailingBlankLines: + EnforcedStyle: final_blank_line + +# RSpec uses unparenthesized blocks all over; allow the idiomatic RSpec style. +Lint/AmbiguousBlockAssociation: + Exclude: + - spec/**/* + +# No way to avoid large blocks in RSpec +Metrics/BlockLength: + Exclude: + - spec/**/* + +# Allow the gem to have a non-snakecase name +Naming/FileName: + Exclude: + - 'lib/onfleet-ruby.rb' + +Style/Documentation: + Enabled: false + +Style/StringLiterals: + Exclude: + - spec/**/* + diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 0000000..124b7d9 --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,23 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2018-05-17 16:58:39 -0400 using RuboCop version 0.55.0. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 1 +Metrics/AbcSize: + Max: 18 + +# Offense count: 1 +# Configuration parameters: CountComments. +Metrics/MethodLength: + Max: 14 + +# Offense count: 62 +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. +# URISchemes: http, https +Metrics/LineLength: + Max: 126 + diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..025e764 --- /dev/null +++ b/.ruby-version @@ -0,0 +1,2 @@ +2.5.1 + diff --git a/Gemfile b/Gemfile index c398068..12f6e01 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ -source "https://rubygems.org" +source 'https://rubygems.org' -gemspec -gem 'rest-client', '~> 1.6.8' +ruby '2.5.1' +gemspec diff --git a/README.md b/README.md index ca3111c..b60f2d2 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Onfleet::Team Onfleet::Destination Onfleet::Recipient Onfleet::Task +Onfleet::Hub ``` ## Organizations @@ -307,6 +308,19 @@ Onfleet::Task.list({state: 0}) # => returns all tasks with state 0, see official **Complete** Currently not supported +## Hubs + +| Name | Type | Description | +| ----------- |--------| --------------| +| name | string | The hub’s name. | +| location | array | The `[longitude, latitude]` geographic coordinates. | +| address | object | The hub’s street address details. | + +**List** +```ruby +list = Onfleet::Hub.list # => [] +list.first # => Onfleet::Hub +``` ## Metadata | Name | Type | Description | diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..22deda5 --- /dev/null +++ b/Rakefile @@ -0,0 +1,10 @@ +require 'bundler/gem_tasks' + +require 'rubocop/rake_task' +RuboCop::RakeTask.new + +require 'rspec/core/rake_task' +RSpec::Core::RakeTask.new(:spec) + +task default: %i[spec rubocop] + diff --git a/lib/onfleet-ruby.rb b/lib/onfleet-ruby.rb index bbbcd3d..4a92ec5 100644 --- a/lib/onfleet-ruby.rb +++ b/lib/onfleet-ruby.rb @@ -3,9 +3,6 @@ require 'base64' require 'uri' -# Utils -require 'onfleet-ruby/util' - # Errors require 'onfleet-ruby/errors/onfleet_error' require 'onfleet-ruby/errors/authentication_error' @@ -27,76 +24,78 @@ require 'onfleet-ruby/recipient' require 'onfleet-ruby/destination' require 'onfleet-ruby/address' +require 'onfleet-ruby/barcode' require 'onfleet-ruby/task' require 'onfleet-ruby/organization' require 'onfleet-ruby/admin' require 'onfleet-ruby/team' require 'onfleet-ruby/vehicle' require 'onfleet-ruby/worker' +require 'onfleet-ruby/hub' require 'onfleet-ruby/webhook' - module Onfleet - @base_url = "https://onfleet.com/api/v2" + @base_url = 'https://onfleet.com/api/v2/' class << self - attr_accessor :api_key, :base_url, :encoded_api_key - end + attr_accessor :api_key, :base_url - def self.request api_url, method, params={} - raise AuthenticationError.new("Set your API Key using Onfleet.api_key = ") unless @api_key + def request(api_url, method, params = {}) + raise(AuthenticationError, 'Set your API Key using Onfleet.api_key = ') unless api_key - begin - response = RestClient::Request.execute(method: method, url: self.base_url+api_url, payload: params.to_json, headers: self.request_headers) - - if response != '' - JSON.parse(response) - end - rescue RestClient::ExceptionWithResponse => e - if response_code = e.http_code and response_body = e.http_body - handle_api_error(response_code, JSON.parse(response_body)) - else + begin + url = URI.join(base_url, api_url).to_s + response = RestClient::Request.execute(method: method, url: url, payload: params.to_json, headers: request_headers) + JSON.parse(response) unless response.empty? + rescue RestClient::ExceptionWithResponse => e + if (response_code = e.http_code) && (response_body = e.http_body) + handle_api_error(response_code, JSON.parse(response_body)) + else + handle_restclient_error(e) + end + rescue RestClient::Exception, Errno::ECONNREFUSED => e handle_restclient_error(e) end - rescue RestClient::Exception, Errno::ECONNREFUSED => e - handle_restclient_error(e) end - end - private - def self.request_headers + private + + def request_headers { - Authorization: "Basic #{self.encoded_api_key}", + Authorization: "Basic #{encoded_api_key}", content_type: :json, accept: :json } end - def self.encoded_api_key - @encoded_api_key ||= Base64.urlsafe_encode64(@api_key) + def encoded_api_key + @encoded_api_key ||= Base64.urlsafe_encode64(api_key) end - def self.handle_api_error code, body + def handle_api_error(code, body) case code when 400, 404 - raise InvalidRequestError.new(body["message"]) + raise InvalidRequestError, body['message'] when 401 - raise AuthenticationError.new(body["message"]) + raise AuthenticationError, body['message'] else - raise OnfleetError.new(body["message"]) + raise OnfleetError, body['message'] end end - def self.handle_restclient_error e - case e - when RestClient::RequestTimeout - message = "Could not connect to Onfleet. Check your internet connection and try again." - when RestClient::ServerBrokeConnection - message = "The connetion with onfleet terminated before the request completed. Please try again." - else - message = "There was a problem connection with Onfleet. Please try again. If the problem persists contact contact@onfleet.com" - end + def handle_restclient_error(exception) + message = + case exception + when RestClient::RequestTimeout + 'Could not connect to Onfleet. Check your internet connection and try again.' + when RestClient::ServerBrokeConnection + 'The connetion with onfleet terminated before the request completed. Please try again.' + else + 'There was a problem connection with Onfleet. Please try again. If the problem persists contact contact@onfleet.com' + end - raise ConnectionError.new(message) + raise ConnectionError, message end + end end + diff --git a/lib/onfleet-ruby/actions/create.rb b/lib/onfleet-ruby/actions/create.rb index 984d247..d5db07c 100644 --- a/lib/onfleet-ruby/actions/create.rb +++ b/lib/onfleet-ruby/actions/create.rb @@ -1,15 +1,18 @@ +require 'active_support/core_ext/hash' + module Onfleet module Actions module Create module ClassMethods - def create params={} - self.new(params).save + def create(params = {}) + new(params.symbolize_keys.except(:id)).save end end - def self.included base + def self.included(base) base.extend(ClassMethods) end end end end + diff --git a/lib/onfleet-ruby/actions/delete.rb b/lib/onfleet-ruby/actions/delete.rb index 648e563..e9f4a44 100644 --- a/lib/onfleet-ruby/actions/delete.rb +++ b/lib/onfleet-ruby/actions/delete.rb @@ -2,16 +2,16 @@ module Onfleet module Actions module Delete module ClassMethods - def delete id - api_url = "#{self.api_url}/#{id}" - response = Onfleet.request(api_url, :delete) + def delete(id) + Onfleet.request("#{api_url}/#{id}", :delete) true end end - def self.included base + def self.included(base) base.extend(ClassMethods) end end end end + diff --git a/lib/onfleet-ruby/actions/find.rb b/lib/onfleet-ruby/actions/find.rb index 83cfe1b..52411f4 100644 --- a/lib/onfleet-ruby/actions/find.rb +++ b/lib/onfleet-ruby/actions/find.rb @@ -2,17 +2,19 @@ module Onfleet module Actions module Find module ClassMethods - def find field, search_term - encoded_term = URI::encode(search_term) - api_url = "#{self.api_url}/#{field}/#{encoded_term}" - response = Onfleet.request(api_url, :get, search_term) - Util.constantize("#{self}").new(response) + def find(field, search_term) + encoded_term = URI.encode_www_form_component(search_term) + url = "#{api_url}/#{field}/#{encoded_term}" + + response = Onfleet.request(url, :get) + new(response) end end - def self.included base + def self.included(base) base.extend(ClassMethods) end end end end + diff --git a/lib/onfleet-ruby/actions/get.rb b/lib/onfleet-ruby/actions/get.rb index 186cace..df386c1 100644 --- a/lib/onfleet-ruby/actions/get.rb +++ b/lib/onfleet-ruby/actions/get.rb @@ -2,16 +2,16 @@ module Onfleet module Actions module Get module ClassMethods - def get id - api_url = "#{self.api_url}/#{id}" - response = Onfleet.request(api_url, :get) - Util.constantize("#{self}").new(response) + def get(id) + url = "#{api_url}/#{id}" + new(Onfleet.request(url, :get)) end end - def self.included base + def self.included(base) base.extend(ClassMethods) end end end end + diff --git a/lib/onfleet-ruby/actions/list.rb b/lib/onfleet-ruby/actions/list.rb index 77316ae..6bd10e3 100644 --- a/lib/onfleet-ruby/actions/list.rb +++ b/lib/onfleet-ruby/actions/list.rb @@ -2,26 +2,28 @@ module Onfleet module Actions module List module ClassMethods - def list query_params={} - api_url = "#{self.api_url}" + def list(filters = {}) + response = Onfleet.request(list_url_for(filters), :get) + response.compact.map { |item| new(item) } + end + + private - if !query_params.empty? - api_url += "?" - query_params.each do |key, value| - api_url += "#{key}=#{value}&" - end - end + def list_url_for(filters) + [api_url, query_params(filters)].compact.join('?') + end - response = Onfleet.request(api_url, :get) - response.compact.map do |listObj| - Util.constantize("#{self}").new(listObj) - end + def query_params(filters) + filters && filters + .collect { |key, value| "#{key}=#{URI.encode_www_form_component(value)}" } + .join('&') end end - def self.included base + def self.included(base) base.extend(ClassMethods) end end end end + diff --git a/lib/onfleet-ruby/actions/query_metadata.rb b/lib/onfleet-ruby/actions/query_metadata.rb index 7da9026..bdcfca7 100644 --- a/lib/onfleet-ruby/actions/query_metadata.rb +++ b/lib/onfleet-ruby/actions/query_metadata.rb @@ -2,16 +2,16 @@ module Onfleet module Actions module QueryMetadata module ClassMethods - def query_by_metadata metadata - api_url = "#{self.api_url}/metadata" - response = Onfleet.request(api_url, :post, metadata) - response.map { |item| Util.constantize("#{self}").new(item) } if response.is_a? Array + def query_by_metadata(metadata) + response = Onfleet.request("#{api_url}/metadata", :post, metadata) + [*response].compact.map { |item| new(item) } end end - def self.included base + def self.included(base) base.extend(ClassMethods) end end end end + diff --git a/lib/onfleet-ruby/actions/save.rb b/lib/onfleet-ruby/actions/save.rb index 8a5e39f..48a8d6d 100644 --- a/lib/onfleet-ruby/actions/save.rb +++ b/lib/onfleet-ruby/actions/save.rb @@ -2,16 +2,21 @@ module Onfleet module Actions module Save def save - if respond_to?('id') && self.id - request_type = :put - api_url = "#{self.api_url}/#{self.id}" - else - request_type = :post - api_url = self.api_url - end - response = Onfleet.request(api_url, request_type, self.attributes) - self.parse_response(response) + response = Onfleet.request(save_url, request_type, as_json) + parse_params(response) + self + end + + private + + def request_type + id ? :put : :post + end + + def save_url + [api_url, id].compact.join('/') end end end end + diff --git a/lib/onfleet-ruby/actions/update.rb b/lib/onfleet-ruby/actions/update.rb index 72e0733..bcb3974 100644 --- a/lib/onfleet-ruby/actions/update.rb +++ b/lib/onfleet-ruby/actions/update.rb @@ -2,15 +2,15 @@ module Onfleet module Actions module Update module ClassMethods - def update id, params - params.merge!(id: id) - self.new(params).save + def update(id, params) + new(params.merge(id: id)).save end end - def self.included base + def self.included(base) base.extend(ClassMethods) end end end end + diff --git a/lib/onfleet-ruby/address.rb b/lib/onfleet-ruby/address.rb index 426b493..e1a0a65 100644 --- a/lib/onfleet-ruby/address.rb +++ b/lib/onfleet-ruby/address.rb @@ -2,3 +2,4 @@ module Onfleet class Address < OnfleetObject end end + diff --git a/lib/onfleet-ruby/admin.rb b/lib/onfleet-ruby/admin.rb index fb78166..c838d8d 100644 --- a/lib/onfleet-ruby/admin.rb +++ b/lib/onfleet-ruby/admin.rb @@ -1,14 +1,6 @@ module Onfleet class Admin < OnfleetObject - include Onfleet::Actions::Create - include Onfleet::Actions::Save - include Onfleet::Actions::Update - include Onfleet::Actions::List - include Onfleet::Actions::Delete - include Onfleet::Actions::QueryMetadata - - def self.api_url - '/admins' - end + onfleet_api at: 'admins', actions: %i[list create update save delete query_metadata] end end + diff --git a/lib/onfleet-ruby/barcode.rb b/lib/onfleet-ruby/barcode.rb new file mode 100644 index 0000000..1c7ccf9 --- /dev/null +++ b/lib/onfleet-ruby/barcode.rb @@ -0,0 +1,5 @@ +module Onfleet + class Barcode < OnfleetObject + end +end + diff --git a/lib/onfleet-ruby/destination.rb b/lib/onfleet-ruby/destination.rb index 6f73e84..2ae4b5a 100644 --- a/lib/onfleet-ruby/destination.rb +++ b/lib/onfleet-ruby/destination.rb @@ -1,12 +1,7 @@ module Onfleet class Destination < OnfleetObject - include Onfleet::Actions::Create - include Onfleet::Actions::Save - include Onfleet::Actions::Get - include Onfleet::Actions::QueryMetadata - - def self.api_url - '/destinations' - end + onfleet_api at: 'destinations', actions: %i[create save get query_metadata] + associated_with :address end end + diff --git a/lib/onfleet-ruby/dsl.rb b/lib/onfleet-ruby/dsl.rb new file mode 100644 index 0000000..c4ca3a4 --- /dev/null +++ b/lib/onfleet-ruby/dsl.rb @@ -0,0 +1,101 @@ +module Onfleet + module Dsl + def self.included(klass) + klass.prepend(Decorations) + klass.extend(ClassMethods) + end + + module Decorations + def as_json(*) + id_only = self.class.id_only_attributes + self.class.id_only_collections + json = super(except: id_only) + json = serialize_id_only_attributes(json) + serialize_id_only_collections(json) + end + + private + + def serialize_id_only_attributes(json) + self.class.id_only_attributes.inject(json) do |result, attribute| + value = public_send(attribute) + result.merge(attribute => value && value.id) + end + end + + def serialize_id_only_collections(json) + self.class.id_only_collections.inject(json) do |result, attribute| + collection = public_send(attribute) + result.merge(attribute => collection.collect(&:id)) + end + end + end + + module ClassMethods + def onfleet_api(at:, actions:) + actions.each { |action| include Onfleet::Actions.const_get(action.to_s.camelize) } + singleton_class.define_method(:api_url) { at } + end + + def associated_with(associated, serialize_as: :object) + associated = associated.to_s + + define_single_association_getter(associated) + define_single_association_setter(associated) + + id_only_attributes << associated if serialize_as.to_sym == :id + end + + def associated_with_many(associated, serialize_as: :object) + associated = associated.to_s + + define_many_association_getter(associated) + define_many_association_setter(associated) + + id_only_collections << associated if serialize_as.to_sym == :id + end + + def id_only_attributes + @id_only_attributes ||= [] + end + + def id_only_collections + @id_only_collections ||= [] + end + + private + + def define_single_association_getter(associated) + define_method(associated) { attributes[associated] } + end + + def define_single_association_setter(associated) + require "onfleet-ruby/#{associated}" + associated_class = Onfleet.const_get(associated.to_s.camelize) + + define_method(:"#{associated}=") do |value| + attributes[associated] = build_from_attributes(associated_class, value) + end + end + + def define_many_association_getter(associated) + define_method(associated) { attributes[associated] || [] } + end + + def define_many_association_setter(associated) + singular = associated.to_s.singularize + require "onfleet-ruby/#{singular}" + associated_class = Onfleet.const_get(singular.camelize) + + define_method(:"#{associated}=") do |values| + values &&= values.collect { |value| build_from_attributes(associated_class, value) } + attributes[associated] = values + end + end + end + + def build_from_attributes(klass, value) + value && (value.respond_to?(:id) ? value : klass.new(value)) + end + end +end + diff --git a/lib/onfleet-ruby/errors/authentication_error.rb b/lib/onfleet-ruby/errors/authentication_error.rb index 9bc5dd4..8d78981 100644 --- a/lib/onfleet-ruby/errors/authentication_error.rb +++ b/lib/onfleet-ruby/errors/authentication_error.rb @@ -1,3 +1,4 @@ module Onfleet class AuthenticationError < OnfleetError; end end + diff --git a/lib/onfleet-ruby/errors/connection_error.rb b/lib/onfleet-ruby/errors/connection_error.rb index 42ba19e..790178d 100644 --- a/lib/onfleet-ruby/errors/connection_error.rb +++ b/lib/onfleet-ruby/errors/connection_error.rb @@ -1,3 +1,4 @@ module Onfleet class ConnectionError < OnfleetError; end end + diff --git a/lib/onfleet-ruby/errors/invalid_request_error.rb b/lib/onfleet-ruby/errors/invalid_request_error.rb index 4bdaa01..1e6260d 100644 --- a/lib/onfleet-ruby/errors/invalid_request_error.rb +++ b/lib/onfleet-ruby/errors/invalid_request_error.rb @@ -1,3 +1,4 @@ module Onfleet class InvalidRequestError < OnfleetError; end end + diff --git a/lib/onfleet-ruby/errors/onfleet_error.rb b/lib/onfleet-ruby/errors/onfleet_error.rb index 7c59b7f..66ee07e 100644 --- a/lib/onfleet-ruby/errors/onfleet_error.rb +++ b/lib/onfleet-ruby/errors/onfleet_error.rb @@ -1,3 +1,4 @@ module Onfleet class OnfleetError < StandardError; end end + diff --git a/lib/onfleet-ruby/hub.rb b/lib/onfleet-ruby/hub.rb new file mode 100644 index 0000000..dbf53b8 --- /dev/null +++ b/lib/onfleet-ruby/hub.rb @@ -0,0 +1,6 @@ +module Onfleet + class Hub < OnfleetObject + onfleet_api at: 'hubs', actions: %i[list] + end +end + diff --git a/lib/onfleet-ruby/onfleet_object.rb b/lib/onfleet-ruby/onfleet_object.rb index 37900ef..a30cfce 100644 --- a/lib/onfleet-ruby/onfleet_object.rb +++ b/lib/onfleet-ruby/onfleet_object.rb @@ -1,102 +1,67 @@ +require 'active_support/core_ext/string/inflections' +require 'active_support/json' +require 'active_support/core_ext/object/json' +require 'onfleet-ruby/dsl' + module Onfleet class OnfleetObject + include Onfleet::Dsl + attr_reader :params - def initialize params - if params.kind_of?(Hash) - @params = params - set_attributes(@params) - elsif params.kind_of?(String) - @params = {id: params} - set_attributes(@params) - else - @params = {} - end - end - def parse_response response - @params = response - set_attributes(response) - self + def initialize(params = {}) + if params.is_a?(Hash) + parse_params(params) + elsif params.is_a?(String) + parse_params(id: params) + end end - def attributes - attrs = Hash.new - instance_variables.select {|var| var != '@params'}.each do |var| - str = var.to_s.gsub /^@/, '' - if respond_to?("#{str}=") - instance_var = instance_variable_get(var) - if klass = Util.object_classes[str] - if instance_var.is_a?(OnfleetObject) - attrs[Util.to_camel_case_lower(str).to_sym] = parse_onfleet_obj(instance_var) - elsif instance_var.is_a?(Array) - objs = [] - instance_var.each do |object| - objs << parse_onfleet_obj(object) - end - attrs[Util.to_camel_case_lower(str).to_sym] = objs - else - attrs[Util.to_camel_case_lower(str).to_sym] = instance_var - end - else - attrs[Util.to_camel_case_lower(str).to_sym] = instance_var - end - end - end - attrs + def id + attributes['id'] end - def class_name - self.class.name.split("::").last + def id=(value) + attributes['id'] = value end - def api_url - "/#{CGI.escape(class_name.downcase)}s" + def as_json(*options) + attributes + .as_json(*options) + .transform_keys { |key| camelize_with_acronym(key) } end private - def parse_onfleet_obj obj - if obj.is_a?(OnfleetObject) - if obj.respond_to?('id') && obj.id && (obj.is_a?(Destination) || obj.is_a?(Recipient) || obj.is_a?(Task)) - obj.id - else - obj.attributes - end - end - end - - def set_attributes params - params.each do |key, value| - key_underscore = Util.to_underscore(key) + def attributes + @attributes ||= {} + end - if klass = Util.object_classes[key.to_s] - case value - when Array - objs = [] - value.each do |v| - objs << klass.new(v) - end - value = objs - when Hash - value = klass.new(value) - end - end + def api_url + self.class.api_url + end - if respond_to?("#{key_underscore}=") - send(:"#{key_underscore}=", value) - else - add_attrs({"#{key_underscore}" => value}) - end + def parse_params(params) + @params = params - end + params.each do |key, value| + key = key.to_s.underscore + define_attribute_accessors(key) unless respond_to?(key) + public_send(:"#{key}=", value) end + end - def add_attrs attrs - attrs.each do |var, value| - self.class.class_eval { attr_accessor var } - instance_variable_set "@#{var}", value - end - end + def define_attribute_accessors(attr) + attr = attr.to_s + + singleton_class.define_method(:"#{attr}=") { |value| attributes[attr] = value } + singleton_class.define_method(attr) { attributes[attr] } + end + def camelize_with_acronym(string) + camelized = string.camelize(:lower) + camelized.gsub('Sms', 'SMS') + end end end + diff --git a/lib/onfleet-ruby/organization.rb b/lib/onfleet-ruby/organization.rb index 95d5ad6..c67b512 100644 --- a/lib/onfleet-ruby/organization.rb +++ b/lib/onfleet-ruby/organization.rb @@ -1,19 +1,14 @@ module Onfleet class Organization < OnfleetObject - class << self def get - url = "/organization" - response = Onfleet.request(url, :get) - Util.constantize("#{self}").new(response) + new(Onfleet.request('organization', :get)) end - def get_delegatee_details id - url = "/organizations/#{id}" - response = Onfleet.request(url, :get) - Util.constantize("#{self}").new(response) + def get_delegatee_details(id) + new(Onfleet.request("organizations/#{id}", :get)) end end - end end + diff --git a/lib/onfleet-ruby/recipient.rb b/lib/onfleet-ruby/recipient.rb index cc9ce68..170d1d9 100644 --- a/lib/onfleet-ruby/recipient.rb +++ b/lib/onfleet-ruby/recipient.rb @@ -1,14 +1,6 @@ module Onfleet class Recipient < OnfleetObject - include Onfleet::Actions::Create - include Onfleet::Actions::Update - include Onfleet::Actions::Save - include Onfleet::Actions::Find - include Onfleet::Actions::Get - include Onfleet::Actions::QueryMetadata - - def self.api_url - "/recipients" - end + onfleet_api at: 'recipients', actions: %i[find get create update save query_metadata] end end + diff --git a/lib/onfleet-ruby/task.rb b/lib/onfleet-ruby/task.rb index c3deeac..31d023d 100644 --- a/lib/onfleet-ruby/task.rb +++ b/lib/onfleet-ruby/task.rb @@ -1,24 +1,17 @@ module Onfleet class Task < OnfleetObject - include Onfleet::Actions::Create - include Onfleet::Actions::Save - include Onfleet::Actions::Update - include Onfleet::Actions::Get - include Onfleet::Actions::List - include Onfleet::Actions::Delete - include Onfleet::Actions::QueryMetadata - - def self.api_url - '/tasks' - end + onfleet_api at: 'tasks', actions: %i[list get create update save delete query_metadata] + associated_with :destination, serialize_as: :id + associated_with_many :recipients, serialize_as: :id + associated_with_many :barcodes def complete # CURRENTLY DOESN'T WORK - url = "#{self.url}/#{self.id}/complete" - params = {"completionDetails" => {"success" => true }} + url = "#{self.url}/#{id}/complete" + params = { 'completionDetails' => { 'success' => true } } Onfleet.request(url, :post, params) true end - end end + diff --git a/lib/onfleet-ruby/team.rb b/lib/onfleet-ruby/team.rb index a4485d3..6b80107 100644 --- a/lib/onfleet-ruby/team.rb +++ b/lib/onfleet-ruby/team.rb @@ -1,10 +1,7 @@ module Onfleet class Team < OnfleetObject - include Onfleet::Actions::List - include Onfleet::Actions::Get - - def self.api_url - '/teams' - end + onfleet_api at: 'teams', actions: %i[list get] + associated_with_many :tasks, serialize_as: :id end end + diff --git a/lib/onfleet-ruby/util.rb b/lib/onfleet-ruby/util.rb deleted file mode 100644 index 9ca8062..0000000 --- a/lib/onfleet-ruby/util.rb +++ /dev/null @@ -1,36 +0,0 @@ -module Onfleet - class Util - SPECIAL_PARSE = { "skip_sms_notifications" => "skipSMSNotifications" } - - def self.constantize class_name - Object.const_get(class_name) - end - - def self.to_underscore key - if key.kind_of?(Symbol) - key = key.to_s - end - key.gsub(/::/, '/'). - gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). - gsub(/([a-z\d])([A-Z])/,'\1_\2'). - tr("-", "_"). - downcase - end - - def self.to_camel_case_lower str - SPECIAL_PARSE[str] || str.camelize(:lower) - end - - def self.object_classes - @object_classes ||= { - "address" => Address, - "recipients" => Recipient, - "recipient" => Recipient, - "tasks" => Task, - "destination" => Destination, - "vehicle" => Vehicle - } - end - end - -end diff --git a/lib/onfleet-ruby/vehicle.rb b/lib/onfleet-ruby/vehicle.rb index 0ecd324..4baaaf6 100644 --- a/lib/onfleet-ruby/vehicle.rb +++ b/lib/onfleet-ruby/vehicle.rb @@ -2,3 +2,4 @@ module Onfleet class Vehicle < OnfleetObject end end + diff --git a/lib/onfleet-ruby/webhook.rb b/lib/onfleet-ruby/webhook.rb index b6e234e..99e9828 100644 --- a/lib/onfleet-ruby/webhook.rb +++ b/lib/onfleet-ruby/webhook.rb @@ -1,13 +1,6 @@ module Onfleet class Webhook < OnfleetObject - include Onfleet::Actions::Create - include Onfleet::Actions::List - include Onfleet::Actions::Save - include Onfleet::Actions::Delete - - - def self.api_url - '/webhooks' - end + onfleet_api at: 'webhooks', actions: %i[list create save delete] end end + diff --git a/lib/onfleet-ruby/worker.rb b/lib/onfleet-ruby/worker.rb index 7fa419c..fc0edde 100644 --- a/lib/onfleet-ruby/worker.rb +++ b/lib/onfleet-ruby/worker.rb @@ -1,15 +1,8 @@ module Onfleet class Worker < OnfleetObject - include Onfleet::Actions::Create - include Onfleet::Actions::List - include Onfleet::Actions::Get - include Onfleet::Actions::Save - include Onfleet::Actions::Update - include Onfleet::Actions::Delete - include Onfleet::Actions::QueryMetadata - - def self.api_url - '/workers' - end + onfleet_api at: 'workers', actions: %i[list get create update save delete query_metadata] + associated_with :vehicle + associated_with_many :tasks, serialize_as: :id end end + diff --git a/onfleet-ruby.gemspec b/onfleet-ruby.gemspec index 5e4e916..f5ccc59 100644 --- a/onfleet-ruby.gemspec +++ b/onfleet-ruby.gemspec @@ -2,19 +2,24 @@ Gem::Specification.new do |s| s.name = 'onfleet-ruby' s.version = '0.1.4' s.date = '2016-04-08' - s.summary = "Onfleet ruby api" + s.summary = 'Onfleet ruby api' s.description = "To interact with Onfleet's API" - s.authors = ["Nick Wargnier"] + s.authors = ['Nick Wargnier'] s.email = 'nick@stylelend.com' s.homepage = 'http://rubygems.org/gems/onfleet-ruby' s.license = 'MIT' + s.add_dependency('activesupport', '>= 4.2') s.add_dependency('rest-client', '~> 1.4') - s.add_development_dependency("rspec",'~> 3.3.0', '>= 3.0.0') - + s.add_development_dependency('rake') + s.add_development_dependency('rspec', '~> 3.3') + s.add_development_dependency('rspec-its') + s.add_development_dependency('rubocop', '~> 0.55') + s.add_development_dependency('webmock', '~> 3.4') s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.require_paths = ['lib'] end + diff --git a/spec/onfleet/admin_spec.rb b/spec/onfleet/admin_spec.rb new file mode 100644 index 0000000..1851f51 --- /dev/null +++ b/spec/onfleet/admin_spec.rb @@ -0,0 +1,71 @@ +RSpec.describe Onfleet::Admin do + let(:admin) { described_class.new(params) } + let(:params) { { id: id, name: 'An Admin' } } + let(:id) { 'an-admin' } + + it_should_behave_like Onfleet::OnfleetObject + + describe ".list" do + subject { -> { described_class.list(query_params) } } + + context "with no filter" do + let(:query_params) { nil } + it_should_behave_like Onfleet::Actions::List, path: 'admins' + end + + context "with query params" do + let(:query_params) { { food: 'pizza', topping: 'mushroom' } } + it_should_behave_like Onfleet::Actions::List, path: 'admins?food=pizza&topping=mushroom' + end + + context "with a URL-unsafe query param" do + let(:query_params) { { food: 'green eggs & ham' } } + it_should_behave_like Onfleet::Actions::List, path: 'admins?food=green+eggs+%26+ham' + end + end + + describe ".create" do + subject { -> { described_class.create(params) } } + it_should_behave_like Onfleet::Actions::Create, path: 'admins' + end + + describe ".update" do + subject { -> { described_class.update(id, params) } } + it_should_behave_like Onfleet::Actions::Update, path: 'admins/an-admin' + end + + describe ".delete" do + subject { -> { described_class.delete(id) } } + it_should_behave_like Onfleet::Actions::Delete, path: 'admins/an-admin' + end + + describe ".query_by_metadata" do + subject { -> { described_class.query_by_metadata(metadata) } } + let(:metadata) { [{ name: 'color', type: 'string', value: 'ochre' }] } + it_should_behave_like Onfleet::Actions::QueryMetadata, path: 'admins' + end + + describe "#save" do + subject { -> { admin.save } } + + context "with an ID attribute" do + before { expect(params[:id]).to be } + it_should_behave_like Onfleet::Actions::Update, path: 'admins/an-admin' + end + + context "without an ID attribute" do + let(:params) { { name: 'An Admin' } } + it_should_behave_like Onfleet::Actions::Create, path: 'admins' + end + end + + %i[id name email type metadata].each do |attr| + describe "##{attr}" do + subject { admin.public_send(attr) } + let(:params) { { attr => value } } + let(:value) { 'pizza' } + it { should == value } + end + end +end + diff --git a/spec/onfleet/destination_spec.rb b/spec/onfleet/destination_spec.rb new file mode 100644 index 0000000..b207cfc --- /dev/null +++ b/spec/onfleet/destination_spec.rb @@ -0,0 +1,96 @@ +RSpec.describe Onfleet::Destination do + let(:destination) { described_class.new(params) } + let(:params) { { id: id, address: address_params } } + let(:id) { 'a-destination' } + let(:address_params) { { street: '123 Main', city: 'Foo', state: 'TX' } } + + it_should_behave_like Onfleet::OnfleetObject + + describe ".create" do + subject { -> { described_class.create(params) } } + it_should_behave_like Onfleet::Actions::Create, path: 'destinations' + end + + describe ".get" do + subject { -> { described_class.get(id) } } + let(:id) { 'a-destination' } + it_should_behave_like Onfleet::Actions::Get, path: 'destinations/a-destination' + end + + describe ".query_by_metadata" do + subject { -> { described_class.query_by_metadata(metadata) } } + let(:metadata) { [{ name: 'color', type: 'string', value: 'ochre' }] } + it_should_behave_like Onfleet::Actions::QueryMetadata, path: 'destinations' + end + + describe "#save" do + subject { -> { destination.save } } + + context "with an ID attribute" do + before { expect(params[:id]).to be } + it_should_behave_like Onfleet::Actions::Update, path: 'destinations/a-destination' + end + + context "without an ID attribute" do + let(:params) { { address: address_params } } + it_should_behave_like Onfleet::Actions::Create, path: 'destinations' + end + end + + describe "#address" do + subject { destination.address } + + context "when initialized with address params" do + let(:address_params) do + { + number: '123', + street: 'Main St.', + apartment: '', + city: 'Foo', + state: 'TX', + postalCode: '99999', + country: 'United States' + } + end + its(:number) { should == '123' } + its(:postal_code) { should == '99999' } + end + + context "when initialized with no address params" do + let(:address_params) { nil } + it { should be_nil } + end + end + + describe "#address=" do + subject { -> { destination.address = address } } + let(:destination) { described_class.new } + + context "with an Address object" do + let(:address) { Onfleet::Address.new(address_params) } + it { should change(destination, :address).from(nil).to(address) } + end + + context "with a hash of address params" do + let(:address) { address_params } + it { should change(destination, :address).from(nil).to be_kind_of(Onfleet::Address) } + end + end + + describe "#as_json" do + subject { destination.as_json } + + its(['id']) { should == params[:id] } + + context "with an address" do + let(:params) { { address: Onfleet::Address.new(address_params) } } + let(:address_params) { { street: 'Main St', postal_code: '99999' } } + + it "should include the full address attributes" do + expect(subject['address']['street']).to eq(address_params[:street]) + expect(subject['address']['postalCode']).to eq(address_params[:postal_code]) + end + end + end +end + diff --git a/spec/onfleet/hub_spec.rb b/spec/onfleet/hub_spec.rb new file mode 100644 index 0000000..ed76d8c --- /dev/null +++ b/spec/onfleet/hub_spec.rb @@ -0,0 +1,26 @@ +RSpec.describe Onfleet::Hub do + let(:hub) { described_class.new(params) } + let(:params) { { id: 'a-hub', name: 'The Warehouse' } } + + it_should_behave_like Onfleet::OnfleetObject + + describe ".list" do + subject { -> { described_class.list(query_params) } } + + context "with no filter" do + let(:query_params) { nil } + it_should_behave_like Onfleet::Actions::List, path: 'hubs' + end + + context "with query params" do + let(:query_params) { { food: 'pizza', topping: 'mushroom' } } + it_should_behave_like Onfleet::Actions::List, path: 'hubs?food=pizza&topping=mushroom' + end + + context "with a URL-unsafe query param" do + let(:query_params) { { food: 'green eggs & ham' } } + it_should_behave_like Onfleet::Actions::List, path: 'hubs?food=green+eggs+%26+ham' + end + end +end + diff --git a/spec/onfleet/organization_spec.rb b/spec/onfleet/organization_spec.rb new file mode 100644 index 0000000..cf79ef7 --- /dev/null +++ b/spec/onfleet/organization_spec.rb @@ -0,0 +1,27 @@ +RSpec.describe Onfleet::Organization do + let(:organization) { described_class.new(params) } + let(:params) { { id: 'an-org' } } + + it_should_behave_like Onfleet::OnfleetObject + + describe ".get" do + subject { -> { described_class.get } } + it_should_behave_like Onfleet::Actions::Get, path: 'organization' + end + + describe ".get_delegatee_details" do + subject { -> { described_class.get_delegatee_details(id) } } + let(:id) { 'my-org' } + it_should_behave_like Onfleet::Actions::Get, path: 'organizations/my-org' + end + + %i[id name email country timezone time_created time_last_modified].each do |attr| + describe "##{attr}" do + subject { organization.public_send(attr) } + let(:params) { { attr => value } } + let(:value) { 'pizza' } + it { should == value } + end + end +end + diff --git a/spec/onfleet/recipient_spec.rb b/spec/onfleet/recipient_spec.rb new file mode 100644 index 0000000..e5f60f1 --- /dev/null +++ b/spec/onfleet/recipient_spec.rb @@ -0,0 +1,76 @@ +RSpec.describe Onfleet::Recipient do + let(:recipient) { described_class.new(params) } + let(:params) { { id: id, name: 'Recipient Jones' } } + let(:id) { 'a-recipient' } + + it_should_behave_like Onfleet::OnfleetObject + + describe ".create" do + subject { -> { described_class.create(params) } } + it_should_behave_like Onfleet::Actions::Create, path: 'recipients' + + context "with the `skip_sms_notifications` attribute" do + set_up_request_stub(:post, 'recipients') + let(:params) { { skip_sms_notifications: true } } + let(:response_body) { { id: 'an-object' } } + + it "should camelize the attribute name properly" do + subject.call + expect( + a_request(:post, url).with(body: { 'skipSMSNotifications' => true }.to_json) + ).to have_been_made.once + end + end + end + + describe ".get" do + subject { -> { described_class.get(id) } } + it_should_behave_like Onfleet::Actions::Get, path: 'recipients/a-recipient' + end + + describe ".update" do + subject { -> { described_class.update(id, params) } } + it_should_behave_like Onfleet::Actions::Update, path: 'recipients/a-recipient' + + context "with the `skip_sms_notifications` attribute" do + set_up_request_stub(:put, 'recipients/a-recipient') + let(:params) { { id: id, skip_sms_notifications: true } } + let(:response_body) { { id: 'an-object' } } + + it "should camelize the attribute name properly" do + subject.call + expect( + a_request(:put, url).with(body: { id: id, 'skipSMSNotifications' => true }.to_json) + ).to have_been_made.once + end + end + end + + describe ".find" do + subject { -> { described_class.find(attribute, value) } } + let(:attribute) { 'name' } + let(:value) { 'Ma Bell' } + it_should_behave_like Onfleet::Actions::Find, path: "recipients/name/Ma+Bell" + end + + describe ".query_by_metadata" do + subject { -> { described_class.query_by_metadata(metadata) } } + let(:metadata) { [{ name: 'color', type: 'string', value: 'ochre' }] } + it_should_behave_like Onfleet::Actions::QueryMetadata, path: 'recipients' + end + + describe "#save" do + subject { -> { recipient.save } } + + context "with an ID attribute" do + before { expect(params[:id]).to be } + it_should_behave_like Onfleet::Actions::Update, path: 'recipients/a-recipient' + end + + context "without an ID attribute" do + let(:params) { { name: 'Recipient Jones' } } + it_should_behave_like Onfleet::Actions::Create, path: 'recipients' + end + end +end + diff --git a/spec/onfleet/task_spec.rb b/spec/onfleet/task_spec.rb new file mode 100644 index 0000000..deae846 --- /dev/null +++ b/spec/onfleet/task_spec.rb @@ -0,0 +1,254 @@ +RSpec.describe Onfleet::Task do + let(:task) { described_class.new(params) } + let(:params) { { id: id, short_id: 'at', destination: 'a-destination', recipients: ['jeff'] } } + let(:id) { 'a-task' } + + it_should_behave_like Onfleet::OnfleetObject + + describe ".list" do + subject { -> { described_class.list(query_params) } } + + context "with no filter" do + let(:query_params) { nil } + it_should_behave_like Onfleet::Actions::List, path: 'tasks' + end + + context "with query params" do + let(:query_params) { { food: 'pizza', topping: 'mushroom' } } + it_should_behave_like Onfleet::Actions::List, path: 'tasks?food=pizza&topping=mushroom' + end + + context "with a URL-unsafe query param" do + let(:query_params) { { food: 'green eggs & ham' } } + it_should_behave_like Onfleet::Actions::List, path: 'tasks?food=green+eggs+%26+ham' + end + end + + describe ".create" do + subject { -> { described_class.create(params) } } + it_should_behave_like Onfleet::Actions::Create, path: 'tasks' + + context "with the skip_sms_notification override attribute" do + set_up_request_stub(:post, 'tasks') + let(:params) { { recipient_skip_sms_notifications: true } } + let(:response_body) { { id: 'an-object' } } + + it "should camelize the attribute name properly" do + subject.call + expect( + a_request(:post, url).with(body: { + recipientSkipSMSNotifications: true, + destination: nil, + recipients: [] + }.to_json) + ).to have_been_made.once + end + end + + context "with barcode attributes" do + set_up_request_stub(:post, 'tasks') + let(:params) { { barcodes: [{ data: 'abc', block_completion: true }] } } + let(:response_body) { { id: 'an-object' } } + + it "should camelize the attribute name properly" do + subject.call + expect( + a_request(:post, url).with(body: { + barcodes: [{ data: 'abc', 'blockCompletion' => true }], + destination: nil, + recipients: [] + }.to_json) + ).to have_been_made.once + end + end + end + + describe ".get" do + subject { -> { described_class.get(id) } } + it_should_behave_like Onfleet::Actions::Get, path: 'tasks/a-task' + end + + describe ".update" do + subject { -> { described_class.update(id, params) } } + it_should_behave_like Onfleet::Actions::Update, path: 'tasks/a-task' + + context "with the skip_sms_notification override attribute" do + set_up_request_stub(:put, 'tasks/a-task') + let(:params) { { id: id, recipient_skip_sms_notifications: true } } + let(:response_body) { { id: 'an-object' } } + + it "should camelize the attribute name properly" do + subject.call + expect( + a_request(:put, url).with(body: { + id: id, + recipientSkipSMSNotifications: true, + destination: nil, + recipients: [] + }.to_json) + ).to have_been_made.once + end + end + + context "with barcode attributes" do + set_up_request_stub(:put, 'tasks/a-task') + let(:params) { { id: id, barcodes: [{ data: 'abc', block_completion: true }] } } + let(:response_body) { { id: 'an-object' } } + + it "should camelize the attribute name properly" do + subject.call + expect( + a_request(:put, url).with(body: { + id: id, + barcodes: [{ data: 'abc', 'blockCompletion' => true }], + destination: nil, + recipients: [] + }.to_json) + ).to have_been_made.once + end + end + end + + describe ".delete" do + subject { -> { described_class.delete(id) } } + let(:id) { 'an-task' } + it_should_behave_like Onfleet::Actions::Delete, path: 'tasks/an-task' + end + + describe ".query_by_metadata" do + subject { -> { described_class.query_by_metadata(metadata) } } + let(:metadata) { [{ name: 'color', type: 'string', value: 'ochre' }] } + it_should_behave_like Onfleet::Actions::QueryMetadata, path: 'tasks' + end + + describe "#save" do + subject { -> { task.save } } + + context "with an ID attribute" do + before { expect(params[:id]).to be } + it_should_behave_like Onfleet::Actions::Update, path: 'tasks/a-task' + end + + context "without an ID attribute" do + let(:params) { { short_id: 'at', destination: 'a-destination', recipients: ['jeff'] } } + it_should_behave_like Onfleet::Actions::Create, path: 'tasks' + end + end + + describe "#destination" do + subject { task.destination } + + context "when initialized with destination params" do + let(:params) { { destination: destination_params } } + let(:destination_params) { { location: [-107, 44] } } + its(:location) { should == destination_params[:location] } + end + + context "when initialized with no destination params" do + let(:params) { { destination: nil } } + it { should be_nil } + end + end + + describe "#destination=" do + subject { -> { task.destination = destination } } + let(:task) { described_class.new } + let(:destination_params) { { location: [-107, 44] } } + + context "with an Destination object" do + let(:destination) { Onfleet::Destination.new(destination_params) } + it { should change(task, :destination).from(nil).to(destination) } + end + + context "with a hash of destination params" do + let(:destination) { destination_params } + it { should change(task, :destination).from(nil).to be_kind_of(Onfleet::Destination) } + end + + context "with nil" do + let(:destination) { nil } + before { task.destination = destination_params } + it { should change(task, :destination).to be_nil } + end + end + + describe "#recipients" do + subject { task.recipients } + + context "when initialized with recipients params" do + let(:params) { { recipients: [{ name: 'Leia' }, { name: 'Han' }] } } + its(:size) { should == params[:recipients].size } + its('first.name') { should == 'Leia' } + end + + context "when initialized with no recipients params" do + let(:params) { {} } + it { should be_empty } + end + end + + describe "#recipients=" do + subject { -> { task.recipients = recipients } } + let(:recipient) { described_class.new } + let(:task) { described_class.new } + + context "with an array of Recipient objects" do + let(:recipients) { [Onfleet::Recipient.new(name: 'Chewy')] } + it { should change(task, :recipients).from([]).to(recipients) } + end + + context "with an array that contains a hash of recipient params" do + let(:recipients) { [{ name: 'Chewy' }] } + it { should change { task.recipients.first }.from(nil).to be_kind_of(Onfleet::Recipient) } + end + end + + describe "#barcodes" do + subject { task.barcodes } + + context "when initialized with barcodes params" do + let(:params) { { barcodes: [{ data: 'foo' }, { data: 'bar' }] } } + its(:size) { should == params[:barcodes].size } + its('first.data') { should == 'foo' } + end + + context "when initialized with no barcodes params" do + let(:params) { {} } + it { should be_empty } + end + end + + describe "#barcodes=" do + subject { -> { task.barcodes = barcodes } } + let(:barcode) { described_class.new } + let(:task) { described_class.new } + + context "with an array of Barcode objects" do + let(:barcodes) { [Onfleet::Barcode.new(data: 'foo')] } + it { should change(task, :barcodes).from([]).to(barcodes) } + end + + context "with an array that contains a hash of barcode params" do + let(:barcodes) { [{ data: 'foo' }] } + it { should change { task.barcodes.first }.from(nil).to be_kind_of(Onfleet::Barcode) } + end + end + + describe "#as_json" do + subject { task.as_json } + + its(['id']) { should == params[:id] } + its(['shortId']) { should == params[:short_id] } + + context "with a destination" do + let(:params) { { destination: Onfleet::Destination.new(id: 'a-destination') } } + its(['destination']) { should == 'a-destination' } + end + + context "with recipients" do + let(:params) { { recipients: [{ id: 'a-recipient' }, { id: 'another-recipient' }] } } + its(['recipients']) { should == ['a-recipient', 'another-recipient'] } + end + end +end + diff --git a/spec/onfleet/team_spec.rb b/spec/onfleet/team_spec.rb new file mode 100644 index 0000000..9c2dbf7 --- /dev/null +++ b/spec/onfleet/team_spec.rb @@ -0,0 +1,74 @@ +RSpec.describe Onfleet::Team do + let(:team) { described_class.new(params) } + let(:params) { { id: 'a-team', name: 'Detroit Redwings' } } + + it_should_behave_like Onfleet::OnfleetObject + + describe ".list" do + subject { -> { described_class.list(query_params) } } + + context "with no filter" do + let(:query_params) { nil } + it_should_behave_like Onfleet::Actions::List, path: 'teams' + end + + context "with query params" do + let(:query_params) { { food: 'pizza', topping: 'mushroom' } } + it_should_behave_like Onfleet::Actions::List, path: 'teams?food=pizza&topping=mushroom' + end + + context "with a URL-unsafe query param" do + let(:query_params) { { food: 'green eggs & ham' } } + it_should_behave_like Onfleet::Actions::List, path: 'teams?food=green+eggs+%26+ham' + end + end + + describe ".get" do + subject { -> { described_class.get(id) } } + let(:id) { 'a-team' } + it_should_behave_like Onfleet::Actions::Get, path: 'teams/a-team' + end + + describe "#tasks" do + subject { team.tasks } + + context "when initialized with vehicle params" do + let(:params) { { tasks: tasks_params } } + let(:tasks_params) { [{ team: 'xavier' }, { team: 'francine' }] } + its(:size) { should == tasks_params.size } + it { should be_all { |task| task.is_a?(Onfleet::Task) } } + end + + context "when initialized with no task params" do + let(:tasks_params) { nil } + it { should be_empty } + end + end + + describe "#tasks=" do + subject { -> { team.tasks = tasks } } + let(:task) { described_class.new } + + context "with an array of Task objects" do + let(:tasks) { [Onfleet::Task.new(worker: 'Leia')] } + it { should change(team, :tasks).from([]).to(tasks) } + end + + context "with an array that contains a hash of task params" do + let(:tasks) { [{ worker: 'Leia' }] } + it { should change { team.tasks.first }.from(nil).to be_kind_of(Onfleet::Task) } + end + end + + describe "#as_json" do + subject { team.as_json } + + its(['id']) { should == params[:id] } + + context "with tasks" do + let(:params) { { tasks: [{ id: 'a-task' }, { id: 'another-task' }] } } + its(['tasks']) { should == ['a-task', 'another-task'] } + end + end +end + diff --git a/spec/onfleet/webhook_spec.rb b/spec/onfleet/webhook_spec.rb new file mode 100644 index 0000000..1c390ed --- /dev/null +++ b/spec/onfleet/webhook_spec.rb @@ -0,0 +1,51 @@ +RSpec.describe Onfleet::Webhook do + let(:webhook) { described_class.new(params) } + let(:params) { { id: id, url: 'https://example.com', is_enabled: true } } + let(:id) { 'a-webhook' } + + it_should_behave_like Onfleet::OnfleetObject + + describe ".list" do + subject { -> { described_class.list(query_params) } } + + context "with no filter" do + let(:query_params) { nil } + it_should_behave_like Onfleet::Actions::List, path: 'webhooks' + end + + context "with query params" do + let(:query_params) { { food: 'pizza', topping: 'mushroom' } } + it_should_behave_like Onfleet::Actions::List, path: 'webhooks?food=pizza&topping=mushroom' + end + + context "with a URL-unsafe query param" do + let(:query_params) { { food: 'green eggs & ham' } } + it_should_behave_like Onfleet::Actions::List, path: 'webhooks?food=green+eggs+%26+ham' + end + end + + describe ".create" do + subject { -> { described_class.create(params) } } + it_should_behave_like Onfleet::Actions::Create, path: 'webhooks' + end + + describe ".delete" do + subject { -> { described_class.delete(id) } } + it_should_behave_like Onfleet::Actions::Delete, path: 'webhooks/a-webhook' + end + + describe "#save" do + subject { -> { webhook.save } } + + context "with an ID attribute" do + before { expect(params[:id]).to be } + it_should_behave_like Onfleet::Actions::Update, path: 'webhooks/a-webhook' + end + + context "without an ID attribute" do + let(:params) { { name: 'An Webhook' } } + it_should_behave_like Onfleet::Actions::Create, path: 'webhooks' + end + end +end + diff --git a/spec/onfleet/worker_spec.rb b/spec/onfleet/worker_spec.rb new file mode 100644 index 0000000..78e9ead --- /dev/null +++ b/spec/onfleet/worker_spec.rb @@ -0,0 +1,158 @@ +RSpec.describe Onfleet::Worker do + let(:worker) { described_class.new(params) } + let(:params) { { id: id, name: 'F. Prefect', phone: '5551212', tasks: [] } } + let(:id) { 'a-worker' } + + it_should_behave_like Onfleet::OnfleetObject + + describe ".list" do + subject { -> { described_class.list(query_params) } } + + context "with no filter" do + let(:query_params) { nil } + it_should_behave_like Onfleet::Actions::List, path: 'workers' + end + + context "with query params" do + let(:query_params) { { food: 'pizza', topping: 'mushroom' } } + it_should_behave_like Onfleet::Actions::List, path: 'workers?food=pizza&topping=mushroom' + end + + context "with a URL-unsafe query param" do + let(:query_params) { { food: 'green eggs & ham' } } + it_should_behave_like Onfleet::Actions::List, path: 'workers?food=green+eggs+%26+ham' + end + end + + describe ".create" do + subject { -> { described_class.create(params) } } + it_should_behave_like Onfleet::Actions::Create, path: 'workers' + end + + describe ".get" do + subject { -> { described_class.get(id) } } + it_should_behave_like Onfleet::Actions::Get, path: 'workers/a-worker' + end + + describe ".update" do + subject { -> { described_class.update(id, params) } } + it_should_behave_like Onfleet::Actions::Update, path: 'workers/a-worker' + end + + describe ".delete" do + subject { -> { described_class.delete(id) } } + it_should_behave_like Onfleet::Actions::Delete, path: 'workers/a-worker' + end + + describe ".query_by_metadata" do + subject { -> { described_class.query_by_metadata(metadata) } } + let(:metadata) { [{ name: 'color', type: 'string', value: 'ochre' }] } + it_should_behave_like Onfleet::Actions::QueryMetadata, path: 'workers' + end + + describe "#save" do + subject { -> { worker.save } } + + context "with an ID attribute" do + before { expect(params[:id]).to be } + it_should_behave_like Onfleet::Actions::Update, path: 'workers/a-worker' + end + + context "without an ID attribute" do + let(:params) { { name: 'A Worker', tasks: [] } } + it_should_behave_like Onfleet::Actions::Create, path: 'workers' + end + end + + describe "#vehicle" do + subject { worker.vehicle } + + context "when initialized with vehicle params" do + let(:params) { { vehicle: vehicle_params } } + let(:vehicle_params) { { type: 'TRUCK', description: 'Oscar Meyer Weinermobile' } } + its(:type) { should == vehicle_params[:type] } + its(:description) { should == vehicle_params[:description] } + end + + context "when initialized with no vehicle params" do + let(:vehicle_params) { nil } + it { should be_nil } + end + end + + describe "#vehicle=" do + subject { -> { worker.vehicle = vehicle } } + let(:worker) { described_class.new } + let(:vehicle_params) { { type: 'CAR', description: 'The Batmobile' } } + + context "with an Vehicle object" do + let(:vehicle) { Onfleet::Vehicle.new(vehicle_params) } + it { should change(worker, :vehicle).from(nil).to(vehicle) } + end + + context "with a hash of vehicle params" do + let(:vehicle) { vehicle_params } + it { should change(worker, :vehicle).from(nil).to be_kind_of(Onfleet::Vehicle) } + end + + context "with nil" do + let(:vehicle) { nil } + before { worker.vehicle = vehicle_params } + it { should change(worker, :vehicle).to be_nil } + end + end + + describe "#tasks" do + subject { worker.tasks } + + context "when initialized with vehicle params" do + let(:params) { { tasks: tasks_params } } + let(:tasks_params) { [{ worker: 'xavier' }, { worker: 'francine' }] } + its(:size) { should == tasks_params.size } + it { should be_all { |task| task.is_a?(Onfleet::Task) } } + end + + context "when initialized with no task params" do + let(:tasks_params) { nil } + it { should be_empty } + end + end + + describe "#tasks=" do + subject { -> { worker.tasks = tasks } } + let(:task) { described_class.new } + + context "with an array of Task objects" do + let(:tasks) { [Onfleet::Task.new(worker: 'Leia')] } + it { should change(worker, :tasks).from([]).to(tasks) } + end + + context "with an array that contains a hash of task params" do + let(:tasks) { [{ worker: 'Leia' }] } + it { should change { worker.tasks.first }.from(nil).to be_kind_of(Onfleet::Task) } + end + end + + describe "#as_json" do + subject { worker.as_json } + + its(['id']) { should == params[:id] } + + context "with a vehicle" do + let(:params) { { vehicle: Onfleet::Vehicle.new(vehicle_params) } } + let(:vehicle_params) { { type: 'BICYCLE', description: 'Unicycle', license_plate: 'CLWNSRUL' } } + + it "should include the full vehicle attributes" do + expect(subject['vehicle']['type']).to eq(vehicle_params[:type]) + expect(subject['vehicle']['description']).to eq(vehicle_params[:description]) + expect(subject['vehicle']['licensePlate']).to eq(vehicle_params[:license_plate]) + end + end + + context "with tasks" do + let(:params) { { tasks: [{ id: 'a-task' }, { id: 'another-task' }] } } + its(['tasks']) { should == ['a-task', 'another-task'] } + end + end +end + diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8c9991f..feb56fb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,2 +1,78 @@ -require 'onfleet-ruby' -require File.expand_path('../test_data', __FILE__) +require 'rspec/its' +require 'webmock/rspec' +require File.expand_path(File.join('..', 'lib', 'onfleet-ruby'), __dir__) + +Dir.glob(File.expand_path(File.join('support', '**', '*.rb'), __dir__)).each { |file| require file } + +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = 'spec/examples.txt' + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + config.disable_monkey_patching! + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + config.warnings = true + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed + + config.before { Onfleet.api_key = 'TEST API KEY' } +end + diff --git a/spec/support/http_requests/onfleet/object/shared_examples.rb b/spec/support/http_requests/onfleet/object/shared_examples.rb new file mode 100644 index 0000000..3415653 --- /dev/null +++ b/spec/support/http_requests/onfleet/object/shared_examples.rb @@ -0,0 +1,22 @@ +RSpec.shared_examples_for Onfleet::OnfleetObject do + describe "#initialize" do + subject { described_class.new(param) } + let(:id) { 'an-object' } + + context "with a hash param" do + let(:param) { { id: id, name: 'Slartibartfast' } } + its(:id) { should == id } + end + + context "with a string param" do + let(:param) { id } + its(:id) { should == id } + end + + context "with no param" do + let(:param) { nil } + its(:id) { should be_nil } + end + end +end + diff --git a/spec/support/http_requests/shared_examples.rb b/spec/support/http_requests/shared_examples.rb new file mode 100644 index 0000000..476ba7d --- /dev/null +++ b/spec/support/http_requests/shared_examples.rb @@ -0,0 +1,124 @@ +RSpec.shared_examples_for "an action that makes a request to Onfleet" do |method:| + it "should include the base64-encoded API key in the auth header" do + encoded_api_key = Base64.urlsafe_encode64(Onfleet.api_key) + + subject.call + expect( + a_request(method, url).with(headers: { 'Authorization' => "Basic #{encoded_api_key}" }) + ).to have_been_made.once + end + + it "should specify that it will accept JSON" do + subject.call + expect( + a_request(method, url).with(headers: { 'Accept' => 'application/json' }) + ).to have_been_made.once + end + + if %i[post put patch].include?(method.to_sym) + it "should set the content type to JSON" do + subject.call + expect( + a_request(method, url).with(headers: { 'Content-Type' => 'application/json' }) + ).to have_been_made.once + end + end + + context "without valid authentication" do + let(:response) { { status: 401, body: { message: 'bad auth' }.to_json } } + it { should raise_error(Onfleet::AuthenticationError) } + end + + context "without valid authorization" do + let(:response) { { status: 404, body: { message: 'bad auth' }.to_json } } + it { should raise_error(Onfleet::InvalidRequestError) } + end + + context "when an unspecified error occurs" do + let(:response) { { status: 500, body: { message: 'all bad' }.to_json } } + it { should raise_error(Onfleet::OnfleetError) } + end +end + +RSpec.shared_examples_for Onfleet::Actions::Get do |path:| + set_up_request_stub(:get, path) + let(:response_body) { { id: 'an-object' } } + it_should_behave_like "an action that makes a request to Onfleet", method: :get +end + +RSpec.shared_examples_for Onfleet::Actions::List do |path:| + set_up_request_stub(:get, path) + let(:response_body) { [{ id: 'an-object' }, { id: 'another-object' }] } + it_should_behave_like "an action that makes a request to Onfleet", method: :get +end + +RSpec.shared_examples_for Onfleet::Actions::Create do |path:| + set_up_request_stub(:post, path) + let(:response_body) { { id: 'an-object' } } + + it_should_behave_like "an action that makes a request to Onfleet", method: :post + + it "should send the object params, not including ID, in JSON" do + expected_params = camelize_keys(params.stringify_keys.except('id')) + + subject.call + expect( + a_request(:post, url).with(body: expected_params.to_json) + ).to have_been_made.once + end +end + +RSpec.shared_examples_for Onfleet::Actions::Update do |path:| + set_up_request_stub(:put, path) + let(:response_body) { { id: 'an-object' } } + + it_should_behave_like "an action that makes a request to Onfleet", method: :put + + it "should send the object params, including ID, in JSON" do + expected_params = camelize_keys(params.merge(id: id).stringify_keys) + + subject.call + expect( + a_request(:put, url).with(body: expected_params.to_json) + ).to have_been_made.once + end +end + +RSpec.shared_examples_for Onfleet::Actions::Delete do |path:| + set_up_request_stub(:delete, path) + let(:response_body) { '' } + + it_should_behave_like "an action that makes a request to Onfleet", method: :delete +end + +RSpec.shared_examples_for Onfleet::Actions::Find do |path:| + set_up_request_stub(:get, path) + let(:response_body) { { id: 'an-object' } } + it_should_behave_like "an action that makes a request to Onfleet", method: :get +end + +RSpec.shared_examples_for Onfleet::Actions::QueryMetadata do |path:| + set_up_request_stub(:post, path + '/metadata') + let(:response_body) { [{ id: 'an-object' }, { id: 'another-object' }] } + it_should_behave_like "an action that makes a request to Onfleet", method: :post + + it "should send metadata in JSON" do + subject.call + expect( + a_request(:post, url).with(body: metadata.to_json) + ).to have_been_made.once + end +end + +def set_up_request_stub(method, path) + let(:url) { URI.join(Onfleet.base_url, path).to_s } + let(:response) { { status: 200, body: response_body.to_json } } + before { stub_request(method, url).to_return(response) } +end + +def camelize_keys(hash) + hash.inject({}) do |accumulator, (key, value)| + accumulator.merge(key.camelize(:lower) => value) + end +end + diff --git a/spec/test_data.rb b/spec/test_data.rb deleted file mode 100644 index df73c60..0000000 --- a/spec/test_data.rb +++ /dev/null @@ -1,14 +0,0 @@ -module Onfleet - module TestData - def recipient - - end - - def destination - - end - - def task - end - end -end