Skip to content
5 changes: 4 additions & 1 deletion lib/mindbody-api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@ def configuration
end

class Config
attr_accessor :log_level, :open_timeout, :read_timeout, :source_name, :source_key, :site_ids
attr_accessor :log_level, :open_timeout, :read_timeout, :source_name, :source_key, :site_ids, :username, :password, :prod_mode

def initialize
@log_level = :debug
@source_name = ENV['MINDBODY_SOURCE_NAME'] || ''
@source_key = ENV['MINDBODY_SOURCE_KEY'] || ''
@site_ids = (ENV['MINDBODY_SITE_IDS'] || '').scan(/-?\d+/).map(&:to_i)
@username = ENV['MINDBODY_USERNAME'] || nil
@password = ENV['MINDBODY_PASSWORD'] || nil
@prod_mode = ENV['MINDBODY_PROD_MODE'] || false
end

# Make sure site_ids is always an Array
Expand Down
45 changes: 40 additions & 5 deletions lib/mindbody-api/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,55 @@ def call(operation_name, locals = {}, &block)
@globals.open_timeout(MindBody.configuration.open_timeout)
@globals.read_timeout(MindBody.configuration.read_timeout)
@globals.log_level(MindBody.configuration.log_level)

auth = locals[:auth]
locals.delete(:auth)

locals = locals.has_key?(:message) ? locals[:message] : locals
locals = fixup_locals(locals)
params = {:message => {'Request' => auth_params.merge(locals)}}
params = {:message => {'Request' => auth_params(auth).merge(locals)}}

# Run the request
response = super(operation_name, params, &block)
Response.new(response)
end

private
def auth_params
{'SourceCredentials'=>{'SourceName'=>MindBody.configuration.source_name,
'Password'=>MindBody.configuration.source_key,
'SiteIDs'=>{'int'=>MindBody.configuration.site_ids}}}
def auth_params(auth = nil)
auth ||=
{
:source_name => MindBody.configuration.source_name,
:source_key => MindBody.configuration.source_key,
:username => MindBody.configuration.username,
:password => MindBody.configuration.password,

:site_ids => MindBody.configuration.site_ids
}

params =
{
'SourceCredentials' => {
'SourceName' => auth[:source_name],
'Password' => auth[:source_key],
'SiteIDs' => {
'int' => auth[:site_ids]
}
}
}

if (auth[:username] && auth[:password])
params['UserCredentials'] =
{
'Username' => auth[:username],
'Password' => auth[:password],
'SiteIDs' => {
'int' => auth[:site_ids]
}
}
end

params.merge!({"Test" => true}) unless MindBody.configuration.prod_mode
params
end

def fixup_locals(locals)
Expand Down
20 changes: 20 additions & 0 deletions lib/mindbody-api/ext/savon_ext.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,23 @@ def #{operation.to_s.snakecase}(#{params.join(',')})
.join(',')}
}
locals ||= {}

client.call #{operation.inspect}, :message => locals.merge(req_hash)
end
RUBY_EVAL

class_operation_module.module_eval <<-RUBY_EVAL, __FILE__, __LINE__+1
def #{operation.to_s.snakecase}_with_auth(auth#{!params.empty? ? ',' : ''} #{params.join(',')})
req_hash = {
#{params.select{|p| p != 'locals = {}'}
.map{|p| "'#{params_key(p)}' => #{p}"}
.join(',')}
}
locals ||= {}

client.call #{operation.inspect}, :auth => auth, :message => locals.merge(req_hash)
end
RUBY_EVAL
end

# Defines an instance-level SOAP operation.
Expand All @@ -31,6 +45,12 @@ def #{operation.to_s.snakecase}(#{params.join(',')})
self.class.#{operation.to_s.snakecase} #{params.join(',').chomp(' = {}')}
end
RUBY_EVAL

instance_operation_module.module_eval <<-RUBY_EVAL, __FILE__, __LINE__+1
def #{operation.to_s.snakecase}_with_auth(auth#{!params.empty? ? ',' : ''} #{params.join(',')})
self.class.#{operation.to_s.snakecase}_with_auth(auth, #{params.join(',').chomp(' = {}')})
end
RUBY_EVAL
end

# Builds the array of params the
Expand Down
3 changes: 2 additions & 1 deletion lib/mindbody-api/models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class Base
end
end

