├── VERSION ├── .ruby-version ├── spec ├── fixtures │ ├── empty.json │ ├── generic_delete.json │ ├── notifications │ │ ├── mark_read.json │ │ ├── post.json │ │ ├── unread.json │ │ ├── new_empty.json │ │ ├── new.json │ │ └── notifications.json │ ├── oauth2 │ │ ├── error.json │ │ ├── access.json │ │ ├── access_with_refresh.json │ │ ├── access_with_old_refresh.json │ │ ├── password_body.json │ │ ├── authorization_code_body.json │ │ └── refresh_body.json │ ├── oauth2_error.json │ ├── accounts │ │ ├── my_put.json │ │ ├── my_save.json │ │ ├── password_save.json │ │ └── my_portal.json │ ├── notes │ │ ├── new.json │ │ ├── agent_shared_empty.json │ │ ├── add.json │ │ └── agent_shared.json │ ├── listing_carts │ │ ├── empty.json │ │ ├── put_name.json │ │ ├── add_listing_post.json │ │ ├── add_portal_cart_listings_post.json │ │ ├── add_listings_post.json │ │ ├── post.json │ │ ├── put.json │ │ ├── put_ids.json │ │ ├── new_portal_cart.json │ │ ├── post_portal_cart.json │ │ ├── new.json │ │ ├── add_listing.json │ │ ├── remove_listing.json │ │ ├── add_listings.json │ │ ├── add_portal_cart_listings.json │ │ ├── listing_cart.json │ │ └── listing_portal_cart.json │ ├── success.json │ ├── generic_failure.json │ ├── listings │ │ ├── put.json │ │ ├── photos │ │ │ ├── rollback.json │ │ │ ├── rotate.json │ │ │ ├── batch_delete.json │ │ │ └── post.json │ │ ├── put_reorder_photo.json │ │ ├── put_expiration_date.json │ │ ├── shared_listing_new.json │ │ ├── floplans_index.json │ │ ├── shared_listing_post.json │ │ ├── constraints.json │ │ ├── shared_listing_get.json │ │ ├── rental_calendar.json │ │ ├── constraints_with_pagination.json │ │ ├── open_houses.json │ │ ├── document_index.json │ │ ├── tour_of_homes.json │ │ ├── reorder_photo.json │ │ ├── videos_index.json │ │ ├── no_subresources.json │ │ ├── virtual_tours_index.json │ │ ├── with_permissions.json │ │ └── with_vtour.json │ ├── messages │ │ ├── post.json │ │ ├── new_empty.json │ │ ├── count.json │ │ ├── new.json │ │ └── new_with_recipients.json │ ├── portal │ │ ├── disable.json │ │ ├── enable.json │ │ ├── my_non_existant.json │ │ ├── new.json │ │ ├── post.json │ │ └── my.json │ ├── errors │ │ ├── failure.json │ │ ├── expired.json │ │ ├── failure_with_msg.json │ │ └── failure_with_constraint.json │ ├── contacts │ │ ├── new_empty.json │ │ ├── vow_accounts │ │ │ ├── edit.json │ │ │ ├── post.json │ │ │ ├── new.json │ │ │ └── get.json │ │ ├── new.json │ │ ├── post.json │ │ ├── new_notify.json │ │ ├── tags.json │ │ ├── my.json │ │ └── contacts.json │ ├── logo_fbs.png │ ├── no_results.json │ ├── saved_searches │ │ ├── update.json │ │ ├── new.json │ │ ├── post.json │ │ ├── without_newsfeed.json │ │ ├── get.json │ │ ├── get_provided.json │ │ ├── with_newsfeed.json │ │ └── with_inactive_newsfeed.json │ ├── comments │ │ ├── new.json │ │ ├── post.json │ │ └── get.json │ ├── authentication_failure.json │ ├── sharedlinks │ │ └── success.json │ ├── count.json │ ├── session.json │ ├── finders.json │ ├── fields │ │ ├── settings.json │ │ ├── order.json │ │ └── order_a.json │ ├── base.json │ ├── idx_links │ │ └── get.json │ ├── property_types │ │ └── property_types.json │ ├── activities │ │ └── get.json │ ├── newsfeeds │ │ ├── get.json │ │ └── inactive.json │ ├── sorts │ │ └── get.json │ ├── standardfields │ │ ├── stateorprovince.json │ │ └── nearby.json │ ├── rules │ │ └── get.json │ ├── listing_meta_translations │ │ └── get.json │ └── search_templates │ │ └── quick_searches │ │ └── get.json ├── unit │ ├── spark_api │ │ ├── models │ │ │ ├── connect_prefs_spec.rb │ │ │ ├── rule_spec.rb │ │ │ ├── sort_spec.rb │ │ │ ├── constraint_spec.rb │ │ │ ├── email_link_spec.rb │ │ │ ├── newsfeed_spec.rb │ │ │ ├── floplan_spec.rb │ │ │ ├── activity_spec.rb │ │ │ ├── document_spec.rb │ │ │ ├── search_template │ │ │ │ └── quick_search_spec.rb │ │ │ ├── property_types_spec.rb │ │ │ ├── open_house_spec.rb │ │ │ ├── listing_meta_translations_spec.rb │ │ │ ├── rental_calendar_spec.rb │ │ │ ├── video_spec.rb │ │ │ ├── concerns │ │ │ │ └── destroyable_spec.rb │ │ │ ├── tour_of_home_spec.rb │ │ │ ├── finders_spec.rb │ │ │ ├── virtual_tour_spec.rb │ │ │ ├── portal_spec.rb │ │ │ ├── account_report_spec.rb │ │ │ ├── shared_listing_spec.rb │ │ │ ├── defaultable_spec.rb │ │ │ ├── dirty_spec.rb │ │ │ ├── standard_fields_spec.rb │ │ │ └── fields_spec.rb │ │ ├── authentication │ │ │ ├── oauth2_impl │ │ │ │ ├── single_session_provider_spec.rb │ │ │ │ ├── grant_type_base_spec.rb │ │ │ │ └── faraday_middleware_spec.rb │ │ │ └── base_auth_spec.rb │ │ ├── options_hash_spec.rb │ │ ├── authentication_spec.rb │ │ └── primary_array_spec.rb │ └── spark_api_spec.rb ├── support │ ├── search_container_shared_examples.rb │ ├── account_examples.rb │ ├── mock_helper.rb │ ├── oauth2_helper.rb │ └── json_helper.rb ├── config │ └── spark_api │ │ ├── test_key.yml │ │ ├── test_single_session_oauth.yml │ │ └── test_oauth.yml ├── formatters │ └── api_support_formatter.rb └── spec_helper.rb ├── script ├── release ├── bootstrap ├── console ├── ci_build ├── access_token_example.rb ├── spark_auth_example.rb ├── example.rb ├── combined_flow_example.rb └── oauth2_example.rb ├── lib ├── spark_api │ ├── version.rb │ ├── models │ │ ├── concerns.rb │ │ ├── idx.rb │ │ ├── system_info_search.rb │ │ ├── activity.rb │ │ ├── document.rb │ │ ├── sort.rb │ │ ├── comment.rb │ │ ├── connect_prefs.rb │ │ ├── account_roster.rb │ │ ├── portal_listing_cart.rb │ │ ├── property_types.rb │ │ ├── incomplete_listing.rb │ │ ├── idx_link.rb │ │ ├── custom_fields.rb │ │ ├── rule.rb │ │ ├── search_template │ │ │ └── quick_search.rb │ │ ├── newsfeed.rb │ │ ├── system_info.rb │ │ ├── tour_of_home.rb │ │ ├── email_link.rb │ │ ├── fields.rb │ │ ├── listing_meta_translations.rb │ │ ├── open_house.rb │ │ ├── constraint.rb │ │ ├── floplan.rb │ │ ├── account_report.rb │ │ ├── rental_calendar.rb │ │ ├── virtual_tour.rb │ │ ├── media.rb │ │ ├── portal.rb │ │ ├── market_statistics.rb │ │ ├── defaultable.rb │ │ ├── shared_listing.rb │ │ ├── news_feed_meta.rb │ │ ├── message.rb │ │ ├── note.rb │ │ ├── vow_account.rb │ │ ├── concerns │ │ │ ├── destroyable.rb │ │ │ └── savable.rb │ │ ├── finders.rb │ │ ├── shared_link.rb │ │ ├── dirty.rb │ │ ├── notification.rb │ │ ├── standard_fields.rb │ │ └── subresource.rb │ ├── authentication │ │ ├── oauth2_impl │ │ │ ├── simple_provider.rb │ │ │ ├── single_session_provider.rb │ │ │ ├── grant_type_refresh.rb │ │ │ ├── grant_type_password.rb │ │ │ └── grant_type_code.rb │ │ └── base_auth.rb │ ├── cli │ │ ├── api_auth.rb │ │ ├── oauth2.rb │ │ └── setup.rb │ ├── options_hash.rb │ ├── reso_faraday_middleware.rb │ ├── primary_array.rb │ ├── configuration │ │ └── oauth2_configurable.rb │ ├── client.rb │ ├── response.rb │ ├── authentication.rb │ ├── errors.rb │ ├── connection.rb │ ├── models.rb │ └── multi_client.rb └── spark_api.rb ├── bin └── spark_api ├── Guardfile ├── .gitignore ├── Gemfile ├── LICENSE ├── Rakefile ├── History.txt ├── spark_api.gemspec └── .github └── workflows └── ci.yml /VERSION: -------------------------------------------------------------------------------- 1 | 2.0.0 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2.3 2 | -------------------------------------------------------------------------------- /spec/fixtures/empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { } 3 | } -------------------------------------------------------------------------------- /spec/fixtures/generic_delete.json: -------------------------------------------------------------------------------- 1 | {"D": {"Success": true}} 2 | -------------------------------------------------------------------------------- /spec/fixtures/notifications/mark_read.json: -------------------------------------------------------------------------------- 1 | {"D":{"Read":true}} -------------------------------------------------------------------------------- /spec/fixtures/oauth2/error.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": "invalid_request" 3 | } -------------------------------------------------------------------------------- /spec/fixtures/oauth2_error.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": "invalid_request" 3 | } -------------------------------------------------------------------------------- /spec/fixtures/oauth2/access.json: -------------------------------------------------------------------------------- 1 | { 2 | "access_token": "04u7h-4cc355-70k3n" 3 | } -------------------------------------------------------------------------------- /spec/fixtures/accounts/my_put.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /spec/fixtures/notes/new.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Note": "lorem ipsum dolor" 4 | } 5 | } -------------------------------------------------------------------------------- /spec/fixtures/listing_carts/empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "ListingCarts": [{}] 4 | } 5 | } -------------------------------------------------------------------------------- /spec/fixtures/success.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /spec/fixtures/generic_failure.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": false 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /spec/fixtures/listings/put.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "ListPrice": 10000.0 4 | } 5 | } -------------------------------------------------------------------------------- /spec/fixtures/messages/post.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /spec/fixtures/portal/disable.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Enabled": false 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /spec/fixtures/portal/enable.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Enabled": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /spec/fixtures/accounts/my_save.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "GetEmailUpdates": true 4 | } 5 | } -------------------------------------------------------------------------------- /spec/fixtures/errors/failure.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": false 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /spec/fixtures/listings/photos/rollback.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Version": 1 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /spec/fixtures/messages/new_empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Messages": [ 4 | { } 5 | ] 6 | } 7 | } -------------------------------------------------------------------------------- /spec/fixtures/contacts/new_empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Contacts": [ 4 | { } 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /spec/fixtures/logo_fbs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparkapi/spark_api/HEAD/spec/fixtures/logo_fbs.png -------------------------------------------------------------------------------- /spec/fixtures/listing_carts/put_name.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Name": "My Cart's Name" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /spec/fixtures/contacts/vow_accounts/edit.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "LoginName": "Johnny Newman" 4 | } 5 | } -------------------------------------------------------------------------------- /spec/fixtures/listings/put_reorder_photo.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Photos": [{"Order" : "2"}] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /spec/fixtures/no_results.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /spec/fixtures/listings/put_expiration_date.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "ExpirationDate": "2011-10-04" 4 | } 5 | } -------------------------------------------------------------------------------- /spec/fixtures/saved_searches/update.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Name": "A new search name here" 4 | } 5 | } 6 | 7 | -------------------------------------------------------------------------------- /spec/fixtures/portal/my_non_existant.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /spec/fixtures/listings/photos/rotate.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Photos": [ 4 | {"Rotate": "clockwise"} 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /spec/fixtures/accounts/password_save.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "PasswordValidation":"1", 4 | "Password":"1" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /spec/fixtures/listing_carts/add_listing_post.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "ListingIds": [ "20110621133454434543000000" ] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /spec/fixtures/notes/agent_shared_empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Results": [ 4 | ], 5 | "Success": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /script/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | rm -rf pkg/* 5 | bundle exec rake build 6 | gem push pkg/*.gem --key rubygems_production_key 7 | -------------------------------------------------------------------------------- /lib/spark_api/version.rb: -------------------------------------------------------------------------------- 1 | # Gem version information 2 | module SparkApi 3 | VERSION = File.read(File.dirname(__FILE__) + "/../../VERSION").chomp 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/listing_carts/add_portal_cart_listings_post.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "ListingIds": [ "20110621133454434543000000" ] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /spec/fixtures/comments/new.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Comments": [ 4 | { "Comment": "This is a comment." } 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /spec/fixtures/oauth2/access_with_refresh.json: -------------------------------------------------------------------------------- 1 | { 2 | "expires_in":86384, 3 | "refresh_token":"r3fr35h-70k3n", 4 | "access_token": "04u7h-4cc355-70k3n" 5 | } -------------------------------------------------------------------------------- /spec/fixtures/errors/expired.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": false, 4 | "Message": "Session token has expired", 5 | "Code": 1020 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /spec/fixtures/listing_carts/add_listings_post.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "ListingIds": [ "20110621133454434543000000", "20110621133454434543000001" ] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /spec/fixtures/oauth2/access_with_old_refresh.json: -------------------------------------------------------------------------------- 1 | { 2 | "expires_in":0, 3 | "refresh_token":"0ld-r3fr35h-70k3n", 4 | "access_token": "0ld-04u7h-4cc355-70k3n" 5 | } -------------------------------------------------------------------------------- /spec/fixtures/saved_searches/new.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "SavedSearches": [ 4 | { "Name": "A new search name here" } 5 | ] 6 | } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /bin/spark_api: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # File: bin/spark_api 3 | 4 | require File.dirname(__FILE__) + '/../lib/spark_api/cli' 5 | 6 | SparkApi::CLI::ConsoleCLI.execute(STDOUT, ARGV) 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/spark_api/models/concerns.rb: -------------------------------------------------------------------------------- 1 | require 'spark_api/models/concerns/savable' 2 | require 'spark_api/models/concerns/destroyable' 3 | 4 | module SparkApi 5 | module Models 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ruby -v 4 | echo "==> Installing gems..." 5 | bundle config set path 'vendor/bundle' 6 | bundle check 2>&1 > /dev/null || { 7 | bundle install 8 | } 9 | -------------------------------------------------------------------------------- /spec/fixtures/authentication_failure.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": false, 4 | "Message": "Invalid API key and/or request signed improperly", 5 | "Code": 1000 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /spec/fixtures/portal/new.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "DisplayName": "GreatPortal", 4 | "Enabled": true, 5 | "RequiredFields": ["Address", "Phone"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /spec/fixtures/portal/post.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "ResourceUri":"/v1/portal/20100912153422758914000000" 7 | } 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /spec/fixtures/contacts/new.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Contacts": [ 4 | { 5 | "DisplayName": "Contact Four", 6 | "PrimaryEmail": "contact4@fbsdata.com" 7 | } 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /spec/fixtures/oauth2/password_body.json: -------------------------------------------------------------------------------- 1 | { 2 | "client_secret":"example-password", 3 | "client_id":"example-id", 4 | "grant_type":"password", 5 | "username":"angry_dev", 6 | "password":"nerds" 7 | } 8 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | 2 | guard :rspec, cmd: 'rspec' do 3 | watch(%r{^spec/.+_spec\.rb$}) 4 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/unit/#{m[1]}_spec.rb" } 5 | watch('spec/spec_helper.rb') { "spec" } 6 | end 7 | -------------------------------------------------------------------------------- /lib/spark_api/models/idx.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class Idx < Base 4 | 5 | extend Finders 6 | 7 | self.element_name = "idx" 8 | 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/fixtures/sharedlinks/success.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "ResourceUri": "/sharedlinks/YUNO" 7 | } 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/spark_api/models/system_info_search.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class SystemInfoSearch < Base 4 | 5 | self.element_name="system/search" 6 | 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/fixtures/contacts/post.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "ResourceUri":"/v1/contacts/20101230223226074204000000" 7 | } 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /spec/fixtures/errors/failure_with_msg.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Message": "The e-mail address 'bhornseth@fbsdata.com' belongs to another prospect", 4 | "Success": false, 5 | "Code": 1055 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /spec/fixtures/listing_carts/post.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "ResourceUri":"/v1/listingcarts/20100912153422758914000000" 7 | } 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/spark_api/models/activity.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | 4 | class Activity < Base 5 | extend Finders 6 | self.element_name = "activities" 7 | end 8 | 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /script/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # File: script/console 3 | 4 | require File.dirname(__FILE__) + '/../lib/spark_api/cli' 5 | ENV["SPARK_API_CONSOLE"] = "true" 6 | SparkApi::CLI::ConsoleCLI.execute(STDOUT, ARGV) 7 | -------------------------------------------------------------------------------- /spec/fixtures/contacts/vow_accounts/post.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "ResourceUri":"/v1/contacts/20090928182824338901000000/portal" 7 | } 8 | ] 9 | } 10 | } -------------------------------------------------------------------------------- /spec/unit/spark_api/models/connect_prefs_spec.rb: -------------------------------------------------------------------------------- 1 | require './spec/spec_helper' 2 | 3 | describe Connect do 4 | 5 | it "should respond to prefs" do 6 | expect(Connect).to respond_to(:prefs) 7 | end 8 | 9 | end 10 | -------------------------------------------------------------------------------- /lib/spark_api/models/document.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class Document < Base 4 | extend Subresource 5 | 6 | self.element_name="documents" 7 | 8 | 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/fixtures/contacts/new_notify.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Contacts": [ 4 | { 5 | "DisplayName": "Contact Four", 6 | "PrimaryEmail": "contact4@fbsdata.com" 7 | } 8 | ], 9 | "Notify": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /spec/fixtures/count.json: -------------------------------------------------------------------------------- 1 | { "D": 2 | { "Success": true, 3 | "Pagination": { 4 | "TotalPages": 81, 5 | "PageSize": 25, 6 | "TotalRows": 2001, 7 | "CurrentPage": 1 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /spec/fixtures/listings/shared_listing_new.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "ListingIds": [ 4 | "20110224152431857619000000", 5 | "20110125122333785431000000" 6 | ], 7 | "ViewId": "20080125122333787615000000" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /spec/fixtures/notifications/post.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "ResourceUri": "/notifications" 7 | } 8 | ] 9 | } 10 | } -------------------------------------------------------------------------------- /spec/fixtures/notifications/unread.json: -------------------------------------------------------------------------------- 1 | { "D": 2 | { "Success": true, 3 | "Pagination": { 4 | "PageSize": 1, 5 | "CurrentPage": 1, 6 | "TotalPages": 1, 7 | "TotalRows": 30 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /lib/spark_api/models/sort.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class Sort < Base 4 | extend Finders 5 | 6 | self.element_name="/sorts" 7 | self.prefix="/searchtemplates" 8 | 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/spark_api/models/comment.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class Comment < Base 4 | include Concerns::Savable, 5 | Concerns::Destroyable 6 | self.element_name = "comments" 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/fixtures/listing_carts/put.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "ListingIds": [ 4 | "20110112234857732941000000", 5 | "20110302120238448431000000", 6 | "20110510011212354751000000" 7 | ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /spec/fixtures/oauth2/authorization_code_body.json: -------------------------------------------------------------------------------- 1 | { 2 | "code":"my_code", 3 | "client_secret":"example-password", 4 | "client_id":"example-id", 5 | "redirect_uri":"https://exampleapp.fbsdata.com/oauth-callback", 6 | "grant_type":"authorization_code" 7 | } -------------------------------------------------------------------------------- /lib/spark_api/models/connect_prefs.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class Connect < Base 4 | self.element_name="connect" 5 | def self.prefs 6 | connection.get("#{path}/prefs") 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/fixtures/listing_carts/put_ids.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "ListingIds": [ 4 | "20110112234857732941000000", 5 | "20110302120238448431000000", 6 | "20110510011212354751000000" 7 | ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /spec/fixtures/messages/count.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [], 5 | "Pagination": { 6 | "TotalRows": 78, 7 | "PageSize": 25, 8 | "TotalPages": 4, 9 | "CurrentPage": 1 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /spec/fixtures/oauth2/refresh_body.json: -------------------------------------------------------------------------------- 1 | { 2 | "client_id":"example-id", 3 | "client_secret":"example-password", 4 | "refresh_token":"example-token", 5 | "grant_type":"refresh_token", 6 | "redirect_uri":"https://exampleapp.fbsdata.com/oauth-callback", 7 | } 8 | -------------------------------------------------------------------------------- /spec/fixtures/contacts/vow_accounts/new.json: -------------------------------------------------------------------------------- 1 | { 2 | "D":{ 3 | "LoginName":"Johnny Everyman", 4 | "Password":"MyPassw0rd", 5 | "Settings":{ 6 | "Enabled":"true" 7 | }, 8 | "Locale":{ 9 | "Language":"en" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /spec/fixtures/listing_carts/new_portal_cart.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "ListingCarts": [{ 4 | "Name": "Non-existent Portal Cart", 5 | "ListingIds": [ 6 | "20110112234857732941000000" 7 | ] 8 | }] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /spec/fixtures/listing_carts/post_portal_cart.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "ResourceUri":"/v1/contacts/20090928182824338901000000/listingcarts/20100912153422758914000000" 7 | } 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /spec/fixtures/listings/photos/batch_delete.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Results": [ 4 | { 5 | "Id": "20060725225010201442000000,20060918191319441822000000" 6 | } 7 | ], 8 | "Success": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | log/ 3 | ._* 4 | .DEV* 5 | spec/reports/ 6 | test/reports 7 | .bundle/ruby 8 | .bundle/jruby 9 | coverage 10 | bundle 11 | vendor/cache/ 12 | Gemfile.lock 13 | pkg/ 14 | doc/ 15 | config/ 16 | tmp/* 17 | .buildpath 18 | .project 19 | *.swp 20 | .bundle 21 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Refer to the flexmls_api.gemspec or Rakefile for dependency information 4 | gemspec :development_group => :test 5 | 6 | platforms :ruby do 7 | gem 'yajl-ruby' 8 | end 9 | 10 | platforms :jruby do 11 | gem 'jruby-openssl' 12 | end 13 | -------------------------------------------------------------------------------- /spec/fixtures/notifications/new_empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Message": "Your PDF generation has completed!", 4 | "BrowserUri": "http://myapplication.com/cmas/19581825.pdf", 5 | "ResourceUri": "http://myapplication.com/cmas/19581825.json" 6 | } 7 | } -------------------------------------------------------------------------------- /lib/spark_api/models/account_roster.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class AccountRoster < Account 4 | def self.roster(account_id, arguments={}) 5 | collect(connection.get("/accounts/#{account_id}/roster", arguments)).first 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/fixtures/session.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Results": [{ 4 | "Roles": ["basic"], 5 | "Expires": "2110-12-30T11:18:49-06:00", 6 | "AuthToken": "c401736bf3d3f754f07c04e460e09573" 7 | }], 8 | "Success": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/spark_api/models/portal_listing_cart.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | 4 | class PortalListingCart < ListingCart 5 | 6 | def self.find(contact_id, *arguments) 7 | @contact_id = contact_id 8 | super(*arguments) 9 | end 10 | 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/spark_api/models/property_types.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class PropertyTypes < Base 4 | self.element_name="propertytypes" 5 | 6 | def self.all(options={}) 7 | collect(connection.get("#{path}/all", options)) 8 | end 9 | 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /script/ci_build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | script/bootstrap 5 | 6 | 7 | export COVERAGE=on # build rcov report when running tests 8 | export CI_REPORTS=test/reports # output JUnit reports to std location 9 | 10 | echo "==> Running tests..." 11 | bundle exec rake ci:setup:rspec spec 12 | -------------------------------------------------------------------------------- /lib/spark_api/models/incomplete_listing.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class IncompleteListing < Base 4 | 5 | extend Finders 6 | 7 | include Concerns::Savable 8 | include Concerns::Destroyable 9 | 10 | self.element_name = "listings/incomplete" 11 | 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/fixtures/listing_carts/new.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "ListingCarts": [{ 4 | "ListingIds": [ 5 | "20110112234857732941000000", 6 | "20110302120238448431000000", 7 | "20110510011212354751000000" 8 | ], 9 | "Name": "My Cart's Name" 10 | }] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /spec/fixtures/notifications/new.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Type": "OperationComplete", 4 | "Message": "Your PDF generation has completed!", 5 | "BrowserUri": "http://myapplication.com/cmas/19581825.pdf", 6 | "ResourceUri": "http://myapplication.com/cmas/19581825.json" 7 | } 8 | } -------------------------------------------------------------------------------- /lib/spark_api/models/idx_link.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class IdxLink < Base 4 | 5 | extend Finders 6 | include Defaultable 7 | 8 | self.element_name = "idxlinks" 9 | 10 | LINK_TYPES = ["QuickSearch", "SavedSearch", "MyListings", "Roster"] 11 | 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/fixtures/saved_searches/post.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "ResourceUri": "/v1/savedsearches/20100815220615294367000000", 7 | "Id": "20100815220615294367000000", 8 | "Name": "A new search name here" 9 | } 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/spark_api/models/custom_fields.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class CustomFields < Base 4 | self.element_name="customfields" 5 | 6 | 7 | def self.find_by_property_type(card_fmt, arguments={}) 8 | collect(connection.get("#{self.path}/#{card_fmt}", arguments)) 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/spark_api/models/rule.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class Rule < Base 4 | 5 | self.element_name="listings/rules" 6 | 7 | def self.for_property_type(property_type, args={}) 8 | collect(connection.get("/listings/rules/propertytypes/#{property_type}", args)) 9 | end 10 | 11 | end 12 | end 13 | end 14 | 15 | -------------------------------------------------------------------------------- /lib/spark_api/models/search_template/quick_search.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class QuickSearch < Base 4 | extend Finders 5 | include Defaultable 6 | include Concerns::Savable, 7 | Concerns::Destroyable 8 | 9 | self.element_name="searchtemplates/quicksearches" 10 | 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/fixtures/notes/add.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Results": [ 4 | { 5 | "ResourceUri": "/v1/listings/20100909200152674436000000/shared/notes/contacts/20110407212043616271000000/", 6 | "Note": "lorem ipsum dolor sit amet" 7 | } 8 | ], 9 | "Success": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /spec/fixtures/notes/agent_shared.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Results": [ 4 | { 5 | "ResourceUri": "/v1/listings/20100909200152674436000000/shared/notes/contacts/20110407212043616271000000/", 6 | "Note": "lorem ipsum dolor sit amet" 7 | } 8 | ], 9 | "Success": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/spark_api/models/newsfeed.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class Newsfeed < Base 4 | 5 | extend Finders 6 | include Concerns::Destroyable, 7 | Concerns::Savable 8 | 9 | self.element_name = 'newsfeeds' 10 | 11 | def listing_search_role 12 | :public 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/fixtures/finders.json: -------------------------------------------------------------------------------- 1 | {"D": { 2 | "Success": true, 3 | "Results": [{ 4 | "Id": 1, 5 | "Name": "My Example", 6 | "Test": true 7 | }, 8 | { 9 | "Id": 2, 10 | "Name": "My Example2", 11 | "Test": false 12 | }]} 13 | } 14 | 15 | -------------------------------------------------------------------------------- /spec/fixtures/listings/floplans_index.json: -------------------------------------------------------------------------------- 1 | { 2 | "D":{ 3 | "Results":[{ 4 | "Id": 4300, 5 | "Name": "Feb 13", 6 | "Images": [ 7 | {"Uri": "https://foo.bar", 8 | "Type":"all_in_one_png"}, 9 | {"Uri": "https://foo.bar", 10 | "Type": "all_in_one_thumbnail_png"} 11 | ], 12 | "Success":true 13 | }] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/spark_api/models/system_info.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class SystemInfo < Base 4 | self.element_name="system" 5 | 6 | def primary_logo 7 | logo = nil 8 | mls_logos = attributes['Configuration'].first['MlsLogos'] 9 | logo = mls_logos.first if !mls_logos.nil? and !mls_logos.empty? 10 | logo 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/fixtures/listing_carts/add_listing.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "ResourceUri": "/v1/listingcarts/20100912153422758914000000", 7 | "Id": "20100912153422758914000000", 8 | "Name": "My Listing Cart", 9 | "ListingCount": 11 10 | } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /spec/fixtures/listing_carts/remove_listing.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "ResourceUri": "/v1/listingcarts/20100912153422758914000000", 7 | "Id": "20100912153422758914000000", 8 | "Name": "My Listing Cart", 9 | "ListingCount": 9 10 | } 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /spec/fixtures/listing_carts/add_listings.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "ResourceUri": "/v1/listingcarts/20100912153422758914000000", 7 | "Id": "20100912153422758914000000", 8 | "Name": "My Listing Cart", 9 | "ListingCount": 12 10 | } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /spec/support/search_container_shared_examples.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for 'search_container' do 2 | let(:instance) { described_class.new( respond_to?(:initialization_args) ? initialization_args : {} ) } 3 | 4 | methods = [:template, :filter, :sort_id, :sort_id=].each do |method| 5 | 6 | it "responds to the method ##{method.to_s}" do 7 | expect(instance.respond_to?(method)).to eq(true) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/spark_api/models/tour_of_home.rb: -------------------------------------------------------------------------------- 1 | require 'date' 2 | require 'time' 3 | 4 | module SparkApi 5 | module Models 6 | class TourOfHome < Base 7 | extend Subresource 8 | 9 | self.element_name = "tourofhomes" 10 | 11 | def initialize(attributes={}) 12 | self.class.parse_date_start_and_end_times attributes 13 | super(attributes) 14 | end 15 | 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/fixtures/contacts/tags.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | {"ResourceUri": "/v1/contacts/tags/Current Buyers", "Tag": "Current Buyers"}, 6 | {"ResourceUri": "/v1/contacts/tags/Past Buyers", "Tag": "Past Buyers"}, 7 | {"ResourceUri": "/v1/contacts/tags/Current Sellers", "Tag": "Current Sellers"}, 8 | {"ResourceUri": "/v1/contacts/tags/TEST", "Tag": "TEST"} 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /spec/fixtures/listings/shared_listing_post.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "ResourceUri": "/v1/sharedlistings/15Ar", 7 | "SharedUri": "http://www.flexmls.com/share/15Ar/3544-N-Olsen-Avenue-Tucson-AZ-85719", 8 | "Mode": "Public", 9 | "Id": "15Ar" 10 | } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /spec/fixtures/errors/failure_with_constraint.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Message": "The data you provided failed constraints", 4 | "Success": false, 5 | "Details": [{ 6 | "ListPrice": [{ 7 | "RuleValue": 25.0, 8 | "Value": 500000.0, 9 | "RuleField": null, 10 | "RuleFieldValue": null, 11 | "RuleName": "MaxIncreasePercent" 12 | }] 13 | }], 14 | "Code": 1053 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /lib/spark_api/models/email_link.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class EmailLink < Base 4 | 5 | extend Finders 6 | 7 | self.prefix = "/flexmls/" 8 | self.element_name = "emaillinks" 9 | 10 | attr_accessor :template, :sort_id 11 | 12 | def filter 13 | "EmailLink Eq '#{id}'" 14 | end 15 | 16 | def listing_search_role 17 | :public 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/fixtures/listing_carts/add_portal_cart_listings.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "ResourceUri": "/v1/contact/20090928182824338901000000/listingcarts/20100912153422758914000000", 7 | "Id": "20100912153422758914000000", 8 | "Name": "My Listing Cart", 9 | "ListingCount": 11 10 | } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/spark_api/models/fields.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class Fields < Base 4 | self.element_name="fields" 5 | 6 | def self.order(property_type=nil, arguments={}) 7 | connection.get("#{self.path}/order#{"/"+property_type unless property_type.nil?}", arguments) 8 | end 9 | 10 | def self.settings 11 | connection.get("#{self.path}/order/settings") 12 | end 13 | 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/config/spark_api/test_key.yml: -------------------------------------------------------------------------------- 1 | development: 2 | ssl_verify: false 3 | api_key: 'demo_key' 4 | api_secret: 't3sts3cr3t' 5 | endpoint: 'https://developers.sparkapi.com' 6 | 7 | test: 8 | api_key: 'demo_key' 9 | api_secret: 't3sts3cr3t' 10 | endpoint: 'https://developers.sparkapi.com' 11 | 12 | production: 13 | api_key: 'prod_demo_key' 14 | api_secret: 'prod_t3sts3cr3t' 15 | endpoint: 'https://api.sparkapi.com' 16 | -------------------------------------------------------------------------------- /spec/fixtures/listings/constraints.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Details": [ 4 | { 5 | "ListPrice": [ 6 | { 7 | "RuleValue": 1000000.0, 8 | "RuleField": null, 9 | "RuleFieldValue": null, 10 | "Value": 1000001.0, 11 | "RuleName": "MaxValue" 12 | } 13 | ] 14 | } 15 | ], 16 | "Success": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/spark_api/models/listing_meta_translations.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class ListingMetaTranslations < Base 4 | 5 | def self.for_property_type(pt, options={}) 6 | results = connection.get("/flexmls/propertytypes/#{pt}/translations", options) 7 | if results.any? 8 | collect(results).first 9 | else 10 | new(StandardFields: {}, CustomFields: {}) 11 | end 12 | end 13 | 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/unit/spark_api/models/rule_spec.rb: -------------------------------------------------------------------------------- 1 | require './spec/spec_helper' 2 | 3 | describe Rule do 4 | 5 | describe 'for_property_type' do 6 | 7 | on_get_it "should get documents for a listing" do 8 | stub_auth_request 9 | stub_api_get('/listings/rules/propertytypes/A','rules/get.json') 10 | 11 | rules = Rule.for_property_type('A') 12 | expect(rules).to be_an(Array) 13 | expect(rules.length).to eq(2) 14 | end 15 | 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /spec/unit/spark_api/authentication/oauth2_impl/single_session_provider_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SparkApi::Authentication::SingleSessionProvider do 4 | subject { SparkApi::Authentication::SingleSessionProvider.new({ :access_token => "the_token" }) } 5 | it "should initialize a new session with access_token" do 6 | expect(subject.load_session).to respond_to(:access_token) 7 | expect(subject.load_session.access_token).to eq("the_token") 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/unit/spark_api/options_hash_spec.rb: -------------------------------------------------------------------------------- 1 | require './spec/spec_helper' 2 | 3 | describe SparkApi::OptionsHash do 4 | it "should convert symbol parameters to a string" do 5 | h = {:parameter_one => 1, 6 | "parameter_two" => 2, 7 | 3 => 3} 8 | o_h = SparkApi::OptionsHash.new(h) 9 | expect(o_h.keys.size).to eq(3) 10 | expect(o_h["parameter_one"]).to eq(1) 11 | expect(o_h["parameter_two"]).to eq(2) 12 | expect(o_h[3]).to eq(3) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/unit/spark_api/models/sort_spec.rb: -------------------------------------------------------------------------------- 1 | require './spec/spec_helper' 2 | 3 | describe Sort do 4 | before(:each) do 5 | stub_auth_request 6 | end 7 | 8 | it "should include the finders module" do 9 | expect(Sort).to respond_to(:find) 10 | end 11 | 12 | it "should return sorts" do 13 | stub_api_get("/searchtemplates/sorts", 'sorts/get.json') 14 | sorts = Sort.find(:all) 15 | expect(sorts).to be_an(Array) 16 | expect(sorts.length).to eq(1) 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /spec/fixtures/contacts/vow_accounts/get.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [{ 5 | "ResourceUri": "/v1/contacts/20090928182824338901000000/portal", 6 | "Id": "20060412165917817933000000", 7 | "OwnerId": "20090410349683017933000000", 8 | "LoginName": "dave", 9 | "LastLogin": "2012-03-07T20:09:37Z", 10 | "Locale": { 11 | "Language": "en" 12 | } 13 | }] 14 | } 15 | } -------------------------------------------------------------------------------- /spec/fixtures/messages/new.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Messages": [ 4 | { 5 | "Type": "ShowingRequest", 6 | "EventDateTime": "2011-09-15T14:00:00", 7 | "SenderId": "20110112234857732941000000", 8 | "Subject": "Showing Request For 123 Main St, MLS # 12-345", 9 | "Body": "A showing is requested for ...", 10 | "ListingId": "20110112234857732941000000" 11 | } 12 | ] 13 | } 14 | } -------------------------------------------------------------------------------- /spec/fixtures/listings/shared_listing_get.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "Id": "15Ar", 7 | "ResourceUri": "/v1/sharedlistings/15Ar", 8 | "SharedUri": "http://www.flexmls.com/share/15Ar/3544-N-Olsen-Avenue-Filabee-AZ-85719", 9 | "ListingIds": ["20110224152431857619000000","20110125122333785431000000"], 10 | "Mode": "Public" 11 | } 12 | ] 13 | } 14 | } -------------------------------------------------------------------------------- /spec/unit/spark_api/models/constraint_spec.rb: -------------------------------------------------------------------------------- 1 | require './spec/spec_helper' 2 | 3 | 4 | describe Constraint do 5 | 6 | subject do 7 | Constraint.new( 8 | "RuleValue" => 1000000.0, 9 | "Value" => 1000001.0, 10 | "RuleFieldValue" => 1.0, 11 | "RuleField" => "ListPrice", 12 | "RuleName" => "MaxValue") 13 | end 14 | 15 | it "should print to string" do 16 | expect(subject.to_s).to eq("MaxValue: Field(ListPrice,1.0) Value(1000000.0,1000001.0)") 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /spec/fixtures/fields/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "ShowingInstructions": [ 7 | { 8 | "Domain": "StandardFields", 9 | "Field": "ListOfficePhone", 10 | "GroupField": null 11 | }, 12 | { 13 | "Domain": "CustomFields", 14 | "Field": "Vacant", 15 | "GroupField": "Property Access" 16 | } 17 | ] 18 | } 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /spec/fixtures/base.json: -------------------------------------------------------------------------------- 1 | {"D": { 2 | "Success": true, 3 | "Results": [{ 4 | "Id": 1, 5 | "ResourceUri": "/v1/some/place/20101230223226074201000000", 6 | "Name": "My Example", 7 | "Test": true 8 | }, 9 | { 10 | "Id": 2, 11 | "ResourceUri": "/v1/some/place/20101230223226074204000000", 12 | "Name": "My Example2", 13 | "Test": false 14 | }]} 15 | } 16 | -------------------------------------------------------------------------------- /lib/spark_api/models/open_house.rb: -------------------------------------------------------------------------------- 1 | require 'date' 2 | require 'time' 3 | 4 | module SparkApi 5 | module Models 6 | class OpenHouse < Base 7 | extend Subresource 8 | 9 | self.element_name = "openhouses" 10 | 11 | def initialize(attributes={}) 12 | self.class.parse_date_start_and_end_times attributes 13 | if attributes["Comments"].nil? 14 | attributes["Comments"] = "" 15 | end 16 | super(attributes) 17 | end 18 | 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/unit/spark_api/authentication/oauth2_impl/grant_type_base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SparkApi::Authentication::OAuth2Impl::GrantTypeBase do 4 | subject { SparkApi::Authentication::OAuth2Impl::GrantTypeBase } 5 | # Make sure the client boostraps the right plugin based on configuration. 6 | it "create should " do 7 | expect {subject.create(nil, InvalidAuth2Provider.new())}.to raise_error(SparkApi::ClientError){ |e| expect(e.message).to eq("Unsupported grant type [not_a_real_type]") } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/spark_api/authentication/oauth2_impl/simple_provider.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Authentication 3 | class SimpleProvider < BaseOAuth2Provider 4 | def initialize(credentials) 5 | super(credentials) 6 | @grant_type = :authorization_code 7 | end 8 | 9 | def load_session 10 | @session 11 | end 12 | 13 | def save_session session 14 | @session = session 15 | end 16 | 17 | def destroy_session 18 | @session = nil 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/fixtures/portal/my.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [{ 5 | "ResourceUri": "/v1/portal/20100912153422758914000000", 6 | "Id": "20100912153422758914000000", 7 | "OwnerId": "20110000000000000000000001", 8 | "ModificationTimestamp": "2011-11-18T16:35:43", 9 | "Name": "greatportal", 10 | "DisplayName": "GreatPortal", 11 | "Enabled": true, 12 | "RequiredFields": ["Address", "Phone"] 13 | }] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /spec/fixtures/accounts/my_portal.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "Name" : "Portal User", 7 | "Id" : "20110426173054342350000000", 8 | "Emails": [ 9 | ], 10 | "Phones": [ 11 | ], 12 | "Websites": [ 13 | ], 14 | "Addresses": [ 15 | ], 16 | "GetEmailUpdates": false 17 | } 18 | ] 19 | } 20 | } -------------------------------------------------------------------------------- /spec/unit/spark_api/models/email_link_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe EmailLink do 4 | it_behaves_like 'search_container' 5 | 6 | let(:email_link) { EmailLink.new(Id: 5) } 7 | 8 | describe 'filter' do 9 | it 'returns a filter for the email link' do 10 | expect(email_link.filter).to eq "EmailLink Eq '#{email_link.id}'" 11 | end 12 | end 13 | 14 | describe 'listing_search_role' do 15 | it 'returns a symbol' do 16 | expect(EmailLink.new().listing_search_role).to eq(:public) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/spark_api/models/constraint.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class Constraint 4 | ATTRIBUTES = ["RuleValue","Value","RuleFieldValue","RuleField","RuleName"] 5 | attr_accessor *ATTRIBUTES 6 | def initialize(args) 7 | ATTRIBUTES.each { |f| send("#{f}=", args[f]) if args.include?(f) || args.include?(f.to_sym) } 8 | end 9 | 10 | def to_s 11 | "#{self.RuleName}: Field(#{self.RuleField},#{self.RuleFieldValue}) Value(#{self.RuleValue},#{self.Value})" 12 | end 13 | end 14 | end 15 | end 16 | 17 | -------------------------------------------------------------------------------- /spec/unit/spark_api/authentication/base_auth_spec.rb: -------------------------------------------------------------------------------- 1 | require './spec/spec_helper' 2 | 3 | describe SparkApi::Authentication::BaseAuth do 4 | subject {SparkApi::Authentication::BaseAuth.new(nil) } 5 | it "should raise an error" do 6 | expect {subject.authenticate()}.to raise_error(){ |e| expect(e.message).to eq("Implement me!")} 7 | expect {subject.logout()}.to raise_error(){ |e| expect(e.message).to eq("Implement me!")} 8 | expect {subject.request(nil, nil, nil, nil)}.to raise_error(){ |e| expect(e.message).to eq("Implement me!")} 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/fixtures/fields/order.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "A": [ 7 | { 8 | "Contract Information": [ 9 | { 10 | "Field": "23Patios", 11 | "Label": "Book Section", 12 | "Domain": "CustomFields", 13 | "Detail": true 14 | } 15 | ] 16 | } 17 | ], 18 | "B": [ ] 19 | } 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/spark_api/cli/api_auth.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + "/../cli/setup" 2 | 3 | SparkApi.configure do |config| 4 | config.api_key = ENV["API_KEY"] 5 | config.api_secret = ENV["API_SECRET"] 6 | config.api_user = ENV["API_USER"] if ENV["API_USER"] 7 | config.endpoint = ENV["API_ENDPOINT"] if ENV["API_ENDPOINT"] 8 | config.ssl_verify = ENV["SSL_VERIFY"].downcase != 'false' if ENV["SSL_VERIFY"] 9 | config.middleware = ENV["SPARK_MIDDLEWARE"] if ENV["SPARK_MIDDLEWARE"] 10 | config.dictionary_version = ENV["DICTIONARY_VERSION"] if ENV["DICTIONARY_VERSION"] 11 | end 12 | -------------------------------------------------------------------------------- /spec/fixtures/idx_links/get.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "ResourceUri": "/v1/idxlinks/20130610145525941067000000", 7 | "LinkType":"QuickSearch", 8 | "LinkId":"zq1fkiw4d3f", 9 | "Uri":"http://link.dev.fbsdata.com/zq1fkiw4d3f,1", 10 | "ResourceUri":"/v1/idxlinks/20130610145525941067000000", 11 | "Name":"1 - Residential", 12 | "Id":"20130610145525941067000000" 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /script/access_token_example.rb: -------------------------------------------------------------------------------- 1 | require "spark_api" 2 | 3 | SparkApi.configure do |config| 4 | config.endpoint = 'https://sparkapi.com' 5 | config.authentication_mode = SparkApi::Authentication::OAuth2 6 | end 7 | 8 | #### COPY/PASTE YOUR ACCESS TOKEN WHERE DESIGNATED BELOW 9 | SparkApi.client.session = SparkApi::Authentication::OAuthSession.new({ :access_token => "your_access_token_here" }) 10 | 11 | client = SparkApi.client 12 | 13 | list = client.get '/contacts' 14 | puts "client: #{list.inspect}" 15 | list = SparkApi::Models::Contact.get 16 | puts "model: #{list.inspect}" 17 | -------------------------------------------------------------------------------- /spec/fixtures/listings/rental_calendar.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Results": [ 4 | { 5 | "EndDate": "2012-07-16", 6 | "Id": "20120705192139246257000000", 7 | "StartDate": "2012-07-12", 8 | "Type": "Rented" 9 | }, 10 | { 11 | "EndDate": "2012-07-23", 12 | "Id": "20120705192148668143000000", 13 | "StartDate": "2012-07-20", 14 | "Type": "Rented" 15 | } 16 | ], 17 | "Success": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/spark_api/options_hash.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | # Used for API client options that accept string keys instead of symbols -- 3 | # turns all symbol keys into a string for consistancy, allowing applications 4 | # using the client to pass in parameters as symbols or strings. 5 | class OptionsHash < Hash 6 | def initialize(from_hash={}) 7 | from_hash.keys.each do |k| 8 | if k.is_a?(Symbol) 9 | self[k.to_s] = from_hash[k] 10 | else 11 | self[k] = from_hash[k] 12 | end 13 | end 14 | self 15 | end 16 | end 17 | end 18 | 19 | -------------------------------------------------------------------------------- /spec/fixtures/messages/new_with_recipients.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Messages": [ 4 | { 5 | "Type": "ShowingRequest", 6 | "EventDateTime": "2011-09-15T14:00:00", 7 | "SenderId": "20110112234857732941000000", 8 | "Subject": "Showing Request For 123 Main St, MLS # 12-345", 9 | "Body": "A showing is requested for ...", 10 | "ListingId": "20110112234857732941000000", 11 | "Recipients": ["20110112234857732941000000","20110092234857738467000000"] 12 | } 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /lib/spark_api/models/floplan.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class FloPlan < Base 4 | extend Subresource 5 | self.element_name = 'floplans' 6 | 7 | attr_accessor :images, :thumbnails 8 | 9 | def initialize(attributes={}) 10 | @images = [] 11 | @thumbnails = [] 12 | 13 | attributes['Images'].each do |img| 14 | if img["Type"].include?('thumbnail') 15 | @thumbnails << img 16 | else 17 | @images << img 18 | end 19 | end 20 | super(attributes) 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2011 Financial Business Systems, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /spec/unit/spark_api/models/newsfeed_spec.rb: -------------------------------------------------------------------------------- 1 | require './spec/spec_helper' 2 | 3 | 4 | describe Newsfeed do 5 | 6 | before do 7 | stub_auth_request 8 | end 9 | 10 | describe "update!" do 11 | it "should update the attributes" do 12 | stub_api_get("/newsfeeds/20130625195235039712000000", "newsfeeds/get.json") 13 | stub_api_put("/newsfeeds/20130625195235039712000000", {:Active => false}, "newsfeeds/inactive.json") 14 | @newsfeed = Newsfeed.find("20130625195235039712000000") 15 | @newsfeed.update!(:Active => false) 16 | expect(@newsfeed.Active) == false 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/fixtures/saved_searches/without_newsfeed.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "ResourceUri": "/v1/savedsearches/20100815220615294367000000", 7 | "Id": "20100815220615294367000000", 8 | "Name": "Search name here", 9 | "ContactIds": [ 10 | "20100815220615294367000000" 11 | ], 12 | "NewsFeedSubscriptionSummary": { 13 | "ActiveSubscription": false 14 | }, 15 | "NewsFeeds": [] 16 | } 17 | 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /spec/fixtures/listings/constraints_with_pagination.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Details": [ 4 | { 5 | "ListPrice": [ 6 | { 7 | "RuleValue": 1000000.0, 8 | "RuleField": null, 9 | "RuleFieldValue": null, 10 | "Value": 1000001.0, 11 | "RuleName": "MaxValue" 12 | } 13 | ] 14 | } 15 | ], 16 | "Pagination": { 17 | "TotalRows": 0, 18 | "PageSize": 1, 19 | "TotalPages": 1, 20 | "CurrentPage": 1 21 | }, 22 | "Success": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /spec/config/spark_api/test_single_session_oauth.yml: -------------------------------------------------------------------------------- 1 | development: 2 | oauth2: true 3 | oauth2_provider: 'SparkApi::Authentication::SingleSessionProvider' 4 | endpoint: 'http://api.dev.fbsdata.com' 5 | access_token: 'yay success!' 6 | 7 | test: 8 | oauth2: true 9 | oauth2_provider: 'SparkApi::Authentication::SingleSessionProvider' 10 | endpoint: 'http://api.dev.fbsdata.com' 11 | access_token: 'yay success!' 12 | 13 | production: 14 | oauth2: true 15 | oauth2_provider: 'SparkApi::Authentication::SingleSessionProvider' 16 | endpoint: 'http://api.sparkplatform.com' 17 | access_token: 'yay success!' 18 | -------------------------------------------------------------------------------- /spec/fixtures/listing_carts/listing_cart.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "ResourceUri": "/v1/listingcarts/20100912153422758914000000", 7 | "Id": "20100912153422758914000000", 8 | "Name": "My Listing Cart", 9 | "ListingCount": 10 10 | }, 11 | { 12 | "ResourceUri": "/v1/listingcarts/20110112133422752751000000", 13 | "Id": "20110112133422752751000000", 14 | "Name": "My Other Listing Cart", 15 | "ListingCount": 15 16 | } 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/spark_api/models/account_report.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class AccountReport < Account 4 | def self.report(account_id, arguments={}) 5 | collect(connection.get("/accounts/#{account_id}/report", arguments)).first 6 | end 7 | 8 | def DisplayName 9 | self.Name 10 | end 11 | 12 | def primary_email 13 | if Array(emails).any? && emails.primary 14 | emails.primary.Address 15 | end 16 | end 17 | 18 | def primary_phone 19 | if Array(phones).any? && phones.primary 20 | phones.primary.Number 21 | end 22 | end 23 | 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/fixtures/contacts/my.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Results": [ 4 | { 5 | "HomeStreetAddress": "23541235", 6 | "ResourceUri": "/v1/my/contact", 7 | "DisplayName": "BH FOO", 8 | "Id": "20090928182824338901000000", 9 | "ResourceUri":"/v1/contacts/20090928182824338901000000", 10 | "Tags": [ 11 | "IDX Lead" 12 | ], 13 | "PrimaryEmail": "bhfoo@fbsdata.com", 14 | "FamilyName": "foo", 15 | "GivenName": "bh" 16 | } 17 | ], 18 | "Success": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/spark_api/reso_faraday_middleware.rb: -------------------------------------------------------------------------------- 1 | 2 | module SparkApi 3 | class ResoFaradayMiddleware < FaradayMiddleware 4 | def on_complete(env) 5 | begin 6 | body = MultiJson.decode(env[:body]) 7 | 8 | if body["D"] 9 | super(env) 10 | return 11 | end 12 | 13 | env[:body] = body 14 | rescue MultiJson::ParseError => e 15 | # We will allow the client to choose their XML parser, but should do 16 | # some minor format verification 17 | raise e if body.strip[/\A<\?xml/].nil? 18 | end 19 | end 20 | end 21 | 22 | Faraday::Response.register_middleware :reso_api => ResoFaradayMiddleware 23 | end 24 | -------------------------------------------------------------------------------- /lib/spark_api/authentication/oauth2_impl/single_session_provider.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Authentication 3 | 4 | class SingleSessionProvider < BaseOAuth2Provider 5 | 6 | def initialize(credentials) 7 | @access_token = credentials.delete(:access_token) 8 | super(credentials) 9 | end 10 | 11 | def load_session 12 | @session ||= SparkApi::Authentication::OAuthSession.new({ 13 | :access_token => @access_token 14 | }) 15 | end 16 | 17 | def save_session session 18 | @session = session 19 | end 20 | 21 | def destroy_session 22 | @session = nil 23 | end 24 | 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/unit/spark_api/models/floplan_spec.rb: -------------------------------------------------------------------------------- 1 | require './spec/spec_helper' 2 | 3 | describe FloPlan do 4 | 5 | it "responds to" do 6 | expect(FloPlan).to respond_to(:find_by_listing_key) 7 | end 8 | 9 | describe "/listings//videos", :support do 10 | before do 11 | stub_auth_request 12 | stub_api_get('/listings/1234/floplans','listings/floplans_index.json') 13 | end 14 | 15 | on_get_it "should correctly split images and thumbnails" do 16 | p = FloPlan.find_by_listing_key('1234').first 17 | expect(p.attributes['Images'].length).to eq(2) 18 | expect(p.images.length).to eq(1) 19 | expect(p.thumbnails.length).to eq(1) 20 | end 21 | 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /spec/fixtures/listing_carts/listing_portal_cart.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "ResourceUri": "/v1/contacts/20090928182824338901000000/listingcarts/20100912153422758914000000", 7 | "Id": "20100912153422758914000000", 8 | "Name": "My Listing Cart", 9 | "ListingCount": 10 10 | }, 11 | { 12 | "ResourceUri": "/v1/contacts/20090928182824338901000000/listingcarts/20110112133422752751000000", 13 | "Id": "20110112133422752751000000", 14 | "Name": "My Other Listing Cart", 15 | "ListingCount": 15 16 | } 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /script/spark_auth_example.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | 4 | Bundler.require(:default, "development") if defined?(Bundler) 5 | 6 | path = File.expand_path(File.dirname(__FILE__) + "/../lib/") 7 | $LOAD_PATH.unshift(path) unless $LOAD_PATH.include?(path) 8 | require path + '/spark_api' 9 | 10 | SparkApi.logger.info("Hello!") 11 | 12 | #### COPY/PASTE YOUR API KEY AND SECRET BELOW 13 | SparkApi.configure do |config| 14 | config.api_key = "agent_key" 15 | config.api_secret = "agent_secret" 16 | config.version = "v1" 17 | config.endpoint = "https://api.sparkapi.com" 18 | end 19 | 20 | client = SparkApi.client 21 | 22 | list = client.get '/contacts' 23 | puts "client: #{list.inspect}" 24 | list = SparkApi::Models::Contact.get 25 | puts "model: #{list.inspect}" 26 | 27 | -------------------------------------------------------------------------------- /lib/spark_api/models/rental_calendar.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class RentalCalendar < Base 4 | extend Subresource 5 | self.element_name="rentalcalendar" 6 | 7 | def initialize(attributes={}) 8 | # Transform the date strings 9 | unless attributes['StartDate'].nil? 10 | date = Date.parse(attributes['StartDate']) 11 | attributes['StartDate'] = date 12 | end 13 | unless attributes['EndDate'].nil? 14 | date = Date.parse(attributes['EndDate']) 15 | attributes['EndDate'] = date 16 | end 17 | super(attributes) 18 | end 19 | 20 | def include_date? (day) 21 | day >= self.StartDate && day <= self.EndDate 22 | end 23 | 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/spark_api/primary_array.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | class PrimaryArray < Array 3 | 4 | def primary 5 | find_primary 6 | end 7 | 8 | private 9 | 10 | # This is a very simplistic but reliable implementation. 11 | def find_primary 12 | self.each do |arg| 13 | if arg.primary? 14 | return arg 15 | end 16 | end 17 | nil 18 | end 19 | end 20 | 21 | #=== Primary: interface to implement for elements that are added to a "PrimaryArray" collection 22 | module Primary 23 | # Return true if the element is the primary resource in a collection. 24 | # Default implementation looks for a "Primary" attribute 25 | def primary? 26 | @attributes.key?("Primary") && self.Primary == true 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | 3 | require 'rubygems/user_interaction' 4 | require 'rspec' 5 | require 'rspec/core/rake_task' 6 | require 'ci/reporter/rake/rspec' 7 | require 'bundler/gem_tasks' 8 | 9 | RSpec::Core::RakeTask.new do |t| 10 | t.rspec_opts = ["-c", "-f progress"] 11 | t.pattern = 'spec/**/*_spec.rb' 12 | end 13 | 14 | desc "Run all the tests" 15 | task :default => :spec 16 | 17 | desc "Generate (and test) supported Spark API paths and methods" 18 | RSpec::Core::RakeTask.new(:api_support) do |t| 19 | t.rspec_opts = ["--require spec/formatters/api_support_formatter", 20 | "--format ApiSupportFormatter", 21 | "--color", 22 | "--tag support"] 23 | t.pattern = 'spec/unit/spark_api/models/**/*_spec.rb' 24 | t.verbose = false 25 | end 26 | -------------------------------------------------------------------------------- /lib/spark_api/models/virtual_tour.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class VirtualTour < Base 4 | extend Subresource 5 | include Media 6 | include Concerns::Savable, 7 | Concerns::Destroyable 8 | 9 | self.element_name="virtualtours" 10 | 11 | 12 | def branded? 13 | attributes["Type"] == "branded" 14 | end 15 | 16 | def unbranded? 17 | attributes["Type"] == "unbranded" 18 | end 19 | 20 | def url 21 | attributes['Uri'] 22 | end 23 | 24 | def description 25 | attributes['Name'] 26 | end 27 | 28 | def display_image 29 | # Currently we have no universally good mechanism to get images for virtual tours 30 | return nil 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/fixtures/listings/open_houses.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "ResourceUri": "/v1/listings/20060412165917817933000000/openhouses/20101127153422574618000000", 7 | "Id": "20101127153422574618000000", 8 | "Date": "10/01/2010", 9 | "StartTime": "9:00 am", 10 | "EndTime": "12:00 pm" 11 | }, 12 | { 13 | "ResourceUri": "/v1/listings/20060412165917817933000000/openhouses/20101127153422174618000000", 14 | "Id": "20101127153422174618000000", 15 | "Date": "10/08/2010", 16 | "StartTime": "09:00-07:00", 17 | "EndTime": "12:00-07:00" 18 | } 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /spec/fixtures/saved_searches/get.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "ResourceUri": "/v1/savedsearches/20100815220615294367000000", 7 | "Id": "20100815220615294367000000", 8 | "Name": "Search name here", 9 | "Filter": "City Eq 'Moorhead' And MlsStatus Eq 'Active' And PropertyType Eq 'A'", 10 | "ContactIds": [ 11 | "20100815220615294367000000" 12 | ], 13 | "Provided": false 14 | }, 15 | { 16 | "ResourceUri": "/v1/savedsearches/20100615220615292711000000", 17 | "Id": "20100615220615292711000000", 18 | "Name": "Second search name here" 19 | } 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /script/example.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | 4 | Bundler.require(:default, "development") if defined?(Bundler) 5 | 6 | path = File.expand_path(File.dirname(__FILE__) + "/../lib/") 7 | $LOAD_PATH.unshift(path) unless $LOAD_PATH.include?(path) 8 | require path + '/spark_api' 9 | 10 | SparkApi.logger.info("Hello!") 11 | 12 | SparkApi.configure do |config| 13 | config.endpoint = 'https://sparkapi.com' 14 | config.authentication_mode = SparkApi::Authentication::OAuth2 15 | end 16 | 17 | SparkApi.client.session = SparkApi::Authentication::OAuthSession.new({ :access_token => "your_access_token_here" }) 18 | 19 | client = SparkApi.client 20 | 21 | list = client.get '/contacts' 22 | puts "client: #{list.inspect}" 23 | list = SparkApi::Models::Contact.get 24 | puts "model: #{list.inspect}" 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /spec/fixtures/saved_searches/get_provided.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "ResourceUri": "/v1/savedsearches/20100815220615294367000000", 7 | "Id": "20100815220615294367000000", 8 | "Name": "Search name here", 9 | "Filter": "City Eq 'Moorhead' And MlsStatus Eq 'Active' And PropertyType Eq 'A'", 10 | "ContactIds": [ 11 | "20100815220615294367000000" 12 | ], 13 | "Provided": true 14 | }, 15 | { 16 | "ResourceUri": "/v1/savedsearches/20100615220615292711000000", 17 | "Id": "20100615220615292711000000", 18 | "Name": "Second search name here" 19 | } 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /spec/fixtures/property_types/property_types.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "MlsName": "Residential", 7 | "MlsCode": "A" 8 | }, 9 | { 10 | "MlsName": "Multi Family", 11 | "MlsCode": "B" 12 | }, 13 | { 14 | "MlsName": "Land", 15 | "MlsCode": "C" 16 | }, 17 | { 18 | "MlsName": "Commercial", 19 | "MlsCode": "D" 20 | }, 21 | { 22 | "MlsName": "Farm", 23 | "MlsCode": "E" 24 | }, 25 | { 26 | "MlsName": "Rental", 27 | "MlsCode": "F" 28 | } 29 | ] 30 | } 31 | } -------------------------------------------------------------------------------- /spec/fixtures/listings/document_index.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Results": [ 4 | { 5 | "Uri": "http://images.dev.fbsdata.com/documents/cda/20060725224801143085000000.pdf", 6 | "ResourceUri": "/v1/listings/20060725224713296297000000/documents/20060725224801143085000000", 7 | "Name": "Disclosure", 8 | "Id": "20060725224801143085000000" 9 | }, 10 | { 11 | "Uri": "http://images.dev.fbsdata.com/documents/cda/20060725224818080340000000.pdf", 12 | "ResourceUri": "/v1/listings/20060725224713296297000000/documents/20060725224818080340000000", 13 | "Name": "Plat Map", 14 | "Id": "20060725224818080340000000" 15 | } 16 | ], 17 | "Success": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /spec/fixtures/comments/post.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Results": [ 4 | { 5 | "CreatedTimestamp":"2012-11-14T10:02:01-06:00", 6 | "UserId":"20000426143505724628000000", 7 | "ReferenceId":"20110105173455471471000000", 8 | "ResourceUri":"/v1/contacts/20110105173455471471000000/comments/20121114100201798092000005", 9 | "UserUri":"/v1/accounts/20000426143505724628000000", 10 | "ModificationTimestamp":"2012-11-14T10:02:01-06:00", 11 | "ReferenceType":"Contact", 12 | "Comment":"This is a comment.", 13 | "ReferenceUri":"/v1/contacts/20110105173455471471000000", 14 | "UserType":"Mls", 15 | "Id":"20121114100201798092000005" 16 | } 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/spark_api/models/media.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | module Media 4 | # This module is effectively an interface and helper to combine common media 5 | # actions and information. Media types (videos, virtual tours, etc) 6 | # should include this module and implement the methods contained 7 | 8 | def url 9 | raise "Not Implemented" 10 | end 11 | 12 | def description 13 | raise "Not Implemented" 14 | end 15 | 16 | def private? 17 | attributes['Privacy'] == 'Private' 18 | end 19 | 20 | def public? 21 | attributes['Privacy'] == 'Public' 22 | end 23 | 24 | def automatic? 25 | attributes['Privacy'] == 'Automatic' 26 | end 27 | 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/spark_api/models/portal.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | 4 | class Portal < Base 5 | extend Finders 6 | include Concerns::Savable 7 | 8 | self.element_name = "portal" 9 | 10 | def self.my(arguments = {}) 11 | portal = collect(connection.get("/portal", arguments)).first 12 | portal = Portal.new if portal.nil? 13 | portal 14 | end 15 | 16 | def enabled? 17 | @attributes['Enabled'] == true 18 | end 19 | 20 | def enable 21 | attribute_will_change! "Enabled" 22 | @attributes['Enabled'] = true 23 | save 24 | end 25 | 26 | def disable 27 | attribute_will_change! "Enabled" 28 | @attributes['Enabled'] = false 29 | save 30 | end 31 | 32 | def post_data; attributes end 33 | 34 | end 35 | 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/unit/spark_api/models/activity_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Activity do 4 | 5 | before :each do 6 | stub_auth_request 7 | end 8 | 9 | context '/activities' do 10 | it "gets a current user's activities" do 11 | s = stub_api_get("/activities", "activities/get.json") 12 | activities = Activity.get 13 | expect(activities).to be_an(Array) 14 | expect(activities.size).to eq(2) 15 | expect(s).to have_been_requested 16 | end 17 | end 18 | 19 | context '/activities/' do 20 | let(:id) { "20121128132106172132000004" } 21 | it "gets an individual activity" do 22 | s = stub_api_get("/activities/#{id}", "activities/get.json") 23 | activity = Activity.find(id) 24 | expect(activity).to be_an(Activity) 25 | expect(s).to have_been_requested 26 | end 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /spec/fixtures/activities/get.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Results": [ 4 | { 5 | "Id":"20121128132106172132000004", 6 | "ResourceUri":"/v1/activities/20121128132106172132000004", 7 | "ActivityId":null, 8 | "CreatedTimestamp":"2012-11-28T13:21:06Z", 9 | "ActivityResourceUri":"/v1/contacts/20121102143633121730000000/comments", 10 | "ActivityType":"ContactComment" 11 | }, 12 | { 13 | "Id":"20121128132106172132000005", 14 | "ResourceUri":"/v1/activities/20121128132106172132000005", 15 | "ActivityId":null, 16 | "CreatedTimestamp":"2012-11-28T13:21:06Z", 17 | "ActivityResourceUri":"/v1/contacts/20121102143633121730000000/comments", 18 | "ActivityType":"ContactComment" 19 | } 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /spec/fixtures/contacts/contacts.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "ResourceUri":"/v1/contacts/20101230223226074201000000", 7 | "DisplayName":"Contact One", 8 | "Id":"20101230223226074201000000", 9 | "PrimaryEmail":"contact1@fbsdata.com", 10 | "Tags": ["IDX Lead"] 11 | }, 12 | { 13 | "ResourceUri":"/v1/contacts/20101230223226074202000000", 14 | "DisplayName":"Contact Two", 15 | "Id":"20101230223226074202000000", 16 | "PrimaryEmail":"contact2fbsdata.com", 17 | "Tags": ["IDX Lead"] 18 | }, 19 | { 20 | "ResourceUri":"/v1/contacts/20101230223226074203000000", 21 | "DisplayName":"Contact Three", 22 | "Id":"20101230223226074203000000", 23 | "PrimaryEmail":"contact3@fbsdata.com", 24 | "Tags": ["IDX Lead"] 25 | } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /spec/unit/spark_api_spec.rb: -------------------------------------------------------------------------------- 1 | require './spec/spec_helper' 2 | 3 | describe SparkApi do 4 | 5 | it "should use 'yajl-ruby' for parsing json" do 6 | expect(MultiJson.engine).to eq(MultiJson::Adapters::Yajl) unless jruby? 7 | end 8 | 9 | it "should load the version" do 10 | expect(subject::VERSION).to match(/\d+\.\d+\.\d+/) 11 | end 12 | 13 | it "should give me a client connection" do 14 | expect(subject.client).to be_a(SparkApi::Client) 15 | end 16 | 17 | it "should reset my connection" do 18 | c1 = subject.client 19 | subject.reset 20 | expect(subject.client).not_to eq(c1) 21 | end 22 | 23 | it "should let me override the default logger" do 24 | expect(subject.logger.level).to eq(Logger::DEBUG) # default overridden in spec_helper 25 | 26 | subject.logger = Logger.new('/dev/null') 27 | subject.logger.level = Logger::WARN 28 | 29 | expect(SparkApi.logger.level).to eq(Logger::WARN) 30 | end 31 | end 32 | 33 | -------------------------------------------------------------------------------- /lib/spark_api/models/market_statistics.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class MarketStatistics < Base 4 | self.element_name="marketstatistics" 5 | 6 | def self.absorption(parameters={}) 7 | self.stat('absorption',parameters) 8 | end 9 | def self.inventory(parameters={}) 10 | self.stat('inventory',parameters) 11 | end 12 | def self.price(parameters={}) 13 | self.stat('price',parameters) 14 | end 15 | def self.ratio(parameters={}) 16 | self.stat('ratio',parameters) 17 | end 18 | def self.dom(parameters={}) 19 | self.stat('dom',parameters) 20 | end 21 | def self.volume(parameters={}) 22 | self.stat('volume',parameters) 23 | end 24 | 25 | private 26 | def self.stat(stat_name, parameters={}) 27 | resp = connection.get("#{path}/#{stat_name}", parameters) 28 | new(resp.first) 29 | end 30 | 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/spark_api/models/defaultable.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | module Defaultable 4 | 5 | DEFAULT_ID = "default" 6 | 7 | extend Finders 8 | 9 | def self.included(base) 10 | 11 | class << base 12 | alias original_find find 13 | end 14 | 15 | base.extend(ClassMethods) 16 | 17 | end 18 | 19 | module ClassMethods 20 | 21 | def default(options = {}) 22 | response = connection.get("/#{element_name}/default", options).first 23 | unless response.nil? 24 | response["Id"] = DEFAULT_ID if response["Id"].nil? 25 | new(response) 26 | end 27 | end 28 | 29 | def find(*arguments) 30 | if arguments.first == DEFAULT_ID 31 | options = arguments.slice!(1) || {} 32 | default(options) 33 | else 34 | original_find(*arguments) 35 | end 36 | end 37 | 38 | end 39 | 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/unit/spark_api/models/document_spec.rb: -------------------------------------------------------------------------------- 1 | require './spec/spec_helper' 2 | 3 | describe Document do 4 | before(:each) do 5 | @document = Document.new({ 6 | :Uri => "http://images.dev.fbsdata.com/documents/cda/20060725224801143085000000.pdf", 7 | :ResourceUri => "/v1/listings/20060725224713296297000000/documents/20060725224801143085000000", 8 | :Name => "Disclosure", 9 | :Id => "20110105165843978012000000", 10 | }) 11 | end 12 | 13 | it "should respond to a few methods" do 14 | expect(Document).to respond_to(:find_by_listing_key) 15 | end 16 | 17 | context "/listings//documents" do 18 | on_get_it "should get documents for a listing" do 19 | stub_auth_request 20 | stub_api_get('/listings/1234/documents','listings/document_index.json') 21 | 22 | v = Document.find_by_listing_key('1234') 23 | expect(v).to be_an(Array) 24 | expect(v.length).to eq(2) 25 | end 26 | end 27 | 28 | after(:each) do 29 | @document = nil 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /spec/unit/spark_api/models/search_template/quick_search_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe QuickSearch do 4 | 5 | before :each do 6 | stub_auth_request 7 | end 8 | 9 | context '/searchtemplates/quicksearches' do 10 | it "gets a current user's quick searches" do 11 | s = stub_api_get("/searchtemplates/quicksearches", "search_templates/quick_searches/get.json") 12 | quicksearches = QuickSearch.get 13 | expect(quicksearches).to be_an(Array) 14 | expect(quicksearches.size).to eq(2) 15 | expect(s).to have_been_requested 16 | end 17 | end 18 | 19 | context '/searchtemplates/quicksearches/' do 20 | let(:id) { "20121128132106172132000004" } 21 | it "gets an individual quick search" do 22 | s = stub_api_get("/searchtemplates/quicksearches/#{id}", "search_templates/quick_searches/get.json") 23 | quicksearch = QuickSearch.find(id) 24 | expect(quicksearch).to be_an(QuickSearch) 25 | expect(s).to have_been_requested 26 | end 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /spec/unit/spark_api/models/property_types_spec.rb: -------------------------------------------------------------------------------- 1 | require './spec/spec_helper' 2 | 3 | describe PropertyTypes do 4 | 5 | it "should respond to get" do 6 | expect(PropertyTypes).to respond_to(:get) 7 | end 8 | 9 | describe "/propertytypes", :support do 10 | before(:each) do 11 | stub_auth_request 12 | end 13 | 14 | on_get_it "should return a list of property types" do 15 | stub_api_get("/propertytypes", "property_types/property_types.json") 16 | 17 | types = PropertyTypes.get 18 | expect(types).to be_an(Array) 19 | expect(types.count).to be(6) 20 | end 21 | end 22 | 23 | describe "/propertytypes/all", :support do 24 | before(:each) do 25 | stub_auth_request 26 | end 27 | 28 | on_get_it "should return a list of all property types" do 29 | stub_api_get("/propertytypes/all", "property_types/property_types.json") 30 | 31 | types = PropertyTypes.all 32 | expect(types).to be_an(Array) 33 | expect(types.count).to be(6) 34 | end 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /spec/fixtures/listings/tour_of_homes.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Results": [ 4 | {"ResourceUri":"/listings/20060725224713296297000000/tourofhomes/20101127153422574618000000", 5 | "Id": "20101127153422574618000000", 6 | "Date": "10/01/2010", 7 | "StartTime": "09:00-07:00", 8 | "EndTime": "12:00-07:00", 9 | "Comments": "Wonderful home; must see!", 10 | "AdditionalInfo": [{"Hosted By": "Joe Smith"}, {"Host Phone": "123-456-7890"}, {"Tour Area": "North-Central"}] 11 | }, 12 | {"ResourceUri":"/listings/20060725224713296297000000/tourofhomes/20101127153422174618000000", 13 | "Id": "20101127153422174618000000", 14 | "Date": "10/08/2010", 15 | "StartTime": "09:00-07:00", 16 | "EndTime": "12:00-07:00", 17 | "Comments": "Wonderful home; must see!", 18 | "AdditionalInfo": [{"Hosted By": "Joe Smith"}, {"Host Phone": "123-456-7890"}, {"Tour Area": "North-Central"}] 19 | } 20 | ], 21 | "Success": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /spec/support/account_examples.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for :account do |test_class| 2 | 3 | let(:account) { 4 | test_class.new({ 5 | "Id" => "12345", 6 | "Name" => "Agent McAgentson", 7 | "Office" => "Office Name", 8 | "Emails"=> [], 9 | "Phones"=> [], 10 | "Websites"=> [], 11 | "Addresses"=> [] 12 | }) 13 | } 14 | 15 | describe 'logo' do 16 | 17 | it 'returns the logo' do 18 | logo = SparkApi::Models::Base.new( {"Type" => "Logo"} ) 19 | not_logo = SparkApi::Models::Base.new( {"Type" => "Nope" } ) 20 | account.images = [logo, not_logo] 21 | expect(account.logo).to be logo 22 | end 23 | 24 | it 'returns nil if there is no logo' do 25 | not_logo = SparkApi::Models::Base.new( {"Type" => "Nope" } ) 26 | account.images = [not_logo] 27 | expect(account.logo).to be nil 28 | end 29 | 30 | it 'returns nil if there are no images' do 31 | expect(account.images).to be nil 32 | expect(account.logo).to be nil 33 | end 34 | 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /spec/fixtures/listings/reorder_photo.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Results": [ 4 | { 5 | "Uri300": "http://photos.flexmls.com/fgo/20050505220032167405000000.jpg", 6 | "ResourceUri": "/v1/listings/20050505200220759069000000/photos/20050505220032167405000000", 7 | "Name": "Front", 8 | "Primary": true, 9 | "Uri800": "http://cdn.resize.flexmls.com/fgo/800x600/true/20050505220032167405000000-o.jpg", 10 | "Id": "20050505220032167405000000", 11 | "UriLarge": "http://photos.flexmls.com/fgo/20050505220032167405000000-o.jpg", 12 | "Uri1024": "http://cdn.resize.flexmls.com/fgo/1024x768/true/20050505220032167405000000-o.jpg", 13 | "Caption": "", 14 | "Uri1280": "http://cdn.resize.flexmls.com/fgo/1280x1024/true/20050505220032167405000000-o.jpg", 15 | "Uri640": "http://cdn.resize.flexmls.com/fgo/640x480/true/20050505220032167405000000-o.jpg", 16 | "UriThumb": "http://photos.flexmls.com/fgo/20050505220032167405000000-t.jpg" 17 | } 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /spec/unit/spark_api/models/open_house_spec.rb: -------------------------------------------------------------------------------- 1 | require './spec/spec_helper' 2 | 3 | require 'time' 4 | 5 | describe OpenHouse do 6 | subject do 7 | OpenHouse.new( 8 | 'ResourceUri'=>"/v1/listings/20060412165917817933000000/openhouses/20101127153422574618000000", 9 | 'Id'=>"20060412165917817933000000", 10 | 'Date'=>"10/01/2010", 11 | 'StartTime'=>"9:00 am", 12 | 'EndTime'=>"12:00 pm" 13 | ) 14 | end 15 | 16 | it "should respond to a few methods" do 17 | expect(subject.class).to respond_to(:find_by_listing_key) 18 | end 19 | 20 | context "/listings//openhouses", :support do 21 | on_get_it "should get open house for a listing" do 22 | stub_auth_request 23 | stub_api_get('/listings/20060412165917817933000000/openhouses','listings/open_houses.json') 24 | houses = subject.class.find_by_listing_key('20060412165917817933000000') 25 | expect(houses).to be_an(Array) 26 | expect(houses.length).to eq(2) 27 | expect(houses.first.Id).to eq("20101127153422574618000000") 28 | end 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /lib/spark_api/models/shared_listing.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class SharedListing < Base 4 | extend Finders 5 | self.element_name="sharedlistings" 6 | 7 | def ListingIds=(listing_ids) 8 | attributes["ListingIds"] = Array(listing_ids) 9 | end 10 | def ViewId=(id) 11 | attributes["ViewId"] = id 12 | end 13 | 14 | def save(arguments={}) 15 | begin 16 | return save!(arguments) 17 | rescue BadResourceRequest => e 18 | rescue NotFound => e 19 | # log and leave 20 | SparkApi.logger.error("Failed to save SharedListing #{self}: #{e.message}") 21 | end 22 | false 23 | end 24 | def save!(arguments={}) 25 | results = connection.post self.class.path, attributes, arguments 26 | result = results.first 27 | attributes['Id'] = result['Id'] 28 | attributes['Mode'] = result['Mode'] 29 | attributes['ResourceUri'] = result['ResourceUri'] 30 | attributes['SharedUri'] = result['SharedUri'] 31 | true 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/spark_api/models/news_feed_meta.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class NewsFeedMeta < Base 4 | 5 | self.element_name = "newsfeeds/meta" 6 | 7 | def minimum_core_fields 8 | data['Subscriptions']['SavedSearches']['MinimumCoreFields'] 9 | end 10 | 11 | def core_field_names 12 | fields = data['Subscriptions']['SavedSearches']['CoreSearchFields'].dup 13 | 14 | data['Subscriptions']['SavedSearches']['CoreStandardFields'].each do |field| 15 | fields << field[1]['Label'] 16 | end 17 | 18 | fields 19 | end 20 | 21 | def core_fields 22 | fields = data['Subscriptions']['SavedSearches']['CoreSearchFields'].dup 23 | 24 | data['Subscriptions']['SavedSearches']['CoreStandardFields'].each do |field| 25 | fields << field.first 26 | end 27 | 28 | fields 29 | end 30 | 31 | private 32 | 33 | def data 34 | if attributes.empty? 35 | @data ||= connection.get(self.path).first 36 | else 37 | attributes 38 | end 39 | end 40 | 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/unit/spark_api/authentication/oauth2_impl/faraday_middleware_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'support/oauth2_helper' 3 | 4 | describe SparkApi::Authentication::OAuth2Impl::SparkbarFaradayMiddleware do 5 | subject { SparkApi::Authentication::OAuth2Impl::SparkbarFaradayMiddleware.new("test") } 6 | # Make sure the client boostraps the right plugin based on configuration. 7 | it "should parse token on successful response" do 8 | env = { 9 | :body => '{"token":"sp4rkb4rt0k3n"}', 10 | :status => 201 11 | } 12 | subject.on_complete env 13 | expect(env[:body]["token"]).to eq("sp4rkb4rt0k3n") 14 | end 15 | 16 | it "should raise error on unsuccessful response" do 17 | env = { 18 | :body => '{"token":"sp4rkb4rt0k3n"}', 19 | :status => 500 20 | } 21 | expect {subject.on_complete env }.to raise_error(SparkApi::ClientError) 22 | end 23 | 24 | it "should raise error on invalid json" do 25 | env = { 26 | :body => '{"BORKBORKBORK"}', 27 | :status => 200 28 | } 29 | expect {subject.on_complete env }.to raise_error(MultiJson::DecodeError) 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /spec/fixtures/newsfeeds/get.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "Id": "20130625195235039712000000", 7 | "ResourceUri": "/vX/newsfeeds/20130625195235039712000000", 8 | "Active": true, 9 | "NotificationsActive": true, 10 | "Curated": false, 11 | "Name": "My Newsfeed Subscription", 12 | "Type": "SavedSearch", 13 | "NotificationMethods": ["Email"], 14 | "CreatedTimestamp": "2013-06-27T10:01:06-05:00", 15 | "ModificationTimestamp": "2013-06-27T10:01:06-05:00", 16 | "LastEventTimestamp": "2013-07-07T12:12:06-05:00", 17 | "ExpirationDate": "2014-10-10", 18 | "Subscription": { 19 | "ResourceUri": "/vX/savedsearches/20100815220615294367000000", 20 | "Id": "20100815220615294367000000", 21 | "OwnerId": "20090815223215294334000000", 22 | "Name": "Search name here", 23 | "Description": "A longer description of the search", 24 | "Filter": null, 25 | "ModificationTimestamp": "2011-03-14T08:39:38-05:00" 26 | } 27 | } 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/spark_api/configuration/oauth2_configurable.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Configuration 3 | module OAuth2Configurable 4 | def convert_to_oauth2? 5 | (self.authentication_mode == SparkApi::Authentication::OAuth2 || 6 | self.authentication_mode == SparkApi::Authentication::OpenId || 7 | self.authentication_mode == SparkApi::Authentication::OpenIdOAuth2Hybrid) && 8 | self.oauth2_provider.nil? 9 | end 10 | 11 | def oauth2_enabled? 12 | self.authentication_mode == SparkApi::Authentication::OAuth2 13 | end 14 | 15 | def oauthify! 16 | self.oauth2_provider = SparkApi::Authentication::SimpleProvider.new( 17 | :access_uri => grant_uri, 18 | :client_id => self.api_key, 19 | :client_secret => self.api_secret, 20 | :authorization_uri => self.auth_endpoint, 21 | :redirect_uri => self.callback 22 | ) 23 | end 24 | 25 | def grant_uri 26 | e = self.endpoint.gsub(/\/+$/,"") 27 | v = self.version.gsub(/\/+/,"/") 28 | "#{e}/#{v}/oauth2/grant" 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/fixtures/newsfeeds/inactive.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "Id": "20130625195235039712000000", 7 | "ResourceUri": "/vX/newsfeeds/20130625195235039712000000", 8 | "Active": false, 9 | "NotificationsActive": true, 10 | "Curated": false, 11 | "Name": "My Newsfeed Subscription", 12 | "Type": "SavedSearch", 13 | "NotificationMethods": ["Email"], 14 | "CreatedTimestamp": "2013-06-27T10:01:06-05:00", 15 | "ModificationTimestamp": "2013-06-27T10:01:06-05:00", 16 | "LastEventTimestamp": "2013-07-07T12:12:06-05:00", 17 | "ExpirationDate": "2014-10-10", 18 | "Subscription": { 19 | "ResourceUri": "/vX/savedsearches/20100815220615294367000000", 20 | "Id": "20100815220615294367000000", 21 | "OwnerId": "20090815223215294334000000", 22 | "Name": "Search name here", 23 | "Description": "A longer description of the search", 24 | "Filter": null, 25 | "ModificationTimestamp": "2011-03-14T08:39:38-05:00" 26 | } 27 | } 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /spec/fixtures/listings/photos/post.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Results": [ 4 | { 5 | "Uri300": "http://photos.flexmls.com/fgo/20110826220032167405000000.jpg", 6 | "ResourceUri": "/v1/listings/20050505200220759069000000/photos/20110826220032167405000000", 7 | "Name": "Front", 8 | "Uri800": "http://cdn.resize.flexmls.com/fgo/800x600/true/20110826220032167405000000-o.jpg", 9 | "Id": "20110826220032167405000000", 10 | "UriLarge": "http://photos.flexmls.com/fgo/20110826220032167405000000-o.jpg", 11 | "Uri1024": "http://cdn.resize.flexmls.com/fgo/1024x768/true/20110826220032167405000000-o.jpg", 12 | "Caption": "Creaters of flexMLS!", 13 | "Uri1280": "http://cdn.resize.flexmls.com/fgo/1280x1024/true/20110826220032167405000000-o.jpg", 14 | "Uri640": "http://cdn.resize.flexmls.com/fgo/640x480/true/20110826220032167405000000-o.jpg", 15 | "UriThumb": "http://photos.flexmls.com/fgo/20110826220032167405000000-t.jpg" 16 | } 17 | ], 18 | "Success": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /spec/unit/spark_api/models/listing_meta_translations_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ListingMetaTranslations do 4 | 5 | before(:each) do 6 | stub_auth_request 7 | end 8 | 9 | context "/flexmls/propertytypes//translations", :support do 10 | 11 | on_get_it "gets all translations" do 12 | stub_api_get("/flexmls/propertytypes/A/translations", 'listing_meta_translations/get.json') 13 | 14 | translations = ListingMetaTranslations.for_property_type('A') 15 | expect(translations).to be_an(ListingMetaTranslations) 16 | expect(translations.StandardFields).to be_an(Hash) 17 | expect(translations.CustomFields).to be_an(Hash) 18 | end 19 | 20 | on_get_it "doesn't explode if there are no results" do 21 | stub_api_get("/flexmls/propertytypes/A/translations", 'no_results.json') 22 | 23 | translations = ListingMetaTranslations.for_property_type('A') 24 | expect(translations).to be_an(ListingMetaTranslations) 25 | expect(translations.StandardFields).to be_an(Hash) 26 | expect(translations.CustomFields).to be_an(Hash) 27 | end 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /lib/spark_api/models/message.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class Message < Base 4 | extend Finders 5 | self.element_name="messages" 6 | 7 | def save(arguments={}) 8 | begin 9 | return save!(arguments) 10 | rescue BadResourceRequest => e 11 | SparkApi.logger.warn("Failed to save resource #{self}: #{e.message}") 12 | rescue NotFound => e 13 | SparkApi.logger.error("Failed to save resource #{self}: #{e.message}") 14 | end 15 | false 16 | end 17 | 18 | def save!(arguments={}) 19 | results = connection.post self.class.path, {"Messages" => [ attributes ]}, arguments 20 | true 21 | end 22 | 23 | def replies(args = {}) 24 | arguments = {:_expand => "Body, Sender"}.merge(args) 25 | Message.collect(connection.get("#{self.class.path}/#{self.Id}/replies", arguments)) 26 | end 27 | 28 | def self.unread(args={}) 29 | collect(connection.get("#{path}/unread", args)) 30 | end 31 | 32 | def self.unread_count 33 | unread _pagination: 'count' 34 | end 35 | 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/fixtures/fields/order_a.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "Contract Information": [ 7 | { 8 | "Field": "23Patios", 9 | "Label": "Book Section", 10 | "Domain": "CustomFields", 11 | "Detail": true 12 | }, 13 | { 14 | "Field": "ListPrice", 15 | "Label": "List Price", 16 | "Domain": "StandardFields", 17 | "Detail": false 18 | } 19 | ] 20 | }, 21 | { 22 | "General Property Description": [ 23 | { 24 | "Field": "24Patios", 25 | "Label": "Street Number", 26 | "Domain": "CustomFields", 27 | "Detail": true 28 | }, 29 | { 30 | "Field": "MlsStatus", 31 | "Label": "Total Square Feet", 32 | "Domain": "StandardFields", 33 | "Detail": false 34 | } 35 | ] 36 | } 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/spark_api/client.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | # =API Client 3 | # Main class to setup and run requests on the API. A default client is accessible globally as 4 | # SparkApi::client if the global configuration has been set as well. Otherwise, this class may 5 | # be instantiated separately with the configuration information. 6 | class Client 7 | include Connection 8 | include Authentication 9 | include Request 10 | 11 | require File.expand_path('../configuration/oauth2_configurable', __FILE__) 12 | include Configuration::OAuth2Configurable 13 | 14 | attr_accessor :authenticator 15 | attr_accessor *Configuration::VALID_OPTION_KEYS 16 | 17 | # Constructor bootstraps the client with configuration and authorization class. 18 | # options - see Configuration::VALID_OPTION_KEYS 19 | def initialize(options={}) 20 | options = SparkApi.options.merge(options) 21 | Configuration::VALID_OPTION_KEYS.each do |key| 22 | send("#{key}=", options[key]) 23 | end 24 | # Instantiate the authentication class passed in. 25 | @authenticator = authentication_mode.send("new", self) 26 | end 27 | 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/unit/spark_api/models/rental_calendar_spec.rb: -------------------------------------------------------------------------------- 1 | require './spec/spec_helper' 2 | 3 | describe RentalCalendar do 4 | 5 | describe "/listings//rentalcalendar", :support do 6 | before do 7 | stub_auth_request 8 | stub_api_get('/listings/1234/rentalcalendar','listings/rental_calendar.json') 9 | end 10 | 11 | on_get_it "should get an array of rental calendars" do 12 | p = RentalCalendar.find_by_listing_key('1234') 13 | expect(p).to be_an(Array) 14 | expect(p.length).to eq(2) 15 | end 16 | 17 | end 18 | 19 | describe "test include_date method" do 20 | it "knows about included dates" do 21 | cal = RentalCalendar.new 22 | cal.StartDate = Date.parse("2012-07-12") 23 | cal.EndDate = Date.parse("2012-07-18") 24 | expect(cal.include_date?(Date.parse("2012-06-01"))).to be false 25 | expect(cal.include_date?(Date.parse("2012-07-12"))).to be true 26 | expect(cal.include_date?(Date.parse("2012-07-15"))).to be true 27 | expect(cal.include_date?(Date.parse("2012-07-18"))).to be true 28 | expect(cal.include_date?(Date.parse("2012-08-01"))).to be false 29 | end 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /spec/unit/spark_api/models/video_spec.rb: -------------------------------------------------------------------------------- 1 | require './spec/spec_helper' 2 | 3 | describe Video do 4 | 5 | it "responds to" do 6 | expect(Video).to respond_to(:find_by_listing_key) 7 | expect(Video.new).to respond_to(:branded?) 8 | expect(Video.new).to respond_to(:unbranded?) 9 | end 10 | 11 | it "has a type" do 12 | expect(Video.new(:Type => "branded").branded?).to eq(true) 13 | expect(Video.new(:Type => "unbranded").branded?).to eq(false) 14 | expect(Video.new(:Type => "unbranded").unbranded?).to eq(true) 15 | expect(Video.new(:Type => "branded").unbranded?).to eq(false) 16 | end 17 | 18 | describe "/listings//videos", :support do 19 | before do 20 | stub_auth_request 21 | stub_api_get('/listings/1234/videos','listings/videos_index.json') 22 | end 23 | 24 | on_get_it "should get an array of videos" do 25 | p = Video.find_by_listing_key('1234') 26 | expect(p).to be_an(Array) 27 | expect(p.length).to eq(2) 28 | end 29 | 30 | end 31 | 32 | context "/listings//videos/", :support do 33 | on_get_it "should return information about a single video" 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /spec/fixtures/sorts/get.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Results": [ 4 | { 5 | "ResourceUri": "/v1/searchtemplates/sorts/20120717212004874996000000", 6 | "Id": "20130717212004874996000000", 7 | "Name": "My Custom Listing Sort", 8 | "OwnerId": "20000426173054342350000000", 9 | "MlsId": "20000426143505724628000000", 10 | "Inheritable": true, 11 | "Inherited": false, 12 | "Fields": [ 13 | { 14 | "Domain": "StandardFields", 15 | "GroupField": null, 16 | "Field": "ListPrice", 17 | "SortType": "Ascending" 18 | }, 19 | { 20 | "Domain": "StandardFields", 21 | "GroupField": null, 22 | "Field": "MlsStatus", 23 | "SortType": "Descending" 24 | } 25 | ], 26 | "ModificationTimestamp": "2013-07-09T15:31:47Z" 27 | } 28 | ], 29 | "Success": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/spark_api/models/note.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class Note < Base 4 | extend Subresource 5 | self.element_name = "notes" # not sure this is really of any use... 6 | 7 | def self.get(options={}) 8 | ret = super(options) 9 | if ret.empty? 10 | return nil 11 | else 12 | return ret.first 13 | end 14 | end 15 | 16 | def save(arguments={}) 17 | begin 18 | return save!(arguments) 19 | rescue BadResourceRequest => e 20 | rescue NotFound => e 21 | # log and leave 22 | SparkApi.logger.error("Failed to save note #{self} (path: #{self.class.path}): #{e.message}") 23 | end 24 | false 25 | end 26 | 27 | def save!(args={}) 28 | args.merge(:Notes => attributes['Note']) 29 | results = connection.put(self.class.path, {:Note => attributes['Note']}, args) 30 | result = results.first 31 | attributes['ResourceUri'] = result['ResourceUri'] 32 | true 33 | end 34 | 35 | def delete(args={}) 36 | connection.delete(self.class.path, args) 37 | end 38 | 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/spark_api/models/vow_account.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class VowAccount < Base 4 | extend Finders 5 | include Concerns::Savable, 6 | Concerns::Destroyable 7 | 8 | self.element_name = "portal" 9 | 10 | def initialize(attributes={}) 11 | super(attributes) 12 | end 13 | 14 | def enabled? 15 | (@attributes['Settings'].class == Hash) && @attributes['Settings']['Enabled'] == 'true' 16 | end 17 | 18 | def enable 19 | change_setting :Enabled, 'true' 20 | save 21 | end 22 | 23 | def disable 24 | change_setting :Enabled, 'false' 25 | save 26 | end 27 | 28 | def change_password(new_password) 29 | attribute_will_change! 'Password' 30 | @attributes['Password'] = new_password 31 | save 32 | end 33 | 34 | def change_setting(key, val) 35 | attribute_will_change! "Settings" 36 | @attributes['Settings'] = {} if @attributes['Settings'].nil? || @attributes['Settings'] != Hash 37 | @attributes['Settings'][key.to_s] = val 38 | end 39 | 40 | def post_data; attributes end 41 | 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/fixtures/standardfields/stateorprovince.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [{ 5 | "StateOrProvince": { 6 | "ResourceUri": "/v1/standardfields/StateOrProvince", 7 | "FieldList": [{ 8 | "Name": "IA", 9 | "Applies To": ["A", "B", "G", "I", "J", "K", "M"], 10 | "Value": "IA" 11 | }, 12 | { 13 | "Name": "MN", 14 | "Value": "MN" 15 | }, 16 | { 17 | "Name": "ND", 18 | "Value": "ND" 19 | }, 20 | { 21 | "Name": "SD", 22 | "Applies To": ["A", "B", "G", "I", "J", "K", "M"], 23 | "Value": "SD" 24 | }, 25 | { 26 | "Name": "WI", 27 | "Applies To": ["A", "B", "G", "I", "J", "K", "M"], 28 | "Value": "WI" 29 | }], 30 | "HasList": true, 31 | "Searchable": true, 32 | "Type": "Character" 33 | } 34 | }] 35 | } 36 | } -------------------------------------------------------------------------------- /lib/spark_api/authentication/oauth2_impl/grant_type_refresh.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Authentication 3 | # OAuth2 authentication flow to refresh an access token 4 | module OAuth2Impl 5 | class GrantTypeRefresh < GrantTypeBase 6 | attr_accessor :params 7 | def initialize(client, provider, session) 8 | super(client, provider, session) 9 | @params = {} 10 | end 11 | 12 | def authenticate 13 | new_session = nil 14 | unless @session.refresh_token.nil? 15 | SparkApi.logger.debug { "[oauth2] Refreshing authentication to #{provider.access_uri} using [#{session.refresh_token}]" } 16 | new_session = create_session(token_params) 17 | end 18 | new_session 19 | end 20 | 21 | private 22 | def token_params 23 | hash = @params.merge({ 24 | "client_id" => @provider.client_id, 25 | "client_secret" => @provider.client_secret, 26 | "grant_type" => "refresh_token", 27 | "refresh_token"=> session.refresh_token, 28 | }) 29 | MultiJson.dump(hash) 30 | end 31 | end 32 | 33 | end 34 | end 35 | end 36 | 37 | -------------------------------------------------------------------------------- /spec/unit/spark_api/models/concerns/destroyable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class MyExampleModel < Base 4 | include Concerns::Destroyable 5 | self.prefix = "/test/" 6 | self.element_name = "example" 7 | end 8 | 9 | describe Concerns::Destroyable, "Destroyable Concern" do 10 | 11 | before :each do 12 | stub_auth_request 13 | stub_api_get("/test/example", 'base.json') 14 | @model = MyExampleModel.first 15 | end 16 | 17 | describe 'destroyed?' do 18 | 19 | it "should not be destroyed" do 20 | expect(@model.destroyed?).to eq(false) 21 | end 22 | end 23 | 24 | describe 'destroy' do 25 | 26 | it "should be destroyable" do 27 | stub_api_delete("/some/place/20101230223226074201000000") 28 | @model = MyExampleModel.first 29 | @model.destroy 30 | expect(@model.destroyed?).to eq(true) 31 | end 32 | 33 | end 34 | 35 | describe 'destroy class method' do 36 | 37 | it "allows you to destroy with only the id" do 38 | stub_api_delete("/test/example/20101230223226074201000000") 39 | MyExampleModel.destroy('20101230223226074201000000') 40 | expect_api_request(:delete, "/test/example/20101230223226074201000000").to have_been_made.once 41 | end 42 | 43 | end 44 | 45 | end 46 | -------------------------------------------------------------------------------- /spec/unit/spark_api/authentication_spec.rb: -------------------------------------------------------------------------------- 1 | require './spec/spec_helper' 2 | 3 | describe SparkApi::Authentication do 4 | it "should give me a session object" do 5 | stub_auth_request 6 | stub_request(:get, "#{SparkApi.endpoint}/#{SparkApi.version}/session/c401736bf3d3f754f07c04e460e09573"). 7 | with(:query => { 8 | :ApiSig => "d4cea51b4a6b9eb930e4320866aae7d0", 9 | :ApiUser => "foobar", 10 | :AuthToken => "c401736bf3d3f754f07c04e460e09573" 11 | }). 12 | to_return(:body => fixture("session.json")) 13 | client = SparkApi.client 14 | stub_auth_request 15 | session = client.get "/session/c401736bf3d3f754f07c04e460e09573" 16 | expect(session[0]["AuthToken"]).to eq("c401736bf3d3f754f07c04e460e09573") 17 | end 18 | it "should delete a session" do 19 | stub_auth_request 20 | stub_request(:delete, "#{SparkApi.endpoint}/#{SparkApi.version}/session/c401736bf3d3f754f07c04e460e09573"). 21 | with(:query => { 22 | :ApiSig => "d4cea51b4a6b9eb930e4320866aae7d0", 23 | :ApiUser => "foobar", 24 | :AuthToken => "c401736bf3d3f754f07c04e460e09573" 25 | }). 26 | to_return(:body => fixture("success.json")) 27 | client = SparkApi.client 28 | client.logout 29 | expect(client.session).to eq(nil) 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /lib/spark_api/models/concerns/destroyable.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | module Concerns 4 | 5 | module Destroyable 6 | 7 | def self.included(base) 8 | base.extend(ClassMethods) 9 | end 10 | 11 | module ClassMethods 12 | 13 | def destroy(id, arguments = {}) 14 | connection.delete("#{path}/#{id}", arguments) 15 | end 16 | 17 | end 18 | 19 | 20 | def destroy(arguments = {}) 21 | self.errors = [] 22 | begin 23 | return destroy!(arguments) 24 | rescue BadResourceRequest => e 25 | self.errors << {:code => e.code, :message => e.message} 26 | SparkApi.logger.error("Failed to destroy resource #{self}: #{e.message}") 27 | rescue NotFound => e 28 | SparkApi.logger.error("Failed to destroy resource #{self}: #{e.message}") 29 | end 30 | false 31 | end 32 | def destroy!(arguments = {}) 33 | connection.delete(resource_uri, arguments) if persisted? 34 | @destroyed = true 35 | true 36 | end 37 | alias_method :delete, :destroy # backwards compatibility 38 | 39 | def destroyed?; @destroyed ? @destroyed : false end 40 | 41 | end 42 | 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/unit/spark_api/models/tour_of_home_spec.rb: -------------------------------------------------------------------------------- 1 | require './spec/spec_helper' 2 | 3 | require 'time' 4 | 5 | describe TourOfHome do 6 | subject do 7 | TourOfHome.new( 8 | 'ResourceUri'=>"/listings/20060725224713296297000000/tourofhomes/20101127153422574618000000", 9 | 'Id'=>"20101127153422574618000000", 10 | 'Date'=>"10/01/2010", 11 | 'StartTime'=>"9:00 am", 12 | 'EndTime'=>"11:00 pm", 13 | 'Comments'=>"Wonderful home; must see!", 14 | 'AdditionalInfo'=> [{"Hosted By"=>"Joe Smith"}, {"Host Phone"=>"123-456-7890"}, {"Tour Area"=>"North-Central"}] 15 | ) 16 | end 17 | 18 | it "should respond to a few methods" do 19 | expect(subject.class).to respond_to(:find_by_listing_key) 20 | end 21 | 22 | context "/listings//tourofhomes", :support do 23 | on_get_it "should get home tours for a listing" do 24 | stub_auth_request 25 | stub_api_get('/listings/20060725224713296297000000/tourofhomes','listings/tour_of_homes.json') 26 | v = subject.class.find_by_listing_key('20060725224713296297000000') 27 | expect(v).to be_an(Array) 28 | expect(v.length).to eq(2) 29 | end 30 | end 31 | 32 | context "/listings//tourofhomes/", :support do 33 | on_get_it "should get information for a single tour of a home" 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /spec/unit/spark_api/primary_array_spec.rb: -------------------------------------------------------------------------------- 1 | require './spec/spec_helper' 2 | 3 | class PrimaryModel 4 | include SparkApi::Primary 5 | attr_accessor :Primary, :id, :attributes 6 | def initialize(id, prime = false) 7 | @id = id 8 | @Primary = prime 9 | @attributes = {"Primary" => prime } 10 | end 11 | end 12 | 13 | describe SparkApi::PrimaryArray do 14 | it "should give me the primary element" do 15 | a = PrimaryModel.new(1) 16 | b = PrimaryModel.new(2) 17 | c = PrimaryModel.new(3) 18 | d = PrimaryModel.new(4, true) 19 | e = PrimaryModel.new(5) 20 | tester = subject.class.new([d,e]) 21 | expect(tester.primary).to eq(d) 22 | tester = subject.class.new([a,b,c,d,e]) 23 | expect(tester.primary).to eq(d) 24 | # Note, it doesn't care if there is more than one primary, just returns first in the list. 25 | b.Primary = true 26 | expect(tester.primary).to eq(b) 27 | end 28 | it "should return nil when there is no primary element" do 29 | a = PrimaryModel.new(1) 30 | b = PrimaryModel.new(2) 31 | c = PrimaryModel.new(3) 32 | d = PrimaryModel.new(4) 33 | e = PrimaryModel.new(5) 34 | tester = subject.class.new([]) 35 | expect(tester.primary).to be(nil) 36 | tester = subject.class.new([a,b,c,d,e]) 37 | expect(tester.primary).to be(nil) 38 | end 39 | end 40 | 41 | 42 | -------------------------------------------------------------------------------- /spec/fixtures/saved_searches/with_newsfeed.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "ResourceUri": "/v1/savedsearches/20100815220615294367000000", 7 | "Id": "20100815220615294367000000", 8 | "Name": "Search name here", 9 | "ContactIds": [ 10 | "20100815220615294367000000" 11 | ], 12 | "NewsFeedSubscriptionSummary": { 13 | "ActiveSubscription": true 14 | }, 15 | "NewsFeeds": [ 16 | { 17 | "ResourceUri": "/v1/newsfeeds/20141203082619076323088319", 18 | "Name": "My Very Special Newsfeed", 19 | "OwnerId": "20000426143505724628000000", 20 | "ProspectLinkId": "", 21 | "CreatedTimestamp": "2014-12-03T08:26:19Z", 22 | "Id": "20141203082619076323088319", 23 | "Active": "true", 24 | "ModificationTimestamp": "2014-12-03T08:26:19Z", 25 | "ExpirationDate": "2015-03-03", 26 | "Curated": "false", 27 | "NotificationMethods": ["Email"], 28 | "LastEventTimestamp": "", 29 | "Type": "SavedSearch" 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /spec/fixtures/saved_searches/with_inactive_newsfeed.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "ResourceUri": "/v1/savedsearches/20100815220615294367000000", 7 | "Id": "20100815220615294367000000", 8 | "Name": "Search name here", 9 | "ContactIds": [ 10 | "20100815220615294367000000" 11 | ], 12 | "NewsFeedSubscriptionSummary": { 13 | "ActiveSubscription": false 14 | }, 15 | "NewsFeeds": [ 16 | { 17 | "ResourceUri": "/v1/newsfeeds/20141203082619076323088319", 18 | "Name": "My Very Special Newsfeed", 19 | "OwnerId": "20000426143505724628000000", 20 | "ProspectLinkId": "", 21 | "CreatedTimestamp": "2014-12-03T08:26:19Z", 22 | "Id": "20141203082619076323088319", 23 | "Active": "false", 24 | "ModificationTimestamp": "2014-12-03T08:26:19Z", 25 | "ExpirationDate": "2015-03-03", 26 | "Curated": "false", 27 | "NotificationMethods": ["Email"], 28 | "LastEventTimestamp": "", 29 | "Type": "SavedSearch" 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/spark_api.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'logger' 3 | require 'multi_json' 4 | require 'addressable' 5 | 6 | require 'spark_api/version' 7 | require 'spark_api/errors' 8 | require 'spark_api/configuration' 9 | require 'spark_api/multi_client' 10 | require 'spark_api/authentication' 11 | require 'spark_api/response' 12 | require 'spark_api/paginate' 13 | require 'spark_api/request' 14 | require 'spark_api/connection' 15 | require 'spark_api/client' 16 | require 'spark_api/faraday_middleware' 17 | require 'spark_api/reso_faraday_middleware' 18 | require 'spark_api/primary_array' 19 | require 'spark_api/options_hash' 20 | require 'spark_api/models' 21 | 22 | module SparkApi 23 | extend Configuration 24 | extend MultiClient 25 | 26 | #:nocov: 27 | def self.logger 28 | if @logger.nil? 29 | @logger = Logger.new(STDOUT) 30 | @logger.level = Logger::INFO 31 | end 32 | @logger 33 | end 34 | 35 | def self.logger= logger 36 | @logger = logger 37 | end 38 | #:nocov: 39 | 40 | def self.client(opts={}) 41 | Thread.current[:spark_api_client] ||= SparkApi::Client.new(opts) 42 | end 43 | 44 | def self.method_missing(method, *args, &block) 45 | return super unless (client.respond_to?(method)) 46 | client.send(method, *args, &block) 47 | end 48 | 49 | def self.reset 50 | reset_configuration 51 | Thread.current[:spark_api_client] = nil 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /lib/spark_api/models/finders.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | # =Rails-like finders module 4 | # Adds the base set of finder class methods to the models that support them (not all of them do) 5 | module Finders 6 | 7 | def find(*arguments) 8 | scope = arguments.slice!(0) 9 | options = arguments.slice!(0) || {} 10 | case scope 11 | when nil then raise ArgumentError, "Argument for find() can't be nil" 12 | when :all then find_every(options) 13 | when :first then find_every(options).first 14 | when :last then find_every(options).last 15 | when :one then find_every(options.merge(:_limit => 1)).first 16 | else find_single(scope, options) 17 | end 18 | end 19 | 20 | def find_one(*arguments) 21 | find(:one, *arguments) 22 | end 23 | 24 | def first(*arguments) 25 | find(:first, *arguments) 26 | end 27 | 28 | def last(*arguments) 29 | find(:last, *arguments) 30 | end 31 | 32 | private 33 | 34 | def find_every(options) 35 | collect(connection.get("#{path}", options)) 36 | end 37 | 38 | def find_single(scope, options) 39 | resp = connection.get("#{path}/#{scope}", options) 40 | new(resp.first) 41 | end 42 | 43 | end 44 | end 45 | end 46 | 47 | -------------------------------------------------------------------------------- /lib/spark_api/models/shared_link.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class SharedLink < Base 4 | extend Finders 5 | 6 | attr_accessor :sort_id 7 | attr_writer :name 8 | 9 | self.element_name = "sharedlinks" 10 | 11 | def self.create(data) 12 | SharedLink.new SparkApi.client.post("#{path}/#{resource_type(data)}", data).first 13 | rescue SparkApi::BadResourceRequest 14 | false 15 | end 16 | 17 | def name 18 | if @name 19 | @name 20 | elsif respond_to?(:ListingCart) && self.ListingCart.respond_to?(:Name) 21 | self.ListingCart.Name 22 | elsif respond_to?(:SavedSearch) && self.SavedSearch.respond_to?(:Name) 23 | self.SavedSearch.Name 24 | end 25 | end 26 | 27 | def template 28 | if respond_to?(:SavedSearch) && self.SavedSearch.respond_to?(:template) 29 | self.SavedSearch.template 30 | end 31 | end 32 | 33 | def filter 34 | "SharedLink Eq '#{id}'" 35 | end 36 | 37 | def listing_search_role 38 | self.Mode.downcase.to_sym 39 | end 40 | 41 | private 42 | 43 | def self.resource_type(data) 44 | case data.keys[0].to_sym 45 | when :ListingIds; "listings" 46 | when :SearchId; "search" 47 | when :CartId; "cart" 48 | end 49 | end 50 | 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/fixtures/listings/videos_index.json: -------------------------------------------------------------------------------- 1 | { 2 | "D":{ 3 | "Results":[{ 4 | "ResourceUri":"/v1/listings/20060813163018826257000000/videos/20110103201412297033000000", 5 | "Name":"Ode to joy", 6 | "Id":"20110103201412297033000000", 7 | "Caption":"All awesome", 8 | "ObjectHtml":"" 9 | }, 10 | { 11 | "ResourceUri":"/v1/listings/20060813163018826257000000/videos/20110103201413206052000000", 12 | "Name":"Ode to joy", 13 | "Id":"20110103201413206052000000", 14 | "Caption":"All awesome", 15 | "ObjectHtml":"" 16 | }], 17 | "Success":true 18 | }} 19 | -------------------------------------------------------------------------------- /spec/unit/spark_api/models/finders_spec.rb: -------------------------------------------------------------------------------- 1 | require './spec/spec_helper' 2 | 3 | class MyResource < Base 4 | extend Finders 5 | self.element_name = "my_resource" 6 | end 7 | 8 | describe Finders, "Finders model" do 9 | 10 | before(:each) do 11 | stub_auth_request 12 | end 13 | 14 | it "should get first result" do 15 | stub_api_get("/my_resource", 'finders.json') 16 | resource = MyResource.first 17 | expect(resource.Id).to eq(1) 18 | end 19 | 20 | it "should get last result" do 21 | stub_api_get("/my_resource", 'finders.json') 22 | resource = MyResource.last 23 | expect(resource.Id).to eq(2) 24 | end 25 | 26 | it "should find one result" do 27 | stub_api_get("/my_resource", 'finders.json', { 28 | :_limit => 1, 29 | :_filter => "Something Eq 'dude'" 30 | }) 31 | resource = MyResource.find_one(:_filter => "Something Eq 'dude'") 32 | expect(resource.Id).to eq(1) 33 | end 34 | 35 | describe "find" do 36 | 37 | it "should throw an error if no argument is passed" do 38 | stub_api_get("/my_resource/", 'finders.json') 39 | expect { 40 | MyResource.find() 41 | }.to raise_error(ArgumentError) 42 | end 43 | 44 | it "should throw an error when the first argument is nil" do 45 | stub_api_get("/my_resource/", 'finders.json', {:_limit => 1}) 46 | expect { 47 | MyResource.find(nil, {:_limit => 1}) 48 | }.to raise_error(ArgumentError) 49 | end 50 | 51 | end 52 | 53 | end 54 | -------------------------------------------------------------------------------- /spec/unit/spark_api/models/virtual_tour_spec.rb: -------------------------------------------------------------------------------- 1 | require './spec/spec_helper' 2 | 3 | describe VirtualTour do 4 | before(:each) do 5 | @virtualtour = VirtualTour.new({ 6 | :Uri => "http://www.flexmls.com/", 7 | :ResourceUri => "/v1/listings/20060712220814669202000000/virtualtours/20110105165843978012000000", 8 | :Name => "My Branded Tour", 9 | :Id => "20110105165843978012000000", 10 | :Type => "branded" 11 | }) 12 | end 13 | 14 | it "should respond to a few methods" do 15 | expect(VirtualTour).to respond_to(:find_by_listing_key) 16 | expect(@virtualtour).to respond_to(:branded?) 17 | expect(@virtualtour).to respond_to(:unbranded?) 18 | end 19 | 20 | it "should know if it's branded" do 21 | expect(@virtualtour.branded?).to eq(true) 22 | expect(@virtualtour.unbranded?).to eq(false) 23 | end 24 | 25 | context "/listings//virtualtours", :support do 26 | on_get_it "should get virtual tours for a listing" do 27 | stub_auth_request 28 | stub_api_get('/listings/1234/virtualtours','listings/virtual_tours_index.json') 29 | 30 | v = VirtualTour.find_by_listing_key('1234') 31 | expect(v).to be_an(Array) 32 | expect(v.length).to eq(5) 33 | end 34 | end 35 | 36 | context "/listings//virtualtours/", :support do 37 | on_get_it "should return information about a specific virtual tour" 38 | end 39 | 40 | after(:each) do 41 | @virtualtour = nil 42 | end 43 | 44 | end 45 | -------------------------------------------------------------------------------- /spec/config/spark_api/test_oauth.yml: -------------------------------------------------------------------------------- 1 | development: 2 | oauth2: true 3 | authorization_uri: 'https://developers.sparkplatform.com/oauth2' 4 | access_uri: 'https://developers.sparkapi.com/v1/oauth2/grant' 5 | redirect_uri: 'http://localhost/oauth2/callback' 6 | client_id: 'developmentid124nj4qu3pua' 7 | client_secret: 'developmentsecret4orkp29f' 8 | sparkbar_uri: 'https://dev.sparkplatform.com/appbar/authorize' 9 | endpoint: 'https://developers.sparkapi.com' 10 | oauth2_provider: 'SparkApi::TestOAuth2Provider' 11 | 12 | test: 13 | oauth2: true 14 | authorization_uri: 'https://developers.sparkplatform.com/oauth2' 15 | access_uri: 'https://developers.sparkapi.com/v1/oauth2/grant' 16 | redirect_uri: 'http://localhost/oauth2/callback' 17 | client_id: 'developmentid124nj4qu3pua' 18 | client_secret: 'developmentsecret4orkp29f' 19 | endpoint: 'https://developers.sparkapi.com' 20 | oauth2_provider: 'SparkApi::TestOAuth2Provider' 21 | 22 | production: 23 | oauth2: true 24 | authorization_uri: 'https://sparkplatform.com/oauth2' 25 | access_uri: 'https://api.sparkapi.com/v1/oauth2/grant' 26 | redirect_uri: 'http://localhost/oauth2/callback' 27 | client_id: 'production1id124nj4qu3pua' 28 | client_secret: 'productionsecret4orkp29fv' 29 | sparkbar_uri: 'https://sparkplatform.com/appbar/authorize' 30 | endpoint: 'https://api.sparkapi.com' 31 | 32 | -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | == v1.3.0 2013-01-08 2 | * yajl is no longer a dependency. To use it, make sure the gem is installed. 3 | * JRuby support finalized 4 | * Increased default OAuthSession timeout to 12 hours 5 | * New API Support: 6 | * field ordering service 7 | * attaching/detaching contacts to saved searches, 8 | * agent portal administration 9 | * contact commenting 10 | * contact export, export all 11 | * contact vow accounts 12 | * activity stream retrieval 13 | == v1.2.0 2012-11-14 14 | * SSL verification enabled by default 15 | * Sparkbar token access support 16 | * Fixed oauth2 CLI usage 17 | * Model behaviors for CRUD operations 18 | * Dirty module to help manage dirty attributes for updating a model 19 | * Removed nonsensical "all" method for finder models 20 | * ApiUser support for OAuth2 flow 21 | * Subscriptions models 22 | * Single session OAuth2 support 23 | * Update RecipientIds attribute on subscribe/unsubscribe 24 | * Restrict API wrapped json post data for hashes that have content 25 | * Fix sorting on nils in accounts model 26 | * Pluralize module 27 | == v1.1.0 2012-08-07 28 | * Upgraded faraday and other gems, cleand up the middleware 29 | == v1.0.4 2012-07-26 30 | * Moved to github for all development 31 | == v1.0.3 2012-07-09 32 | * Rental calendar 33 | == v1.0.2 2012-05-30 34 | * OpenId/Hybrid flow support 35 | * OAuthSession does not support symbols as init 36 | == v1.0.1 2012-04-30 37 | * Fixed yaml config files 38 | == v1.0.0 2012-04-20 39 | * Switch to spark api branding from flexmls_api 40 | -------------------------------------------------------------------------------- /lib/spark_api/authentication/base_auth.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | 3 | module Authentication 4 | #=Authentication Base 5 | # This base class defines the basic interface supported by all client authentication 6 | # implementations. 7 | class BaseAuth 8 | attr_accessor :session 9 | # All ihheriting classes should accept the spark_api client as a part of initialization 10 | def initialize(client) 11 | @client = client 12 | end 13 | 14 | # Perform requests to authenticate the client with the API 15 | def authenticate 16 | raise "Implement me!" 17 | end 18 | 19 | # Called prior to running authenticate (except in case of api authentication errors) 20 | def authenticated? 21 | !(session.nil? || session.expired?) 22 | end 23 | 24 | # Terminate the active session 25 | def logout 26 | raise "Implement me!" 27 | end 28 | 29 | # Perform an HTTP request (no data) 30 | def request(method, path, body, options) 31 | raise "Implement me!" 32 | end 33 | 34 | # Format a hash as request parameters 35 | # 36 | # :returns: 37 | # Stringized form of the parameters as needed for an HTTP request 38 | def build_url_parameters(parameters={}) 39 | array = parameters.map do |key,value| 40 | escaped_value = CGI.escape("#{value}") 41 | "#{key}=#{escaped_value}" 42 | end 43 | array.join "&" 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/spark_api/models/dirty.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | module Dirty 4 | 5 | def changed? 6 | changed.any? 7 | end 8 | 9 | def changed 10 | changed_attributes.keys 11 | end 12 | 13 | def changes 14 | Hash[changed.map { |attr| [attr, attribute_change(attr)] }] 15 | end 16 | 17 | def previous_changes 18 | @previously_changed 19 | end 20 | 21 | # hash with changed attributes and their original values 22 | def changed_attributes 23 | @changed_attributes ||= {} 24 | end 25 | 26 | # hash with changed attributes and their new values 27 | def dirty_attributes 28 | changed.inject({}) { |h, k| h[k] = attributes[k.to_s]; h } 29 | end 30 | 31 | private 32 | 33 | def reset_dirty 34 | @previously_changed = changed_attributes 35 | @changed_attributes.clear 36 | end 37 | 38 | def attribute_changed?(attr) 39 | changed.include?(attr) 40 | end 41 | 42 | def attribute_change(attr) 43 | [changed_attributes[attr], @attributes[attr.to_s]] if attribute_changed?(attr) 44 | end 45 | 46 | def attribute_will_change!(attr) 47 | attr = attr.to_s 48 | begin 49 | value = @attributes[attr] 50 | value = value.duplicable? ? value.clone : value 51 | rescue TypeError, NoMethodError; end 52 | 53 | changed_attributes[attr] = value unless changed.include?(attr) 54 | end 55 | 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/spark_api/models/notification.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class Notification < Base 4 | 5 | self.element_name = 'notifications' 6 | 7 | def save(arguments={}) 8 | self.errors = [] # clear the errors hash 9 | begin 10 | return save!(arguments) 11 | rescue BadResourceRequest => e 12 | self.errors << {:code => e.code, :message => e.message} 13 | SparkApi.logger.warn("Failed to save resource #{self}: #{e.message}") 14 | rescue NotFound => e 15 | SparkApi.logger.error("Failed to save resource #{self}: #{e.message}") 16 | end 17 | false 18 | end 19 | 20 | def save!(arguments={}) 21 | results = connection.post self.class.path, attributes, arguments 22 | result = results.first 23 | attributes['ResourceUri'] = result['ResourceUri'] 24 | true 25 | end 26 | 27 | def self.unread() 28 | # force pagination so response knows to deal with returned pagination info 29 | result = connection.get "#{self.path}/unread", {:_pagination => 'count'} 30 | result 31 | end 32 | 33 | def self.mark_read(notifications, arguments={}) 34 | notifications = Array(notifications) 35 | 36 | ids = notifications.map { |n| n.respond_to?('Id') ? n.Id : n } 37 | result = connection.put "#{self.path}/#{ids.join(',')}", {'Read' => true}, arguments 38 | end 39 | 40 | def listing_search_role 41 | :public 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/spark_api/response.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | # API Response interface 3 | module Response 4 | ATTRIBUTES = [:code, :message, :results, :success, :pagination, :details, :d, :errors, :sparkql_errors, :request_id] 5 | attr_accessor *ATTRIBUTES 6 | def success? 7 | @success 8 | end 9 | end 10 | 11 | # Nice and handy class wrapper for the api response hash 12 | class ApiResponse < ::Array 13 | MAGIC_D = 'D' 14 | MESSAGE = 'Message' 15 | CODE = 'Code' 16 | RESULTS = 'Results' 17 | SUCCESS = 'Success' 18 | PAGINATION = 'Pagination' 19 | DETAILS = 'Details' 20 | ERRORS = 'Errors' 21 | SPARKQL_ERRORS = 'SparkQLErrors' 22 | include SparkApi::Response 23 | def initialize d, request_id=nil 24 | begin 25 | self.d = d[MAGIC_D] 26 | if self.d.nil? || self.d.empty? 27 | raise InvalidResponse, "The server response could not be understood" 28 | end 29 | self.message = self.d[MESSAGE] 30 | self.code = self.d[CODE] 31 | self.results = Array(self.d[RESULTS]) 32 | self.success = self.d[SUCCESS] 33 | self.pagination = self.d[PAGINATION] 34 | self.details = self.d[DETAILS] || [] 35 | self.errors = self.d[ERRORS] 36 | self.sparkql_errors = self.d[SPARKQL_ERRORS] 37 | self.request_id = request_id 38 | super(results) 39 | rescue Exception => e 40 | SparkApi.logger.error "Unable to understand the response! #{d}" 41 | raise 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/support/mock_helper.rb: -------------------------------------------------------------------------------- 1 | def mock_session() 2 | SparkApi::Authentication::Session.new("AuthToken" => "1234", "Expires" => (Time.now + 3600).to_s, "Roles" => "['idx']") 3 | end 4 | 5 | def mock_oauth_session() 6 | SparkApi::Authentication::OAuthSession.new("access_token" => "1234", "expires_in" => 3600, "scope" => nil, "refresh_token"=> "1000refresh") 7 | end 8 | 9 | class MockClient < SparkApi::Client 10 | attr_accessor :connection 11 | def connection(ssl = false) 12 | @connection 13 | end 14 | end 15 | 16 | class MockApiAuthenticator < SparkApi::Authentication::ApiAuth 17 | # Sign a request 18 | def sign(sig) 19 | "SignedToken" 20 | end 21 | end 22 | 23 | def mock_client(stubs) 24 | c = MockClient.new 25 | c.session = mock_session() 26 | c.connection = test_connection(stubs) 27 | c 28 | end 29 | 30 | def mock_expired_session() 31 | SparkApi::Authentication::Session.new("AuthToken" => "1234", "Expires" => (Time.now - 60).to_s, "Roles" => "['idx']") 32 | end 33 | 34 | def test_connection(stubs) 35 | Faraday.new(nil, {:headers => SparkApi::Client.new.headers}) do |conn| 36 | conn.response :spark_api 37 | conn.adapter :test, stubs 38 | end 39 | end 40 | 41 | def stub_auth_request() 42 | stub_request(:post, "#{SparkApi.endpoint}/#{SparkApi.version}/session"). 43 | with(:query => {:ApiKey => "", :ApiSig => "806737984ab19be2fd08ba36030549ac"}). 44 | to_return(:body => fixture("session.json")) 45 | end 46 | 47 | def fixture(file) 48 | File.new(File.expand_path("../../fixtures", __FILE__) + '/' + file) 49 | end 50 | 51 | -------------------------------------------------------------------------------- /lib/spark_api/authentication/oauth2_impl/grant_type_password.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Authentication 3 | module OAuth2Impl 4 | # OAuth2 authentication flow using username and password parameters for the user in the 5 | # request. This implementation is geared towards authentication styles for native 6 | # applications that need to use OAuth2 7 | class GrantTypePassword < GrantTypeBase 8 | def initialize(client, provider, session) 9 | super(client, provider, session) 10 | end 11 | def authenticate 12 | new_session = nil 13 | if needs_refreshing? 14 | new_session = refresh 15 | end 16 | return new_session unless new_session.nil? 17 | create_session(token_params) 18 | end 19 | 20 | def refresh() 21 | GrantTypeRefresh.new(client,provider,session).authenticate 22 | rescue ClientError => e 23 | SparkApi.logger.info("[oauth2] Refreshing token failed, the library will try and authenticate from scratch: #{e.message}") 24 | nil 25 | end 26 | 27 | private 28 | def token_params 29 | hash = { 30 | "client_id" => @provider.client_id, 31 | "client_secret" => @provider.client_secret, 32 | "grant_type" => "password", 33 | "password" => @provider.password, 34 | "username" => @provider.username 35 | } 36 | MultiJson.dump(hash) 37 | end 38 | end 39 | end 40 | end 41 | end 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /spec/fixtures/comments/get.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Results": [ 4 | { 5 | "CreatedTimestamp":"2012-11-14T10:02:01-06:00", 6 | "UserId":"20000426143505724628000000", 7 | "ReferenceId":"20110105173455471471000000", 8 | "ResourceUri":"/v1/activities/20121128132106172132000004/comments/20121128133936712557000097", 9 | "UserUri":"/v1/accounts/20000426143505724628000000", 10 | "ModificationTimestamp":"2012-11-14T10:02:01-06:00", 11 | "ReferenceType":"Contact", 12 | "Comment":"This is a comment.", 13 | "ReferenceUri":"/v1/contacts/20110105173455471471000000", 14 | "UserType":"Mls", 15 | "Id":"20121128133936712557000097" 16 | }, 17 | { 18 | "CreatedTimestamp":"2012-11-14T10:02:01-06:00", 19 | "UserId":"20000426143505724628000000", 20 | "ReferenceId":"20110105173455471471000000", 21 | "ResourceUri":"/v1/activities/20121128132106172132000004/comments/20121128133936712557000098", 22 | "UserUri":"/v1/accounts/20000426143505724628000000", 23 | "ModificationTimestamp":"2012-11-14T10:02:01-06:00", 24 | "ReferenceType":"Contact", 25 | "Comment":"This is another comment.", 26 | "ReferenceUri":"/v1/contacts/20110105173455471471000000", 27 | "UserType":"Mls", 28 | "Id":"20121128133936712557000098" 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/spark_api/authentication.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'date' 3 | 4 | require 'spark_api/authentication/base_auth' 5 | require 'spark_api/authentication/api_auth' 6 | require 'spark_api/authentication/oauth2' 7 | 8 | module SparkApi 9 | # =Authentication 10 | # Mixin module for handling client authentication and reauthentication to the spark api. Makes 11 | # use of the configured authentication mode (API Auth by default). 12 | module Authentication 13 | 14 | # Main authentication step. Run before any api request unless the user session exists and is 15 | # still valid. 16 | # 17 | # *returns* 18 | # The user session object when authentication succeeds 19 | # *raises* 20 | # SparkApi::ClientError when authentication fails 21 | def authenticate 22 | start_time = Time.now 23 | request_time = Time.now - start_time 24 | new_session = @authenticator.authenticate 25 | SparkApi.logger.info { "[#{(request_time * 1000).to_i}ms]" } 26 | SparkApi.logger.debug { "Session: #{new_session.inspect}" } 27 | new_session 28 | end 29 | 30 | # Test to see if there is an active session 31 | def authenticated? 32 | @authenticator.authenticated? 33 | end 34 | 35 | # Delete the current session 36 | def logout 37 | SparkApi.logger.info { "Logging out." } 38 | @authenticator.logout 39 | end 40 | 41 | # Fetch the active session object 42 | def session 43 | @authenticator.session 44 | end 45 | # Save the active session object 46 | def session=(active_session) 47 | @authenticator.session=active_session 48 | end 49 | 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/unit/spark_api/models/portal_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Portal do 4 | 5 | before :each do 6 | stub_auth_request 7 | end 8 | 9 | context "/portal" do 10 | 11 | it "should return a new portal if the current user doesn't have one yet" do 12 | stub_api_get("/portal", "portal/my_non_existant.json") 13 | portal = Portal.my 14 | expect(portal.persisted?).to eq(false) 15 | end 16 | 17 | it "should get the current user's portal" do 18 | stub_api_get("/portal", "portal/my.json") 19 | portal = Portal.my 20 | expect(portal.persisted?).to eq(true) 21 | end 22 | 23 | it "should create a portal for the current user" do 24 | stub_api_post("/portal", "portal/new.json", "portal/post.json") 25 | portal = Portal.new({ 26 | "DisplayName" => "GreatPortal", 27 | "Enabled" => true, 28 | "RequiredFields" => [ "Address", "Phone" ] 29 | }) 30 | portal.save 31 | end 32 | 33 | it "should enable the current user's portal" do 34 | stub_api_get("/portal", "portal/my.json") 35 | s = stub_api_put("/portal/20100912153422758914000000", "portal/enable.json", "portal/post.json") 36 | portal = Portal.my 37 | portal.enable 38 | expect(s).to have_been_requested 39 | end 40 | 41 | it "should disable the current user's portal" do 42 | stub_api_get("/portal", "portal/my.json") 43 | s = stub_api_put("/portal/20100912153422758914000000", "portal/disable.json", "portal/post.json") 44 | portal = Portal.my 45 | portal.disable 46 | expect(s).to have_been_requested 47 | end 48 | 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/fixtures/rules/get.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "Id": 25, 7 | "ResourceUri": "/v1/listings/rules/25", 8 | "PropertyType": "A", 9 | "Domain": "StandardFields", 10 | "Group": null, 11 | "Field": "BathsTotal", 12 | "Order": 1, 13 | "Action": "SET_REQUIRED", 14 | "Expression": "StandardStatus = 'Active'", 15 | "ModificationTimestamp": "2018-05-07T19:03:07Z", 16 | "CreatedTimestamp": "2017-12-20T19:00:30Z", 17 | "ApprovalStatus": "Published", 18 | "Editable": true, 19 | "Status": "Fatal" 20 | }, 21 | { 22 | "Id": 370, 23 | "ResourceUri": "/v1/listings/rules/370", 24 | "PropertyType": "A", 25 | "Domain": "StandardFields", 26 | "Group": null, 27 | "Field": "City", 28 | "Order": 1, 29 | "Action": "SET_REQUIRED", 30 | "Expression": ".FALSE.", 31 | "ModificationTimestamp": "2018-05-01T15:26:47Z", 32 | "CreatedTimestamp": "2018-01-24T16:01:14Z", 33 | "ApprovalStatus": "Published", 34 | "Editable": true, 35 | "Status": "Fatal" 36 | } 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /spec/fixtures/listings/no_subresources.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Results": [ 4 | { 5 | "ResourceUri": "/v1/listings/20060725224713296297000000", 6 | "StandardFields": { 7 | "StreetNumber": "7298", 8 | "Longitude": "-116.3237", 9 | "City": "Bonners Ferry", 10 | "ListingId": "06-9395", 11 | "PublicRemarks": "Afforadable home in town close to hospital,yet quiet country like setting. Good views. Must see", 12 | "BuildingAreaTotal": "924.0", 13 | "YearBuilt": 1977, 14 | "StreetName": "BIRCH", 15 | "ListPrice": "50000.0", 16 | "PostalCode": "83805", 17 | "Latitude": "48.7001", 18 | "BathsThreeQuarter": null, 19 | "BathsFull": null, 20 | "BathsTotal": "1.0", 21 | "StateOrProvince": "ID", 22 | "PropertyType": "A", 23 | "StreetAdditionalInfo": null, 24 | "StreetDirPrefix": null, 25 | "BedsTotal": 3, 26 | "StreetDirSuffix": null, 27 | "ListingKey": "20060725224713296297000000", 28 | "ListOfficeName": "Century 21 On The Lake", 29 | "BathsHalf": null, 30 | "ModificationTimestamp": "2010-11-22T20:47:21Z", 31 | "CountyOrParish": "Boundary" 32 | }, 33 | "Id": "20060725224713296297000000" 34 | } 35 | ], 36 | "Success": true 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/spark_api/cli/oauth2.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + "/../cli/setup" 2 | 3 | 4 | SparkApi.configure do |config| 5 | oauth = { 6 | :authorization_uri=> ENV.fetch("AUTH_URI", SparkApi::Configuration::DEFAULT_AUTHORIZATION_URI), 7 | :access_uri => ENV.fetch("ACCESS_URI", SparkApi::Configuration::DEFAULT_ACCESS_URI), 8 | :redirect_uri => ENV.fetch("REDIRECT_URI", SparkApi::Configuration::DEFAULT_REDIRECT_URI), 9 | :client_id=> ENV["CLIENT_ID"], 10 | :client_secret=> ENV["CLIENT_SECRET"] 11 | } 12 | oauth[:username] = ENV["USERNAME"] if ENV.include?("USERNAME") 13 | oauth[:password] = ENV["PASSWORD"] if ENV.include?("PASSWORD") 14 | config.oauth2_provider = SparkApi::Authentication::OAuth2Impl::CLIProvider.new(oauth) 15 | unless (oauth.include?(:username) && oauth.include?(:password)) 16 | config.oauth2_provider.grant_type = :authorization_code 17 | config.oauth2_provider.code = ENV["CODE"] if ENV.include?("CODE") 18 | end 19 | config.authentication_mode = SparkApi::Authentication::OAuth2 20 | config.endpoint = ENV["API_ENDPOINT"] if ENV["API_ENDPOINT"] 21 | config.ssl_verify = ENV["SSL_VERIFY"].downcase != 'false' if ENV["SSL_VERIFY"] 22 | config.middleware = ENV["SPARK_MIDDLEWARE"] if ENV["SPARK_MIDDLEWARE"] 23 | config.dictionary_version = ENV["DICTIONARY_VERSION"] if ENV["DICTIONARY_VERSION"] 24 | end 25 | 26 | # Enables saving and loading serialized oauth2 sessions for the system user. 27 | def persist_sessions! my_alias = nil 28 | warn "Warning: persistent session mode saves access tokens in clear text on the filesystem." 29 | SparkApi.client.oauth2_provider.session_alias = my_alias unless my_alias.nil? 30 | SparkApi.client.oauth2_provider.persistent_sessions = true 31 | end 32 | -------------------------------------------------------------------------------- /lib/spark_api/errors.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | 3 | # All known response codes listed in the API 4 | module ResponseCodes 5 | NOT_FOUND = 404 6 | METHOD_NOT_ALLOWED = 405 7 | INVALID_KEY = 1000 8 | DISABLED_KEY = 1010 9 | API_USER_REQUIRED = 1015 10 | SESSION_TOKEN_EXPIRED = 1020 11 | SSL_REQUIRED = 1030 12 | INVALID_JSON = 1035 13 | INVALID_FIELD = 1040 14 | MISSING_PARAMETER = 1050 15 | INVALID_PARAMETER = 1053 16 | CONFLICTING_DATA = 1055 17 | NOT_AVAILABLE= 1500 18 | RATE_LIMIT_EXCEEDED = 1550 19 | end 20 | 21 | # Errors built from API responses 22 | class InvalidResponse < StandardError; end 23 | class ClientError < StandardError 24 | attr_reader :code, :status, :details, :request_path, :request_id, :errors 25 | def initialize (options = {}) 26 | # Support the standard initializer for errors 27 | opts = options.is_a?(Hash) ? options : {:message => options.to_s} 28 | @code = opts[:code] 29 | @status = opts[:status] 30 | @details = opts[:details] 31 | @request_path = opts[:request_path] 32 | @request_id = opts[:request_id] 33 | @errors = opts[:errors] 34 | super(opts[:message]) 35 | end 36 | 37 | end 38 | class NotFound < ClientError; end 39 | class PermissionDenied < ClientError; end 40 | class NotAllowed < ClientError; end 41 | class BadResourceRequest < ClientError; end 42 | 43 | # =Errors 44 | # Error messages and other error handling 45 | module Errors 46 | def self.ssl_verification_error 47 | "SSL verification problem: if connecting to a trusted but non production API endpoint, " + 48 | "set 'ssl_verify' to false in the configuration or add '--no_verify' to the CLI command." 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/formatters/api_support_formatter.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/core/formatters/documentation_formatter' 2 | 3 | # Formatter to support documenting currently-implemented API paths and 4 | # methods. This borrows heavily from the standard 5 | # DocumentationFormatter, but excludes example groups not related 6 | # tagged with :support. It also formats examples with a :method tag 7 | # with the HTTP method and path ahead of the example description. 8 | class ApiSupportFormatter < RSpec::Core::Formatters::DocumentationFormatter 9 | 10 | def example_group_started(example_group) 11 | super(example_group) if describes_support?(example_group) 12 | end 13 | 14 | def example_group_finished(example_group) 15 | super(example_group) if describes_support?(example_group) 16 | end 17 | 18 | def failure_output(example, exception) 19 | red("#{current_indentation}F #{example.description} (FAILED - #{next_failure_index})") 20 | end 21 | 22 | def passed_output(example) 23 | green("#{current_indentation}#{example_output_text(example)}") 24 | end 25 | 26 | def pending_output(example, message) 27 | yellow("#{current_indentation}* #{example_output_text(example)} (PENDING)") 28 | end 29 | 30 | protected 31 | 32 | def example_output_text(example) 33 | if example.metadata[:method] 34 | example_group_text = '' 35 | example_group_text = @example_group.description if @example_group.description =~ /^\// 36 | output = "#{example.metadata[:method]} #{example_group_text} - #{example.description}" 37 | else 38 | output = example.description # consider no output for examples not tagged with a :method 39 | end 40 | end 41 | 42 | def describes_support?(example_group) 43 | example_group.metadata.filter_applies?(:support, true) 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /spec/fixtures/standardfields/nearby.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [{ 5 | "City": { 6 | "ResourceUri": "/v1/standardfields/nearby/A/City", 7 | "HasList": true, 8 | "FieldList": [{ 9 | "Name": "Fargo", 10 | "Value": "'Fargo'" 11 | }, 12 | { 13 | "Name": "Moorhead", 14 | "Value": "'Moorhead'" 15 | }], 16 | "Searchable": true, 17 | "Type": "Character" 18 | }, 19 | "PostalCode": { 20 | "ResourceUri": "/v1/standardfields/nearby/A/PostalCode", 21 | "HasList": true, 22 | "FieldList": [{ 23 | "Name": "56560", 24 | "Value": "'56560'" 25 | }, 26 | { 27 | "Name": "58102", 28 | "Value": "'58102'" 29 | }, 30 | { 31 | "Name": "58103", 32 | "Value": "'58103'" 33 | }], 34 | "Searchable": true, 35 | "Type": "Character" 36 | }, 37 | "StateOrProvince": { 38 | "ResourceUri": "/v1/standardfields/nearby/A/StateOrProvince", 39 | "HasList": true, 40 | "FieldList": [{ 41 | "Name": "MN", 42 | "Value": "'MN'" 43 | }, 44 | { 45 | "Name": "ND", 46 | "Value": "'ND'" 47 | }], 48 | "Searchable": true, 49 | "Type": "Character" 50 | } 51 | }] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /spec/fixtures/listings/virtual_tours_index.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Results": [ 4 | { 5 | "Uri": "http://www.flexmls.com/", 6 | "ResourceUri": "/v1/listings/20060712220814669202000000/virtualtours/20110105165843978012000000", 7 | "Name": "brandedasdfasdf", 8 | "Id": "20110105165843978012000000", 9 | "Type": "branded" 10 | }, 11 | { 12 | "Uri": "http://www.flexmls.comasdf", 13 | "ResourceUri": "/v1/listings/20060712220814669202000000/virtualtours/20110105164609987122000000", 14 | "Name": "unbranded 1", 15 | "Id": "20110105164609987122000000", 16 | "Type": "unbranded" 17 | }, 18 | { 19 | "Uri": "http://too.many.prefs", 20 | "ResourceUri": "/v1/listings/20060712220814669202000000/virtualtours/20110105164725304762000000", 21 | "Name": "branded", 22 | "Id": "20110105164725304762000000", 23 | "Type": "branded" 24 | }, 25 | { 26 | "Uri": "http://www.flexmls.com/", 27 | "ResourceUri": "/v1/listings/20060712220814669202000000/virtualtours/20110105164731217564000000", 28 | "Name": "branded 2", 29 | "Id": "20110105164731217564000000", 30 | "Type": "branded" 31 | }, 32 | { 33 | "Uri": "http://www.flexmls.com/", 34 | "ResourceUri": "/v1/listings/20060712220814669202000000/virtualtours/20110105164628823578000000", 35 | "Name": "unbrandedasdfasdf", 36 | "Id": "20110105164628823578000000", 37 | "Type": "unbranded" 38 | } 39 | ], 40 | "Success": true 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /spec/unit/spark_api/models/account_report_spec.rb: -------------------------------------------------------------------------------- 1 | require './spec/spec_helper' 2 | 3 | describe AccountReport do 4 | 5 | it_behaves_like(:account, AccountReport) 6 | 7 | let(:account_report) { 8 | AccountReport.new({ 9 | "Id" => "12345", 10 | "Name" => "Agent McAgentson", 11 | "Office" => "Office Name", 12 | "Emails"=> [], 13 | "Phones"=> [], 14 | "Websites"=> [], 15 | "Addresses"=> [] 16 | }) 17 | } 18 | 19 | describe 'primary_email' do 20 | 21 | it 'returns the primary email address' do 22 | account_report.emails << double(:Address => 'foo@foo.com', :primary? => true) 23 | expect(account_report.primary_email).to eq account_report.emails.primary.Address 24 | end 25 | 26 | it 'returns nil when there is no primary email address' do 27 | account_report.emails << double(:Address => 'foo@foo.com', :primary? => false) 28 | expect(account_report.primary_email).to eq nil 29 | end 30 | 31 | it 'returns nil when there are no email addresses' do 32 | allow(account_report).to receive(:emails).and_return nil 33 | expect(account_report.primary_email).to eq nil 34 | end 35 | 36 | end 37 | 38 | describe 'primary_phone' do 39 | 40 | it 'returns the primary phone number' do 41 | account_report.phones << double(:Number => '88', :primary? => true) 42 | expect(account_report.primary_phone).to eq account_report.phones.primary.Number 43 | end 44 | 45 | it 'returns nil when there is no primary phone number' do 46 | account_report.phones << double(:Number => '88', :primary? => false) 47 | expect(account_report.primary_phone).to eq nil 48 | end 49 | 50 | it 'returns nil when there are no phone numbers' do 51 | allow(account_report).to receive(:phones).and_return nil 52 | expect(account_report.primary_phone).to eq nil 53 | end 54 | 55 | end 56 | 57 | end 58 | -------------------------------------------------------------------------------- /spark_api.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib/', __FILE__) 3 | $:.unshift lib unless $:.include?(lib) 4 | 5 | require 'spark_api/version' 6 | require 'rubygems/user_interaction' 7 | 8 | Gem::Specification.new do |s| 9 | s.name = "spark_api" 10 | s.version = SparkApi::VERSION 11 | s.platform = Gem::Platform::RUBY 12 | s.authors = ["Brandon Hornseth", "Wade McEwen"] 13 | s.email = %q{api-support@sparkapi.com} 14 | s.homepage = %q{https://github.com/sparkapi/spark_api} 15 | s.summary = %q{A library for interacting with the Spark web services.} 16 | s.description = %q{The spark_api gem handles most of the boilerplate for communicating with the Spark API rest services, including authentication and request parsing.} 17 | 18 | s.required_rubygems_version = ">= 1.8" 19 | s.rubyforge_project = "spark_api" 20 | 21 | s.extra_rdoc_files = [ 22 | "LICENSE", 23 | "README.md" 24 | ] 25 | 26 | s.license = 'Apache 2.0' 27 | 28 | s.files = Dir["{History.txt,LICENSE,Rakefile,README.md,VERSION}", "{bin,lib,script}/**/*"] 29 | s.test_files = Dir["spec/{fixtures,unit}/**/*", "spec/*.rb"] 30 | s.executables = ["spark_api"] 31 | s.require_paths = ["lib"] 32 | 33 | s.add_dependency 'addressable' 34 | s.add_dependency 'faraday', '>= 2.0.0' 35 | s.add_dependency 'multi_json', '> 1.0' 36 | s.add_dependency 'json', '>= 1.7' 37 | s.add_dependency 'will_paginate', '>= 3.0.pre2', '< 4.0.0' 38 | 39 | # TEST GEMS 40 | s.add_development_dependency 'rake' 41 | s.add_development_dependency 'rspec' 42 | s.add_development_dependency 'webmock' 43 | s.add_development_dependency 'rexml' #needed for ruby 3 44 | s.add_development_dependency 'typhoeus' 45 | s.add_development_dependency 'ci_reporter_rspec' 46 | s.add_development_dependency 'builder', '>= 2.1.2', '< 4.0.0' 47 | s.add_development_dependency 'simplecov-rcov' 48 | end 49 | 50 | -------------------------------------------------------------------------------- /spec/unit/spark_api/models/shared_listing_spec.rb: -------------------------------------------------------------------------------- 1 | require './spec/spec_helper' 2 | 3 | 4 | describe SharedListing do 5 | before(:each) do 6 | stub_auth_request 7 | end 8 | 9 | it "should respond to the finders" do 10 | expect(SharedListing).to respond_to(:find) 11 | end 12 | 13 | context "/sharedlistings", :support do 14 | on_post_it "should create shared listings" do 15 | stub_api_post("/#{subject.class.element_name}", 'listings/shared_listing_new.json', 'listings/shared_listing_post.json') 16 | subject.ListingIds = ["20110224152431857619000000","20110125122333785431000000"] 17 | subject.ViewId = "20080125122333787615000000" 18 | expect(subject.save).to be(true) 19 | expect(subject.Id).to eq("15Ar") 20 | expect(subject.Mode).to eq("Public") 21 | expect(subject.ResourceUri).to eq("/v1/sharedlistings/15Ar") 22 | expect(subject.SharedUri).to eq("http://www.flexmls.com/share/15Ar/3544-N-Olsen-Avenue-Tucson-AZ-85719") 23 | end 24 | 25 | on_post_it "should fail creating" do 26 | stub_api_post("/#{subject.class.element_name}", nil) do |request| 27 | request.to_return(:status => 400, :body => fixture('errors/failure.json')) 28 | end 29 | subject 30 | expect(subject.save).to be(false) 31 | expect{ subject.save! }.to raise_error(SparkApi::ClientError){ |e| expect(e.status).to eq(400) } 32 | end 33 | end 34 | 35 | context "/sharedlistings/", :support do 36 | on_get_it "should get shared listing" do 37 | shared_id = '15Ar' 38 | stub_api_get("/#{subject.class.element_name}/#{shared_id}", 39 | 'listings/shared_listing_get.json') 40 | 41 | shared = SharedListing.find(shared_id) 42 | expect(shared).to respond_to('SharedUri') 43 | expect(shared).to respond_to('Mode') 44 | expect(shared.Mode).to eq('Public') 45 | expect(shared.ListingIds).to be_an(Array) 46 | end 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /lib/spark_api/authentication/oauth2_impl/grant_type_code.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Authentication 3 | module OAuth2Impl 4 | # OAuth2 authentication flow using username and password parameters for the user in the 5 | # request. This implementation is geared towards authentication styles for web applications 6 | # that have a OAuth flow for redirects. 7 | class GrantTypeCode < GrantTypeBase 8 | def initialize(client, provider, session) 9 | super(client, provider, session) 10 | end 11 | def authenticate 12 | if(provider.code.nil?) 13 | SparkApi.logger.debug { "[oauth2] No authoriztion code present. Redirecting to #{authorization_url}." } 14 | provider.redirect(authorization_url) 15 | end 16 | if needs_refreshing? 17 | new_session = refresh 18 | end 19 | return new_session unless new_session.nil? 20 | create_session(token_params) 21 | end 22 | 23 | def refresh() 24 | SparkApi.logger.debug { "[oauth2] Refresh oauth session." } 25 | refresher = GrantTypeRefresh.new(client,provider,session) 26 | refresher.params = {"redirect_uri" => @provider.redirect_uri} 27 | refresher.authenticate 28 | rescue ClientError => e 29 | SparkApi.logger.info { "[oauth2] Refreshing token failed, the library will try and authenticate from scratch: #{e.message}" } 30 | nil 31 | end 32 | 33 | private 34 | def token_params 35 | hash = { 36 | "client_id" => @provider.client_id, 37 | "client_secret" => @provider.client_secret, 38 | "code" => @provider.code, 39 | "grant_type" => "authorization_code", 40 | "redirect_uri" => @provider.redirect_uri 41 | } 42 | MultiJson.dump(hash) 43 | end 44 | 45 | end 46 | 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/spark_api/models/standard_fields.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | class StandardFields < Base 4 | extend Finders 5 | self.element_name="standardfields" 6 | 7 | # expand all fields passed in 8 | def self.find_and_expand_all(fields, arguments={}, max_list_size=1000) 9 | returns = {} 10 | 11 | # find all standard fields, but expand only the location fields 12 | # TODO: when _expand support is added to StandardFields API, use the following 13 | # standard_fields = find(:all, {:ApiUser => owner, :_expand => fields.join(",")}) 14 | standard_fields = find(:all, arguments) 15 | 16 | # filter through the list and return only the location fields found 17 | fields.each do |field| 18 | # search for field in the payload 19 | if standard_fields.first.attributes.has_key?(field) 20 | returns[field] = standard_fields.first.attributes[field] 21 | 22 | # lookup fully _expand field, if the field has a list 23 | if returns[field]['HasList'] && returns[field]['MaxListSize'].to_i <= max_list_size 24 | returns[field] = connection.get("/standardfields/#{field}", arguments).first[field] 25 | end 26 | 27 | end 28 | end 29 | 30 | returns 31 | end 32 | 33 | 34 | # find_nearby: find fields nearby via lat/lon 35 | def self.find_nearby(prop_types = ["A"], arguments={}) 36 | return_json = {"D" => {"Success" => true, "Results" => []} } 37 | 38 | # add _expand=1 so the fields are returned 39 | arguments.merge!({:_expand => 1}) 40 | 41 | # find and return 42 | return_json["D"]["Results"] = connection.get("/standardfields/nearby/#{prop_types.join(',')}", arguments) 43 | 44 | # return 45 | return_json 46 | end 47 | 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | if ENV['COVERAGE'] == "on" 2 | require 'simplecov' 3 | require 'simplecov-rcov' 4 | SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter 5 | unless SimpleCov.running # Hack to prevent starting SimpleCov multiple times see: https://github.com/simplecov-ruby/simplecov/issues/1003 6 | SimpleCov.start do 7 | add_filter '/vendor' 8 | add_filter '/spec' 9 | add_filter '/test' 10 | end 11 | end 12 | end 13 | 14 | require "rubygems" 15 | require "rspec" 16 | require 'webmock/rspec' 17 | require "json" 18 | require 'multi_json' 19 | 20 | path = File.expand_path(File.dirname(__FILE__) + "/../lib/") 21 | $LOAD_PATH.unshift(path) unless $LOAD_PATH.include?(path) 22 | require path + '/spark_api' 23 | 24 | require 'spark_api' 25 | 26 | FileUtils.mkdir 'log' unless File.exist? 'log' 27 | 28 | module SparkApi 29 | def self.logger 30 | if @logger.nil? 31 | @logger = Logger.new('log/test.log') 32 | @logger.level = Logger::DEBUG 33 | end 34 | @logger 35 | end 36 | end 37 | 38 | SparkApi.logger.info("Setup gem for rspec testing") 39 | 40 | include SparkApi::Models 41 | 42 | def reset_config 43 | SparkApi.reset 44 | SparkApi.configure { |c| c.api_user = "foobar" } 45 | end 46 | 47 | # Requires supporting ruby files with custom matchers and macros, etc, 48 | # # in spec/support/ and its subdirectories. 49 | Dir[File.expand_path(File.join(File.dirname(__FILE__), 'support', '**', '*.rb'))].each { |f| require f } 50 | 51 | RSpec.configure do |config| 52 | 53 | config.include WebMock::API 54 | config.include StubApiRequests 55 | 56 | config.alias_example_to :on_get_it, :method => 'GET' 57 | config.alias_example_to :on_put_it, :method => 'PUT' 58 | config.alias_example_to :on_post_it, :method => 'POST' 59 | config.alias_example_to :on_delete_it, :method => 'DELETE' 60 | config.before(:all) { reset_config } 61 | config.color = true 62 | end 63 | 64 | def jruby? 65 | RUBY_PLATFORM == "java" 66 | end 67 | -------------------------------------------------------------------------------- /spec/support/oauth2_helper.rb: -------------------------------------------------------------------------------- 1 | # Lightweight example of an oauth2 provider used by the ruby client. 2 | class TestOAuth2Provider < SparkApi::Authentication::BaseOAuth2Provider 3 | 4 | def initialize 5 | @authorization_uri = "https://test.fbsdata.com/r/oauth2" 6 | @access_uri = "https://api.test.fbsdata.com/v1/oauth2/grant" 7 | @redirect_uri = "https://exampleapp.fbsdata.com/oauth-callback" 8 | @client_id="example-id" 9 | @client_secret="example-password" 10 | @sparkbar_uri = "https://test.sparkplatform.com/appbar/authorize" 11 | @session_cache = {} 12 | end 13 | 14 | def redirect(url) 15 | # User redirected to url, signs in, and gets code sent to callback 16 | self.code="my_code" 17 | end 18 | 19 | def load_session() 20 | @session_cache["test_user_session"] 21 | end 22 | 23 | def save_session(session) 24 | @session_cache["test_user_session"] = session 25 | nil 26 | end 27 | 28 | def session_timeout; 57600; end 29 | 30 | end 31 | 32 | 33 | class TestCLIOAuth2Provider < SparkApi::Authentication::BaseOAuth2Provider 34 | def initialize 35 | @authorization_uri = "https://test.fbsdata.com/r/oauth2" 36 | @access_uri = "https://api.test.fbsdata.com/v1/oauth2/grant" 37 | @client_id="example-id" 38 | @client_secret="example-secret" 39 | @username="example-user" 40 | @password="example-password" 41 | @session_cache = {} 42 | end 43 | 44 | def grant_type 45 | :password 46 | end 47 | 48 | def redirect(url) 49 | raise "Unsupported in oauth grant_type=password mode" 50 | end 51 | 52 | def load_session() 53 | @session_cache["test_user_session"] 54 | end 55 | def save_session(session) 56 | @session_cache["test_user_session"] = session 57 | nil 58 | end 59 | def session_timeout; 60; end 60 | 61 | end 62 | 63 | 64 | class InvalidAuth2Provider < SparkApi::Authentication::BaseOAuth2Provider 65 | 66 | def grant_type 67 | :not_a_real_type 68 | end 69 | 70 | end 71 | -------------------------------------------------------------------------------- /spec/unit/spark_api/models/defaultable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Defaultable do 4 | 5 | module SparkApi 6 | module Models 7 | class TestClass < Base 8 | extend Finders 9 | include Defaultable 10 | self.element_name = 'testclass' 11 | end 12 | end 13 | end 14 | 15 | describe 'default' do 16 | 17 | it 'returns an instance of the class' do 18 | allow(TestClass).to receive(:connection).and_return(double(get: [{"Name" => 'foo'}])) 19 | expect(TestClass.default).to be_a TestClass 20 | end 21 | 22 | it 'returns nil when there are no results' do 23 | allow(TestClass).to receive(:connection).and_return(double(get: [])) 24 | expect(TestClass.default).to be nil 25 | end 26 | 27 | it "assigns the default id to the instance if it doesn't have an id" do 28 | allow(TestClass).to receive(:connection).and_return(double(get: [{"Name" => 'foo'}])) 29 | expect(TestClass.default.Id).to eq TestClass::DEFAULT_ID 30 | end 31 | 32 | it "doesn't override the id if one is present" do 33 | allow(TestClass).to receive(:connection).and_return(double(get: [{"Id" => '5', "Name" => 'foo'}])) 34 | expect(TestClass.default.Id).to eq '5' 35 | end 36 | 37 | end 38 | 39 | describe 'find' do 40 | 41 | it "calls 'default' when given the default id" do 42 | expect(TestClass).to receive(:default) 43 | TestClass.find(TestClass::DEFAULT_ID) 44 | end 45 | 46 | it "passes options to 'default'" do 47 | args = { foo: true } 48 | expect(TestClass).to receive(:default).with(args) 49 | TestClass.find(TestClass::DEFAULT_ID, args) 50 | end 51 | 52 | it "calls Finders.find when given a normal id" do 53 | connection = double 54 | expect(connection).to receive(:get).with("/testclass/5", {foo: true}).and_return([{}]) 55 | allow(TestClass).to receive(:connection).and_return(connection) 56 | TestClass.find('5', foo: true) 57 | end 58 | 59 | end 60 | 61 | end 62 | -------------------------------------------------------------------------------- /spec/unit/spark_api/models/dirty_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class MyExampleModel < Base 4 | include Concerns::Savable 5 | self.prefix = "/test/" 6 | self.element_name = "example" 7 | end 8 | 9 | describe Dirty do 10 | 11 | before :each do 12 | stub_auth_request 13 | @model = MyExampleModel.new(:Name => "some old name") 14 | @model.Name = "a new name" 15 | end 16 | 17 | it "lets you know if you've changed any attributes" do 18 | expect(@model.changed?).to be true 19 | end 20 | 21 | it "should return an array of the attributes that have been changed" do 22 | expect(@model.changed).to eq(["Name"]) 23 | end 24 | 25 | it "should return a hash diff of current changes on a model" do 26 | expect(@model.changes).to eq({ 27 | "Name" => ["some old name", "a new name"] 28 | }) 29 | end 30 | 31 | it "should return previously changed attributes after save" do 32 | stub_api_post('/test/example', { :MyExampleModels => [ @model.attributes ] }, 'base.json') 33 | @model.save 34 | expect(@model.previous_changes).to eq({ 35 | }) 36 | end 37 | 38 | it "should return changed attributes with old values" do 39 | expect(@model.changed_attributes).to eq({ 40 | "Name" => "some old name" 41 | }) 42 | end 43 | 44 | it "should return changed attributes with new values" do 45 | expect(@model.dirty_attributes).to eq({ 46 | "Name" => "a new name" 47 | }) 48 | end 49 | 50 | it "does not mark attributes dirty when initialized" do 51 | @model = MyExampleModel.new(:Name => "some sort of name") 52 | expect(@model.attributes.size).to eq(1) 53 | expect(@model.changed_attributes).to eq({}) 54 | expect(@model.dirty_attributes).to eq({}) 55 | end 56 | 57 | it "marks attributes dirty that are loaded later" do 58 | @model.load(:Name => "some sort of name") 59 | expect(@model.attributes.size).to eq(1) 60 | expect(@model.changed_attributes).to eq({"Name"=>"some old name"}) 61 | expect(@model.dirty_attributes).to eq({"Name"=>"some sort of name"}) 62 | end 63 | 64 | end 65 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 'master' 6 | pull_request: 7 | workflow_dispatch: 8 | release: 9 | types: published 10 | 11 | 12 | jobs: 13 | build: 14 | name: >- 15 | ruby-${{ matrix.ruby }} 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | ruby: [ '3.1', '3.2', '3.3' ] 20 | 21 | steps: 22 | - name: repo checkout 23 | uses: actions/checkout@v4 24 | 25 | - name: Set up Ruby ${{ matrix.ruby }} 26 | # https://github.com/ruby/setup-ruby 27 | uses: ruby/setup-ruby@v1 28 | with: 29 | ruby-version: ${{ matrix.ruby }} 30 | 31 | - name: Set up Bundler 32 | run: gem install bundler 33 | 34 | 35 | - name: bundle install 36 | run: | 37 | ./script/bootstrap 38 | 39 | - name: test 40 | run: ./script/ci_build 41 | 42 | publish: 43 | runs-on: ubuntu-latest 44 | 45 | # only run if we pushed a tag 46 | if: github.event_name == 'release' 47 | 48 | # require that the build matrix passed 49 | needs: build 50 | 51 | steps: 52 | - name: repo checkout 53 | uses: actions/checkout@v4 54 | 55 | - name: Set up Ruby 56 | uses: ruby/setup-ruby@v1 57 | with: 58 | ruby-version: '3.3' 59 | 60 | - name: bundle install 61 | run: | 62 | bundle install --jobs 4 --retry 3 --path=.bundle 63 | 64 | - name: package 65 | run: bundle exec rake build 66 | 67 | - name: GitHub Release 68 | uses: softprops/action-gh-release@v1 69 | env: 70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | 72 | - name: Publish to rubygems 73 | run: | 74 | mkdir -p $HOME/.gem 75 | touch $HOME/.gem/credentials 76 | printf -- "---\n:rubygems_api_key: ${RUBYGEMS_API_KEY}\n" > $HOME/.gem/credentials 77 | chmod 0600 $HOME/.gem/credentials 78 | gem push pkg/*.gem 79 | env: 80 | RUBYGEMS_API_KEY: ${{secrets.RUBYGEMS_API_KEY}} 81 | -------------------------------------------------------------------------------- /spec/unit/spark_api/models/standard_fields_spec.rb: -------------------------------------------------------------------------------- 1 | require './spec/spec_helper' 2 | 3 | describe StandardFields do 4 | 5 | before(:each) do 6 | stub_auth_request 7 | end 8 | 9 | it "should respond to get" do 10 | expect(StandardFields).to respond_to(:get) 11 | end 12 | 13 | it "should find and expand all" do 14 | expect(StandardFields).to respond_to(:find_and_expand_all) 15 | 16 | # stub request to standardFields 17 | stub_api_get('/standardfields','standardfields/standardfields.json') 18 | 19 | # stub request for City 20 | stub_api_get('/standardfields/City','standardfields/city.json') 21 | 22 | # stub request for StateOrProvince 23 | stub_api_get('/standardfields/StateOrProvince','standardfields/stateorprovince.json') 24 | 25 | # request 26 | fields = StandardFields.find_and_expand_all(["City","StateOrProvince"]) 27 | 28 | # keys are present 29 | expect(fields).to have_key("City") 30 | expect(fields).to have_key("StateOrProvince") 31 | expect(fields).not_to have_key("SubdivisionName") 32 | 33 | # FieldList 34 | expect(fields["City"]["FieldList"].length).to eq(235) 35 | expect(fields["StateOrProvince"]["FieldList"].length).to eq(5) 36 | 37 | end 38 | 39 | context "/standardfields/nearby/", :support do 40 | on_get_it "should find nearby fields" do 41 | expect(StandardFields).to respond_to(:find_nearby) 42 | 43 | # stub request 44 | stub_api_get('/standardfields/nearby/A','standardfields/nearby.json', 45 | :Lat => "50", 46 | :Lon => "-92", 47 | :_expand => "1") 48 | 49 | # request 50 | fields = StandardFields.find_nearby(["A"], {:Lat => 50, :Lon => -92}) 51 | 52 | # validate response 53 | expect(fields["D"]["Success"]).to eq(true) 54 | expect(fields["D"]["Results"].first).to have_key("City") 55 | expect(fields["D"]["Results"].first).to have_key("PostalCode") 56 | expect(fields["D"]["Results"].first).to have_key("StateOrProvince") 57 | end 58 | end 59 | 60 | end 61 | -------------------------------------------------------------------------------- /spec/fixtures/listings/with_permissions.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Results": [ 4 | { 5 | "ResourceUri": "/v1/listings/20060725224713296297000000", 6 | "StandardFields": { 7 | "StreetNumber": "7298", 8 | "Longitude": "-116.3237", 9 | "City": "Bonners Ferry", 10 | "ListingId": "06-9395", 11 | "PublicRemarks": "Afforadable home in town close to hospital,yet quiet country like setting. Good views. Must see", 12 | "BuildingAreaTotal": "924.0", 13 | "YearBuilt": 1977, 14 | "StreetName": "BIRCH", 15 | "ListPrice": "50000.0", 16 | "PostalCode": "83805", 17 | "VirtualTourURLBranded": "http://www.youtube.com/watch?v=T9uuPza41Uw#t=2s", 18 | "Latitude": "48.7001", 19 | "BathsThreeQuarter": null, 20 | "BathsFull": null, 21 | "BathsTotal": "1.0", 22 | "StateOrProvince": "ID", 23 | "PropertyType": "A", 24 | "StreetAdditionalInfo": null, 25 | "StreetDirPrefix": null, 26 | "BedsTotal": 3, 27 | "StreetDirSuffix": null, 28 | "ListingKey": "20060725224713296297000000", 29 | "ListOfficeName": "Century 21 On The Lake", 30 | "BathsHalf": null, 31 | "ModificationTimestamp": "2011-01-05T14:54:42Z", 32 | "CountyOrParish": "Boundary" 33 | }, 34 | "Permissions": { 35 | "AvailableStatusChanges": ["A","C","D","E","P"], 36 | "Editable": true, 37 | "EditableSettings": {"StatusChange":true,"PriceChange":true, "Photos":false } 38 | }, 39 | "Id": "20060725224713296297000000" 40 | } 41 | ], 42 | "Success": true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/spark_api/models/concerns/savable.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | module Concerns 4 | 5 | module Savable 6 | 7 | def save(arguments = {}) 8 | self.errors = [] # clear the errors hash 9 | begin 10 | return save!(arguments) 11 | rescue BadResourceRequest => e 12 | self.errors << {:code => e.code, :message => e.message} 13 | SparkApi.logger.warn("Failed to save resource #{self}: #{e.message}") 14 | rescue NotFound => e 15 | SparkApi.logger.error("Failed to save resource #{self}: #{e.message}") 16 | end 17 | false 18 | end 19 | def save!(arguments = {}) 20 | persisted? ? update!(arguments) : create!(arguments) 21 | end 22 | 23 | def create!(arguments = {}) 24 | results = connection.post self.path, post_data.merge(params_for_save), arguments 25 | update_attributes_after_create(results.first) 26 | reset_dirty 27 | params_for_save.clear 28 | true 29 | end 30 | 31 | def update_attributes(attrs = {}, options = {}) 32 | load(attrs, options) 33 | save! 34 | end 35 | 36 | def update!(arguments = {}) 37 | return true unless changed? && persisted? 38 | connection.put resource_uri, dirty_attributes, arguments 39 | reset_dirty 40 | params_for_save.clear 41 | true 42 | end 43 | 44 | # extra params to be passed when saving 45 | def params_for_save 46 | @params_for_save ||= {} 47 | end 48 | 49 | # update/create hash (can be overridden) 50 | def post_data 51 | { resource_pluralized => [ attributes ] } 52 | end 53 | 54 | private 55 | 56 | def update_attributes_after_create(result) 57 | attributes['Id'] = result['Id'] ? result['Id'] : parse_id(result['ResourceUri']) 58 | result.delete('Id') 59 | attributes.merge! result 60 | end 61 | 62 | end 63 | 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/spark_api/cli/setup.rb: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require 'pp' 3 | require 'irb/ext/save-history' 4 | IRB.conf[:SAVE_HISTORY] = 1000 5 | IRB.conf[:HISTORY_FILE] = "#{ENV['HOME']}/.spark-api-history" 6 | 7 | if ENV["SPARK_API_CONSOLE"].nil? 8 | require 'spark_api' 9 | else 10 | puts "Enabling console mode for local gem" 11 | Bundler.require(:default, "development") if defined?(Bundler) 12 | path = File.expand_path(File.dirname(__FILE__) + "/../../../lib/") 13 | $LOAD_PATH.unshift(path) unless $LOAD_PATH.include?(path) 14 | require path + '/spark_api' 15 | end 16 | 17 | IRB.conf[:AUTO_INDENT]=true 18 | IRB.conf[:PROMPT][:SPARK]= { 19 | :PROMPT_I => "SparkApi:%03n:%i> ", 20 | :PROMPT_S => "SparkApi:%03n:%i%l ", 21 | :PROMPT_C => "SparkApi:%03n:%i* ", 22 | :RETURN => "%s\n" 23 | } 24 | 25 | IRB.conf[:PROMPT_MODE] = :SPARK 26 | 27 | path = File.expand_path(File.dirname(__FILE__) + "/../../../lib/") 28 | $LOAD_PATH.unshift(path) unless $LOAD_PATH.include?(path) 29 | require path + '/spark_api' 30 | 31 | module SparkApi 32 | def self.logger 33 | if @logger.nil? 34 | @logger = Logger.new(STDOUT) 35 | @logger.level = ENV["DEBUG"].nil? ? Logger::WARN : Logger::DEBUG 36 | end 37 | @logger 38 | end 39 | end 40 | 41 | SparkApi.logger.info("Client configured!") 42 | 43 | include SparkApi::Models 44 | 45 | def c 46 | SparkApi.client 47 | end 48 | 49 | # Straight up HTTP functions y'all!!! 50 | 51 | def get(path, options={}) 52 | c.get(path, options) 53 | end 54 | 55 | def post(path, body = nil, options={}) 56 | c.post(path, body, options) 57 | end 58 | 59 | def put(path, body = nil, options={}) 60 | c.put(path, body, options) 61 | end 62 | 63 | def delete(path, options={}) 64 | c.delete(path, options) 65 | end 66 | 67 | # Handy session persistence 68 | def save_oauth2_session! session_alias = "default" 69 | 70 | rescue => e 71 | puts "Unable to save the oauth2 session: #{e.message}" 72 | end 73 | 74 | def load_oauth2_session session_alias = "default" 75 | c.oauth2_provider.session = "" 76 | rescue => e 77 | puts "Unable to find a saved oauth2 session: #{e.message}" 78 | end -------------------------------------------------------------------------------- /spec/fixtures/notifications/notifications.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Results": [ 4 | { "Id": "20100000000000000000000000", 5 | "Type": "OperationComplete", 6 | "SenderApplication": "My Application", 7 | "SenderId": "20110000000000000000000001", 8 | "RecipientId": "20110000000000000000000002", 9 | "Message": "Your PDF generation has completed!", 10 | "BrowserUri": "http://myapplication.com/cmas/19581825.pdf", 11 | "ResourceUri": "http://myapplication.com/cmas/19581825.json", 12 | "Read": false, 13 | "CreatedTimestamp": "2011-11-18T16:35:43", 14 | "ReadTimestamp": null 15 | }, 16 | { "Id": "20100000000000000000000001", 17 | "Type": "OperationComplete", 18 | "SenderApplication": "My Application", 19 | "SenderId": "20110000000000000000000001", 20 | "RecipientId": "20110000000000000000000002", 21 | "Message": "Your PDF generation has completed!", 22 | "BrowserUri": "http://myapplication.com/cmas/19581826.pdf", 23 | "ResourceUri": "http://myapplication.com/cmas/19581826.json", 24 | "Read": false, 25 | "CreatedTimestamp": "2011-11-18T16:35:40", 26 | "ReadTimestamp": null 27 | }, 28 | { "Id": "20100000000000000000000002", 29 | "Type": "OperationComplete", 30 | "SenderApplication": "My Application", 31 | "SenderId": "20110000000000000000000001", 32 | "RecipientId": "20110000000000000000000002", 33 | "Message": "Your PDF generation has completed!", 34 | "BrowserUri": "http://myapplication.com/cmas/19581827.pdf", 35 | "ResourceUri": "http://myapplication.com/cmas/19581827.json", 36 | "Read": false, 37 | "CreatedTimestamp": "2011-11-18T16:35:37", 38 | "ReadTimestamp": null 39 | } 40 | ], 41 | "Success": true 42 | } 43 | } -------------------------------------------------------------------------------- /spec/support/json_helper.rb: -------------------------------------------------------------------------------- 1 | class ListingJson 2 | @counter = 0 3 | def self.next_tech_id() 4 | "#{@counter + 20110101000000000000}000000" 5 | end 6 | def self.next_list_id() 7 | "06-#{@counter + 1000}" 8 | end 9 | def self.bump() 10 | @counter += 1 11 | end 12 | 13 | def self.create 14 | bump 15 | json = <<-JSON 16 | { 17 | "ResourceUri": "/v1/listings/#{next_tech_id}", 18 | "StandardFields": #{standard_fields}, 19 | "Id": "#{next_tech_id}" 20 | } 21 | JSON 22 | 23 | end 24 | 25 | def self.create_all(count = 1) 26 | listings = [] 27 | count.times { listings << create } 28 | json = listings * "," 29 | end 30 | 31 | private 32 | def self.standard_fields 33 | json = <<-JSON 34 | { 35 | "StreetNumber": "7298", 36 | "Longitude": "-116.3237", 37 | "City": "Bonners Ferry", 38 | "ListingId": "#{next_list_id}", 39 | "PublicRemarks": "Afforadable home in town close to hospital,yet quiet country like setting. Good views. Must see", 40 | "BuildingAreaTotal": 924.0, 41 | "YearBuilt": 1977, 42 | "StreetName": "BIRCH", 43 | "ListPrice": 50000.0, 44 | "PostalCode": 83805, 45 | "Latitude": "48.7001", 46 | "BathsThreeQuarter": null, 47 | "BathsFull": 1.0, 48 | "BathsTotal": 1.0, 49 | "StateOrProvince": "ID", 50 | "PropertyType": "A", 51 | "StreetAdditionalInfo": null, 52 | "StreetDirPrefix": null, 53 | "BedsTotal": 3, 54 | "StreetDirSuffix": null, 55 | "ListingKey": "#{next_tech_id}", 56 | "ListOfficeName": "Century 21 On The Lake", 57 | "BathsHalf": null, 58 | "ModificationTimestamp": "2010-11-22T20:47:21Z", 59 | "CountyOrParish": "Boundary" 60 | } 61 | JSON 62 | end 63 | 64 | end 65 | 66 | def paginate_json(current_page = 1) 67 | json = <<-JSON 68 | "Pagination": { 69 | "TotalRows": 38, 70 | "PageSize": 10, 71 | "TotalPages": 4, 72 | "CurrentPage": #{current_page} 73 | } 74 | JSON 75 | end 76 | 77 | -------------------------------------------------------------------------------- /lib/spark_api/connection.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | require 'faraday' 3 | 4 | module SparkApi 5 | # =Connection 6 | # Mixin module for handling http connection information 7 | module Connection 8 | REG_HTTP = /^http:/ 9 | REG_HTTPS = /^https:/ 10 | HTTP_SCHEME = 'http:' 11 | HTTPS_SCHEME = 'https:' 12 | ACCEPT_ENCODING = 'Accept-Encoding' 13 | X_REQUEST_ID_CHAIN = 'X-Request-Id-Chain' 14 | MIME_JSON = 'application/json' 15 | MIME_RESO = 'application/json, application/xml' 16 | # Main connection object for running requests. Bootstraps the Faraday abstraction layer with 17 | # our client configuration. 18 | def connection(force_ssl = false) 19 | opts = { 20 | :headers => headers 21 | } 22 | if(force_ssl || self.ssl) 23 | opts[:ssl] = {:verify => false } unless self.ssl_verify 24 | opts[:url] = @endpoint.sub REG_HTTP, HTTPS_SCHEME 25 | else 26 | opts[:url] = @endpoint.sub REG_HTTPS, HTTP_SCHEME 27 | end 28 | 29 | if request_id_chain 30 | opts[:headers][X_REQUEST_ID_CHAIN] = request_id_chain 31 | end 32 | 33 | conn = Faraday.new(opts) do |conn| 34 | conn.response self.middleware.to_sym 35 | conn.options[:timeout] = self.timeout 36 | conn.adapter Faraday.default_adapter 37 | end 38 | if self.verbose 39 | SparkApi.logger.debug { "Connection: #{conn.inspect}" } 40 | end 41 | conn 42 | end 43 | 44 | # HTTP request headers for client requests 45 | def headers 46 | if self.middleware.to_sym == :reso_api 47 | reso_headers 48 | else 49 | spark_headers 50 | end 51 | end 52 | 53 | def spark_headers 54 | { 55 | :accept => MIME_JSON, 56 | :content_type => MIME_JSON, 57 | :user_agent => Configuration::DEFAULT_USER_AGENT, 58 | Configuration::X_SPARK_API_USER_AGENT => user_agent 59 | } 60 | end 61 | 62 | def reso_headers 63 | { 64 | :accept => MIME_RESO, 65 | :user_agent => Configuration::DEFAULT_USER_AGENT, 66 | Configuration::X_SPARK_API_USER_AGENT => user_agent 67 | } 68 | end 69 | 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/fixtures/listing_meta_translations/get.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Success": true, 4 | "Results": [ 5 | { 6 | "StandardFields": { 7 | "ParkingTotal": { 8 | "FlexmlsTableName": "feature", 9 | "FlexmlsFieldId": "# Parking Spaces", 10 | "FlexmlsDisplayId": "20010531180858445022000000", 11 | "FlexmlsDisplayType": "N", 12 | "FlexmlsGroupId": "GA", 13 | "SparkDataType": "Decimal" 14 | }, 15 | "VOWAddressDisplayYN": { 16 | "FlexmlsTableName": "list_extra", 17 | "FlexmlsFieldId": "pub_addr", 18 | "FlexmlsDisplayId": null, 19 | "FlexmlsDisplayType": "S", 20 | "FlexmlsGroupId": null, 21 | "SparkDataType": "Boolean" 22 | } 23 | }, 24 | "CustomFields": { 25 | "Garage": { 26 | "# Parking Spaces": { 27 | "FlexmlsTableName": "feature", 28 | "FlexmlsFieldId": "# Parking Spaces", 29 | "FlexmlsDisplayId": "20010531180858445022000000", 30 | "FlexmlsDisplayType": "N", 31 | "FlexmlsGroupId": "GA", 32 | "SparkDataType": "Decimal" 33 | }, 34 | "# Stalls Attached": { 35 | "FlexmlsTableName": "feature", 36 | "FlexmlsFieldId": "# Stalls Attached", 37 | "FlexmlsDisplayId": "20010531180812103075000000", 38 | "FlexmlsDisplayType": "N", 39 | "FlexmlsGroupId": "GA", 40 | "SparkDataType": "Decimal" 41 | } 42 | } 43 | } 44 | } 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/spark_api/models.rb: -------------------------------------------------------------------------------- 1 | require 'spark_api/models/dirty' 2 | require 'spark_api/models/base' 3 | require 'spark_api/models/constraint' 4 | require 'spark_api/models/finders' 5 | require 'spark_api/models/defaultable' 6 | require 'spark_api/models/subresource' 7 | require 'spark_api/models/concerns' 8 | 9 | require 'spark_api/models/account' 10 | require 'spark_api/models/account_report' 11 | require 'spark_api/models/account_roster' 12 | require 'spark_api/models/activity' 13 | require 'spark_api/models/connect_prefs' 14 | require 'spark_api/models/contact' 15 | require 'spark_api/models/comment' 16 | require 'spark_api/models/custom_fields' 17 | require 'spark_api/models/document' 18 | require 'spark_api/models/email_link' 19 | require 'spark_api/models/fields' 20 | require 'spark_api/models/idx' 21 | require 'spark_api/models/idx_link' 22 | require 'spark_api/models/incomplete_listing' 23 | require 'spark_api/models/floplan' 24 | require 'spark_api/models/listing' 25 | require 'spark_api/models/listing_cart' 26 | require 'spark_api/models/listing_meta_translations' 27 | require 'spark_api/models/market_statistics' 28 | require 'spark_api/models/media' 29 | require 'spark_api/models/message' 30 | require 'spark_api/models/news_feed_meta' 31 | require 'spark_api/models/newsfeed' 32 | require 'spark_api/models/note' 33 | require 'spark_api/models/notification' 34 | require 'spark_api/models/open_house' 35 | require 'spark_api/models/photo' 36 | require 'spark_api/models/portal' 37 | require 'spark_api/models/portal_listing_cart' 38 | require 'spark_api/models/property_types' 39 | require 'spark_api/models/rental_calendar' 40 | require 'spark_api/models/rule' 41 | require 'spark_api/models/saved_search' 42 | require 'spark_api/models/search_template/quick_search' 43 | require 'spark_api/models/shared_link' 44 | require 'spark_api/models/shared_listing' 45 | require 'spark_api/models/sort' 46 | require 'spark_api/models/standard_fields' 47 | require 'spark_api/models/system_info' 48 | require 'spark_api/models/system_info_search' 49 | require 'spark_api/models/tour_of_home' 50 | require 'spark_api/models/video' 51 | require 'spark_api/models/virtual_tour' 52 | require 'spark_api/models/vow_account' 53 | 54 | module SparkApi 55 | module Models 56 | 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /script/combined_flow_example.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | 4 | Bundler.require(:default, "development") if defined?(Bundler) 5 | 6 | path = File.expand_path(File.dirname(__FILE__) + "/../lib/") 7 | $LOAD_PATH.unshift(path) unless $LOAD_PATH.include?(path) 8 | require path + '/spark_api' 9 | 10 | SparkApi.logger.info("Hello!") 11 | 12 | SparkApi.configure do |config| 13 | config.authentication_mode = SparkApi::Authentication::OpenIdOAuth2Hybrid 14 | config.api_key = "YOUR_CLIENT_ID" 15 | config.api_secret = "YOUR_CLIENT_SECRET" 16 | config.callback = "YOUR_REDIRECT_URI" 17 | config.auth_endpoint = "https://sparkplatform.com/openid" 18 | config.endpoint = 'https://sparkapi.com' 19 | end 20 | 21 | client = SparkApi.client 22 | 23 | 24 | # Step 1: 25 | # To get your code to post to /v1/oauth2/grant, send the end user to this URI, replacing the all-capped strings with 26 | # the CGI-escaped credentials for your key: 27 | # https://sparkplatform.com/oauth2?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI 28 | # When the user has finished, they will land at: 29 | # YOUR_REDIRECT_URI?code=CODE. 30 | puts "Go here and log in to get your code: #{client.authenticator.authorization_url}" 31 | 32 | # Step 2: Uncomment the following, and add your code in place of CODE_FROM_ABOVE_URI 33 | # Hold on to the tokens. Unless you lose them, you can now pass in these 34 | # values until the access_token expires. 35 | #client.oauth2_provider.code = "CODE_FROM_ABOVE_URI" 36 | #client.authenticate 37 | #puts "Access Token: #{client.session.access_token}, Refresh Token: #{client.session.refresh_token}" 38 | 39 | 40 | # Step 3: Comment out Step 2, and uncomment the following. 41 | # Pass in your access_token and refresh_token to make authenticated requests to the API 42 | #client.session = SparkApi::Authentication::OAuthSession.new "access_token"=> "ACCESS_TOKEN", 43 | # "refresh_token" => "REFRESH_TOKEN", "expires_in" => 86400 44 | 45 | 46 | # Step 2a and 3a: Uncomment with Step 3 and 4. 47 | # Make requests for authorized listing data 48 | #list = client.get '/contacts' 49 | #puts "client: #{list.inspect}" 50 | #list = SparkApi::Models::Contact.get 51 | #puts "model: #{list.inspect}" 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /script/oauth2_example.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | 4 | Bundler.require(:default, "development") if defined?(Bundler) 5 | 6 | path = File.expand_path(File.dirname(__FILE__) + "/../lib/") 7 | $LOAD_PATH.unshift(path) unless $LOAD_PATH.include?(path) 8 | require path + '/spark_api' 9 | 10 | SparkApi.logger.info("Hello!") 11 | 12 | SparkApi.configure do |config| 13 | config.authentication_mode = SparkApi::Authentication::OAuth2 14 | config.api_key = "YOUR_CLIENT_ID" 15 | config.api_secret = "YOUR_CLIENT_SECRET" 16 | config.callback = "YOUR_REDIRECT_URI" 17 | config.version = "v1" 18 | config.endpoint = "https://sparkapi.com" 19 | config.auth_endpoint = "https://sparkplatform.com/oauth2" 20 | end 21 | 22 | client = SparkApi.client 23 | 24 | 25 | # Step 1: 26 | # To get your code to post to /v1/oauth2/grant, send the end user to this URI, replacing the all-capped strings with 27 | # the CGI-escaped credentials for your key: 28 | # https://sparkplatform.com/oauth2?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI 29 | # When the user has finished, they will land at: 30 | # YOUR_REDIRECT_URI?code=CODE. 31 | puts "Go here and log in to get your code: #{client.authenticator.authorization_url}" 32 | 33 | # Step 2: Uncomment the following, and add your code in place of CODE_FROM_ABOVE_URI 34 | # Hold on to the tokens. Unless you lose them, you can now pass in these 35 | # values until the access_token expires. 36 | #client.oauth2_provider.code = "CODE_FROM_ABOVE_URI" 37 | #client.authenticate 38 | #puts "Access Token: #{client.session.access_token}, Refresh Token: #{client.session.refresh_token}" 39 | 40 | 41 | # Step 3: Comment out Step 2, and uncomment the following. 42 | # Pass in your access_token and refresh_token to make authenticated requests to the API 43 | #client.session = SparkApi::Authentication::OAuthSession.new "access_token"=> "ACCESS_TOKEN", 44 | # "refresh_token" => "REFRESH_TOKEN", "expires_in" => 86400 45 | 46 | 47 | # Step 2a and 3a: Uncomment with Step 2 and 3. 48 | # Make requests for authorized listing data 49 | #list = client.get '/contacts' 50 | #puts "client: #{list.inspect}" 51 | #list = SparkApi::Models::Contact.get 52 | #puts "model: #{list.inspect}" 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /spec/fixtures/listings/with_vtour.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Results": [ 4 | { 5 | "ResourceUri": "/v1/listings/20060725224713296297000000", 6 | "StandardFields": { 7 | "StreetNumber": "7298", 8 | "Longitude": "-116.3237", 9 | "City": "Bonners Ferry", 10 | "ListingId": "06-9395", 11 | "PublicRemarks": "Afforadable home in town close to hospital,yet quiet country like setting. Good views. Must see", 12 | "BuildingAreaTotal": "924.0", 13 | "YearBuilt": 1977, 14 | "StreetName": "BIRCH", 15 | "ListPrice": "50000.0", 16 | "PostalCode": "83805", 17 | "VirtualTourURLBranded": "http://www.youtube.com/watch?v=T9uuPza41Uw#t=2s", 18 | "Latitude": "48.7001", 19 | "BathsThreeQuarter": null, 20 | "BathsFull": null, 21 | "BathsTotal": "1.0", 22 | "StateOrProvince": "ID", 23 | "PropertyType": "A", 24 | "StreetAdditionalInfo": null, 25 | "StreetDirPrefix": null, 26 | "BedsTotal": 3, 27 | "StreetDirSuffix": null, 28 | "ListingKey": "20060725224713296297000000", 29 | "ListOfficeName": "Century 21 On The Lake", 30 | "BathsHalf": null, 31 | "ModificationTimestamp": "2011-01-05T14:54:42Z", 32 | "VirtualTours": [ 33 | { 34 | "Uri": "http://www.youtube.com/watch?v=T9uuPza41Uw#t=2s", 35 | "ResourceUri": "/v1/listings/20060725224713296297000000/virtualtours/20110105205442147801000000", 36 | "Name": "Samuel L", 37 | "Id": "20110105205442147801000000", 38 | "Type": "branded" 39 | } 40 | ], 41 | "CountyOrParish": "Boundary" 42 | }, 43 | "Id": "20060725224713296297000000" 44 | } 45 | ], 46 | "Success": true 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/spark_api/multi_client.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | #===Active support for multiple clients 3 | module MultiClient 4 | include SparkApi::Configuration 5 | 6 | # Activate a specific instance of the client (with appropriate config settings). Each client 7 | # is lazily instanciated by calling the matching SparkApi.symbol_name method on the 8 | # SparkApi module. It's the developers responsibility to extend the module and provide this 9 | # method. 10 | # Parameters 11 | # @sym - the unique symbol identifier for a client configuration. 12 | # &block - a block of code to run with the specified client enabled, after which the original 13 | # client setting will be reenabled 14 | def activate(sym) 15 | if block_given? 16 | original_client = Thread.current[:spark_api_client] 17 | begin 18 | activate_client(sym) 19 | yield 20 | ensure 21 | Thread.current[:spark_api_client] = original_client 22 | end 23 | else 24 | activate_client(sym) 25 | end 26 | end 27 | 28 | private 29 | 30 | # set the active client for the symbol 31 | def activate_client(symbol) 32 | if !Thread.current[symbol].nil? 33 | active_client = Thread.current[symbol] 34 | elsif SparkApi.respond_to? symbol 35 | active_client = SparkApi.send(symbol) 36 | elsif YamlConfig.exists? symbol.to_s 37 | active_client = activate_client_from_config(symbol) 38 | else 39 | raise ArgumentError, "The symbol #{symbol} is not setup correctly as a multi client key." 40 | end 41 | Thread.current[symbol] = active_client 42 | Thread.current[:spark_api_client] = active_client 43 | end 44 | 45 | def activate_client_from_config(symbol) 46 | SparkApi.logger.debug { "Loading multiclient [#{symbol.to_s}] from config" } 47 | yaml = YamlConfig.build(symbol.to_s) 48 | if(yaml.oauth2?) 49 | Client.new(yaml.client_keys.merge({ 50 | :oauth2_provider => Authentication::OAuth2Impl.load_provider(yaml.oauth2_provider, yaml.oauth2_keys), 51 | :authentication_mode => SparkApi::Authentication::OAuth2, 52 | })) 53 | else 54 | Client.new(yaml.client_keys) 55 | end 56 | end 57 | 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/unit/spark_api/models/fields_spec.rb: -------------------------------------------------------------------------------- 1 | require './spec/spec_helper' 2 | 3 | describe Fields do 4 | before(:each) do 5 | stub_auth_request 6 | end 7 | 8 | context "/fields/order", :support do 9 | on_get_it "should find field orders for all property types" do 10 | expect(Fields).to respond_to(:order) 11 | 12 | # stub request 13 | stub_api_get('/fields/order','fields/order.json') 14 | 15 | # request 16 | resources = subject.class.order 17 | 18 | # a standard array of results 19 | expect(resources).to be_an(Array) 20 | expect(resources.length).to eq(1) 21 | 22 | # make sure multiple property types are present 23 | expect(resources.first).to have_key("A") 24 | expect(resources.first).to have_key("B") 25 | 26 | expect(resources.first["A"]).to be_an(Array) 27 | end 28 | end 29 | 30 | context "/fields/order/", :support do 31 | on_get_it "should find field order for a single property type" do 32 | expect(Fields).to respond_to(:order) 33 | 34 | # stub request 35 | stub_api_get('/fields/order/A','fields/order_a.json') 36 | 37 | # request 38 | resources = subject.class.order("A") 39 | 40 | # a standard array of results 41 | expect(resources).to be_an(Array) 42 | expect(resources.length).to eq(2) 43 | 44 | # validate a single entity 45 | group = resources.first[resources.first.keys.first] 46 | expect(group).to be_an(Array) 47 | expect(group.length).to eq(2) 48 | group.each do |field| 49 | expect(field).to have_key("Field") 50 | end 51 | 52 | end 53 | end 54 | 55 | context "/fields/order/settings", :support do 56 | on_get_it "returns the field order settings" do 57 | expect(Fields).to respond_to(:settings) 58 | 59 | # stub request 60 | stub_api_get('/fields/order/settings','fields/settings.json') 61 | 62 | # request 63 | settings = subject.class.settings 64 | 65 | # a standard array of results 66 | expect(settings).to be_an(Array) 67 | expect(settings.length).to eq(1) 68 | 69 | # make sure ShowingInstructions is present 70 | expect(settings.first).to have_key("ShowingInstructions") 71 | expect(settings.first["ShowingInstructions"]).to be_an(Array) 72 | end 73 | end 74 | 75 | 76 | end 77 | -------------------------------------------------------------------------------- /lib/spark_api/models/subresource.rb: -------------------------------------------------------------------------------- 1 | module SparkApi 2 | module Models 3 | module Subresource 4 | 5 | def build_subclass 6 | Class.new(self) 7 | end 8 | 9 | def find_by_listing_key(key, arguments={}) 10 | collect(connection.get("/listings/#{key}#{self.path}", arguments)) 11 | end 12 | 13 | def find_by_id(id, parent_id, arguments={}) 14 | collect(connection.get("/listings/#{parent_id}#{self.path}/#{id}", arguments)).first 15 | end 16 | 17 | def parse_date_start_and_end_times(attributes) 18 | # Transform the date strings 19 | unless attributes['Date'].nil? || attributes['Date'].empty? 20 | date = Date.strptime attributes['Date'], '%m/%d/%Y' 21 | ['StartTime','EndTime'].each do |time| 22 | next if attributes[time].nil? || attributes[time].empty? 23 | formatted_date = "#{attributes['Date']}T#{attributes[time]}" 24 | datetime = nil 25 | 26 | begin 27 | datetime = DateTime.strptime(formatted_date, '%m/%d/%YT%l:%M %P') 28 | dst_offset = 0 29 | rescue => ex 30 | ; # Do nothing; doesn't matter 31 | end 32 | 33 | unless datetime 34 | other_formats = ['%m/%d/%YT%H:%M%z', '%m/%d/%YT%H:%M:%S%z'] 35 | other_formats.each_with_index do |format, i| 36 | begin 37 | datetime = DateTime.strptime(formatted_date, format) 38 | datetime = datetime.new_offset DateTime.now.offset 39 | now = Time.now 40 | dst_offset = now.dst? || now.zone == 'UTC' ? 0 : 1 41 | break 42 | rescue => ex 43 | next 44 | end 45 | end 46 | end 47 | 48 | # if we still don't have a valid time, raise an error 49 | unless datetime 50 | raise ArgumentError.new('invalid date') 51 | end 52 | 53 | 54 | 55 | attributes[time] = Time.local(datetime.year, datetime.month, datetime.day, 56 | datetime.hour + dst_offset, datetime.min, datetime.sec) 57 | end 58 | attributes['Date'] = date 59 | end 60 | end 61 | 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/fixtures/search_templates/quick_searches/get.json: -------------------------------------------------------------------------------- 1 | { 2 | "D": { 3 | "Results": [ 4 | { 5 | "ResourceUri": "/v1/searchtemplates/quicksearches/20120717212004874996000000", 6 | "Id": "20130717212004874996000000", 7 | "Name": "My Quick Search", 8 | "OwnerId": "20000426173054342350000000", 9 | "MlsId": "20000426143505724628000000", 10 | "Inheritable": true, 11 | "Inherited": false, 12 | "ViewId": "20130717211827902622000000", 13 | "PropertyTypes": [ 14 | "A" 15 | ], 16 | "Fields": [ 17 | { 18 | "Domain": "StandardFields", 19 | "GroupField": null, 20 | "Field": "ListPrice" 21 | }, 22 | { 23 | "Domain": "CustomFields", 24 | "GroupField": "Amenities", 25 | "Field": "# Ceiling Fans" 26 | } 27 | ], 28 | "ModificationTimestamp": "2013-07-09T15:31:47Z" 29 | }, 30 | { 31 | "ResourceUri": "/v1/searchtemplates/quicksearches/20120717212004874996000001", 32 | "Id": "20130717212004874996000001", 33 | "Name": "My Quick Search 2", 34 | "OwnerId": "20000426173054342350000000", 35 | "MlsId": "20000426143505724628000000", 36 | "Inheritable": true, 37 | "Inherited": false, 38 | "ViewId": "20130717211827902622000000", 39 | "PropertyTypes": [ 40 | "A" 41 | ], 42 | "Fields": [ 43 | { 44 | "Domain": "StandardFields", 45 | "GroupField": null, 46 | "Field": "ListPrice" 47 | }, 48 | { 49 | "Domain": "CustomFields", 50 | "GroupField": "Amenities", 51 | "Field": "# Ceiling Fans" 52 | } 53 | ], 54 | "ModificationTimestamp": "2013-07-09T15:31:47Z" 55 | } 56 | ] 57 | } 58 | } 59 | --------------------------------------------------------------------------------