require 'mindbody-api/models/resource'
require 'mindbody-api/models/location'
require 'mindbody-api/models/client'
require 'mindbody-api/models/schedule_type'
Expand All @@ -29,6 +30,6 @@ class Base
require 'mindbody-api/models/sale'
require 'mindbody-api/models/sale_item'
require 'mindbody-api/models/service'
require 'mindbody-api/models/resource'
require 'mindbody-api/models/appointment_status'
require 'mindbody-api/models/appointment'
require 'mindbody-api/models/waitlist_entry'
1 change: 1 addition & 0 deletions lib/mindbody-api/models/class.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class Class < Base
attribute :end_date_time, DateTime
attribute :class_description, ClassDescription
attribute :staff, Staff
attribute :resource, Resource

def name
class_description.name
Expand Down
5 changes: 5 additions & 0 deletions lib/mindbody-api/models/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class Client < Base
attribute :first_name, String
attribute :last_name, String
attribute :gender, String
attribute :account_balance, Float
attribute :email, String
attribute :email_opt_in, Boolean
attribute :address_line1, String
Expand All @@ -23,6 +24,10 @@ class Client < Base
attribute :photo_url, String
attribute :username, String
attribute :first_appointment_date, DateTime
attribute :emergency_contact_info_name, String
attribute :emergency_contact_info_relationship, String
attribute :emergency_contact_info_phone, String
attribute :emergency_contact_info_email, String

def name
"#{first_name} #{last_name}"
Expand Down
1 change: 1 addition & 0 deletions lib/mindbody-api/models/visit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Visit < Base
attribute :web_signup, Boolean
attribute :signed_in, Boolean
attribute :make_up, Boolean
attribute :late_cancelled, Boolean
attribute :service, ClientService
end
end
Expand Down
11 changes: 11 additions & 0 deletions lib/mindbody-api/models/waitlist_entry.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module MindBody
module Models
class WaitlistEntry < Base
attribute :id, Integer
attribute :class_id, Integer
attribute :class_date, DateTime
attribute :client, Client
attribute :class_schedule, ClassSchedule
end
end
end
4 changes: 3 additions & 1 deletion lib/mindbody-api/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,11 @@ def normalize_hash(hash)
hash[key] = normalize_hash(value)

key_singular = map_key(key).singularize.to_sym
key_plural = key.to_s.pluralize.to_sym

if value.has_key?(key_singular)
hash[key] = value[key_singular]
val = value[key_singular]
hash[key] = (val.is_a?(Array)) ? val : [val]
elsif value.has_key?(:string)
hash[key] = value[:string]
elsif value.has_key?(:int)
Expand Down
5 changes: 4 additions & 1 deletion lib/mindbody-api/services/class_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ class ClassService < Service
service "ClassService"

operation :get_classes
operation :get_class_visits, required:[:class_id]
operation :get_class_visits, required:[:class_id]
operation :get_class_descriptions
operation :get_class_schedules
operation :get_waitlist_entries, required: [:class_ids]
operation :add_clients_to_classes, required: [:class_ids, :client_ids]
operation :remove_clients_from_classes, required: [:class_ids, :client_ids]
end
end
end
1 change: 1 addition & 0 deletions lib/mindbody-api/services/sale_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class SaleService < Service

operation :get_accepted_card_type, :locals => false
operation :get_sales
operation :checkout_shopping_cart, :required => [:client_id, :cart_items, :payments]
operation :get_services
operation :get_packages
operation :get_products
Expand Down
1 change: 1 addition & 0 deletions mindbody-api.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ Gem::Specification.new do |gem|
gem.add_development_dependency 'simplecov', '~> 0.8.2'
gem.add_development_dependency 'guard-rspec', '~> 4.2.8'
gem.add_development_dependency 'rake', '~> 10.0.3'
gem.add_development_dependency 'pry'
end
57 changes: 57 additions & 0 deletions spec/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
creds.stub(:source_name).and_return('test')
creds.stub(:source_key).and_return('test_key')
creds.stub(:site_ids).and_return([-99])
creds.stub(:open_timeout).and_return(nil)
creds.stub(:read_timeout).and_return(nil)
creds.stub(:username).and_return(nil)
creds.stub(:password).and_return(nil)
creds.stub(:prod_mode).and_return(true)
MindBody.stub(:configuration).and_return(creds)
@client = MindBody::Services::Client.new(:wsdl => 'spec/fixtures/wsdl/geotrust.wsdl')

Expand All @@ -30,6 +35,58 @@
}
}}}
end

# We use #send here because `auth_params` is a private method and when
# adding this functionality I didn't want to change the public API contract.
# This should be considered an external-facing unit test as calls to
# {#call} now respect `auth:` alongside `:message`.
describe " (when using `auth:`)" do
it 'should inject MindBody.configuration values for the nil case' do
ret = subject.send(:auth_params, nil)

expect(ret["SourceCredentials"]["SourceName"]).to eq('test')
expect(ret["SourceCredentials"]["Password"]).to eq('test_key')
expect(ret["SourceCredentials"]["SiteIDs"]["int"]).to eq([-99])
expect(ret["UserCredentials"]).to be_nil
end

it 'should defer to passed-in values when provided' do
auth = {
:source_name => "override_name",
:source_key => "override_key",
:site_ids => [ -105 ]
}

ret = subject.send(:auth_params, auth)

expect(ret["SourceCredentials"]["SourceName"]).to eq('override_name')
expect(ret["SourceCredentials"]["Password"]).to eq('override_key')
expect(ret["SourceCredentials"]["SiteIDs"]["int"]).to eq([-105])
expect(ret["UserCredentials"]).to be_nil
end

it 'should inject UserCredentials when provided' do
auth = {
:source_name => "override_name",
:source_key => "override_key",

:username => "override_user",
:password => "override_pass",

:site_ids => [ -105 ]
}

ret = subject.send(:auth_params, auth)

expect(ret["SourceCredentials"]["SourceName"]).to eq('override_name')
expect(ret["SourceCredentials"]["Password"]).to eq('override_key')
expect(ret["SourceCredentials"]["SiteIDs"]["int"]).to eq([-105])
expect(ret["UserCredentials"]["Username"]).to eq('override_user')
expect(ret["UserCredentials"]["Password"]).to eq('override_pass')
expect(ret["UserCredentials"]["SiteIDs"]["int"]).to eq([-105])
end
end

it 'should inject the auth params' do
Savon::Operation.any_instance.should_receive(:call).once.with(@locals)
subject.call(:hello)
Expand Down
38 changes: 31 additions & 7 deletions spec/service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,29 @@
MindBody::Services::Service.operation :test
end

it { should respond_to(:test) }
its(:new) { should respond_to(:test) }
it do
should respond_to(:test)
should respond_to(:test_with_auth)
end
its(:new) do
should respond_to(:test)
should respond_to(:test_with_auth)
end

it 'should have optional locals' do
expect(subject.method(:test).arity).to eql(-1)
expect(subject.method(:test_with_auth).arity).to eql(-2)
end

it 'should delegate to the class method' do
instance = subject.new
locals = {:foo => :bar}

subject.should_receive(:test).with(locals).once

instance.test(locals)

subject.should_receive(:test_with_auth).with({}, locals).once
instance.test_with_auth({}, locals)
end
end

Expand All @@ -52,23 +61,33 @@
MindBody::Services::Service.operation :test_required, :required => [:foo, :bar]
end

it { should respond_to(:test_required) }
its(:new) { should respond_to(:test_required) }
it do
should respond_to(:test_required)
should respond_to(:test_required_with_auth)
end
its(:new) do
should respond_to(:test_required)
should respond_to(:test_required_with_auth)
end

it 'should have two required params and optional locals' do
expect(subject.method(:test_required).arity).to eql(-3)
expect(subject.method(:test_required_with_auth).arity).to eql(-4)
end

it 'should delegate to the class method' do
instance = subject.new
locals = {:foo => :bar}

subject.should_receive(:test_required).with(:foo, :bar, locals).once

instance.test_required(:foo, :bar, locals)

subject.should_receive(:test_required_with_auth).with({}, :foo, :bar, locals).once
instance.test_required_with_auth({}, :foo, :bar, locals)
end

it 'should require two params' do
expect{subject.send(:test_required_with_auth, {})}.to raise_error(ArgumentError)
expect{subject.send(:test_required)}.to raise_error(ArgumentError)
end
end
Expand All @@ -83,21 +102,26 @@

it 'should have two required params and optional locals' do
expect(subject.method(:test_no_locals).arity).to eql(2)
expect(subject.method(:test_no_locals_with_auth).arity).to eql(3)
end

it 'should delegate to the class method' do
instance = subject.new

subject.should_receive(:test_no_locals).with(:foo, :bar).once

instance.test_no_locals(:foo, :bar)

subject.should_receive(:test_no_locals_with_auth).with({}, :foo, :bar).once
instance.test_no_locals_with_auth({}, :foo, :bar)
end

it 'should require two params' do
expect{subject.send(:test_no_locals_with_auth, {})}.to raise_error(ArgumentError)
expect{subject.send(:test_no_locals)}.to raise_error(ArgumentError)
end

it 'should not allow locals' do
expect{subject.send(:test_no_locals_with_auth, {}, :foo, :bar, :foobar => 'foobar')}.to raise_error(ArgumentError)
expect{subject.send(:test_no_locals, :foo, :bar, :foobar => 'foobar')}.to raise_error(ArgumentError)
end
end
Expand Down