├── spec
├── fixtures
│ ├── sobject
│ │ ├── upsert_updated_success_response.json
│ │ ├── upsert_created_success_response.json
│ │ ├── query_empty_response.json
│ │ ├── create_success_response.json
│ │ ├── list_sobjects_error_response.json
│ │ ├── query_error_response.json
│ │ ├── describe_sobjects_error_response.json
│ │ ├── upsert_multiple_error_response.json
│ │ ├── upsert_error_response.json
│ │ ├── delete_error_response.json
│ │ ├── sobject_describe_error_response.json
│ │ ├── write_error_response.json
│ │ ├── search_error_response.json
│ │ ├── sobject_find_error_response.json
│ │ ├── org_query_response.json
│ │ ├── query_paginated_last_page_response.json
│ │ ├── query_paginated_first_page_response.json
│ │ ├── search_success_response.json
│ │ ├── recent_success_response.json
│ │ ├── query_success_response.json
│ │ ├── describe_sobjects_success_response.json
│ │ ├── list_sobjects_success_response.json
│ │ └── sobject_find_success_response.json
│ ├── refresh_error_response.json
│ ├── auth_error_response.json
│ ├── chatter
│ │ ├── trending_topics_get_success_response.json
│ │ ├── groups_photo_get_id_success_response.json
│ │ ├── users_photo_get_id_success_response.json
│ │ ├── records_get_id_error_response.json
│ │ ├── filter_feed_get_all_success_response.json
│ │ ├── records_get_id_success_response.json
│ │ ├── users_status_get_id_success_response.json
│ │ ├── likes_get_id_success_response.json
│ │ ├── groups_get_id_success_response.json
│ │ ├── group-memberships_get_id_success_response.json
│ │ ├── users_groups_get_id_success_response.json
│ │ ├── subscriptions_get_id_success_response.json
│ │ ├── groups_get_success_response.json
│ │ ├── groups_members_get_id_success_response.json
│ │ ├── users_following_get_id_success_response.json
│ │ ├── users_get_success_response.json
│ │ ├── messages_get_id_success_response.json
│ │ ├── comments_get_id_success_response.json
│ │ ├── users_get_id_success_response.json
│ │ ├── users_followers_get_id_success_response.json
│ │ ├── users_batch_get_mixed_response.json
│ │ ├── conversations_get_success_response.json
│ │ ├── messages_get_success_response.json
│ │ ├── users_batch_get_success_response.json
│ │ ├── conversations_get_id_success_response.json
│ │ ├── feed-items_get_id_success_response.json
│ │ └── news_feed_items_get_success_response.json
│ ├── reauth_success_response.json
│ ├── databasedotcom.yml
│ ├── refresh_success_response.json
│ ├── services_data_success_response.json
│ ├── omniauth_response.json
│ └── auth_success_response.json
├── lib
│ ├── chatter
│ │ ├── like_spec.rb
│ │ ├── comment_spec.rb
│ │ ├── subscription_spec.rb
│ │ ├── group_membership_spec.rb
│ │ ├── filter_feed_spec.rb
│ │ ├── record_spec.rb
│ │ ├── feed_item_spec.rb
│ │ ├── feeds_spec.rb
│ │ ├── message_spec.rb
│ │ ├── group_spec.rb
│ │ ├── conversation_spec.rb
│ │ └── user_spec.rb
│ ├── core_extensions
│ │ └── string_extensions_spec.rb
│ ├── sales_force_error_spec.rb
│ ├── collection_spec.rb
│ └── shared_behaviors
│ │ ├── resource_with_photo.rb
│ │ ├── token_refreshing_request.rb
│ │ └── restful_resource.rb
└── spec_helper.rb
├── lib
├── databasedotcom
│ ├── sobject.rb
│ ├── version.rb
│ ├── core_extensions.rb
│ ├── chatter
│ │ ├── like.rb
│ │ ├── comment.rb
│ │ ├── feeds.rb
│ │ ├── subscription.rb
│ │ ├── group_membership.rb
│ │ ├── filter_feed.rb
│ │ ├── message.rb
│ │ ├── feed_item.rb
│ │ ├── group.rb
│ │ ├── photo_methods.rb
│ │ ├── feed.rb
│ │ ├── conversation.rb
│ │ ├── record.rb
│ │ └── user.rb
│ ├── core_extensions
│ │ └── string_extensions.rb
│ ├── chatter.rb
│ ├── sales_force_error.rb
│ ├── collection.rb
│ ├── sobject
│ │ └── sobject.rb
│ └── client.rb
└── databasedotcom.rb
├── .travis.yml
├── .gitignore
├── Gemfile
├── Rakefile
├── databasedotcom.gemspec
├── MIT-LICENSE
└── README.md
/spec/fixtures/sobject/upsert_updated_success_response.json:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/databasedotcom/sobject.rb:
--------------------------------------------------------------------------------
1 | require 'databasedotcom/sobject/sobject'
2 |
3 |
--------------------------------------------------------------------------------
/lib/databasedotcom/version.rb:
--------------------------------------------------------------------------------
1 | module Databasedotcom
2 | VERSION = "1.3.2"
3 | end
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | rvm:
2 | - 1.8.7
3 | - 1.9.2
4 | - 1.9.3
5 | - 2.0.0
6 | - ree
7 | - jruby
8 |
--------------------------------------------------------------------------------
/lib/databasedotcom/core_extensions.rb:
--------------------------------------------------------------------------------
1 | require 'databasedotcom/core_extensions/string_extensions'
2 |
--------------------------------------------------------------------------------
/spec/fixtures/sobject/upsert_created_success_response.json:
--------------------------------------------------------------------------------
1 | {"id":"foo" +
2 | "","errors":[],"success":true}
--------------------------------------------------------------------------------
/spec/fixtures/refresh_error_response.json:
--------------------------------------------------------------------------------
1 | {"error":"invalid_grant","error_description":"expired access/refresh token"}
--------------------------------------------------------------------------------
/spec/fixtures/sobject/query_empty_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "totalSize" : 0,
3 | "done" : true,
4 | "records" : [ ]
5 | }
--------------------------------------------------------------------------------
/spec/fixtures/auth_error_response.json:
--------------------------------------------------------------------------------
1 | {"error":"invalid_grant","error_description":"authentication failure - Invalid Password"}
--------------------------------------------------------------------------------
/spec/fixtures/sobject/create_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "id" : "some_id",
3 | "errors" : [ ],
4 | "success" : true
5 | }
--------------------------------------------------------------------------------
/spec/fixtures/sobject/list_sobjects_error_response.json:
--------------------------------------------------------------------------------
1 | [{"message":"Session expired or invalid","errorCode":"INVALID_SESSION_ID"}]
--------------------------------------------------------------------------------
/spec/fixtures/sobject/query_error_response.json:
--------------------------------------------------------------------------------
1 | [ {
2 | "message" : "error_message",
3 | "errorCode" : "INVALID_FIELD"
4 | } ]
--------------------------------------------------------------------------------
/spec/fixtures/sobject/describe_sobjects_error_response.json:
--------------------------------------------------------------------------------
1 | [{"message":"Session expired or invalid","errorCode":"INVALID_SESSION_ID"}]
--------------------------------------------------------------------------------
/spec/fixtures/chatter/trending_topics_get_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "topics": [
3 | {
4 | "name": "dude"
5 | }
6 | ]
7 | }
--------------------------------------------------------------------------------
/spec/fixtures/chatter/groups_photo_get_id_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "smallPhotoUrl": "/small/photo",
3 | "largePhotoUrl": "/large/photo"
4 | }
--------------------------------------------------------------------------------
/spec/fixtures/sobject/upsert_multiple_error_response.json:
--------------------------------------------------------------------------------
1 | ["/services/data/v23.0/sobjects/Whizbang/foo","/services/data/v23.0/sobjects/Whizbang/bar"]
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .rvmrc
2 | *.gem
3 | .bundle
4 | Gemfile.lock
5 | pkg/*
6 | .idea/*
7 | bin/*
8 | doc/*
9 | .DS_Store
10 | **/.DS_Store
11 | .yardoc
12 |
--------------------------------------------------------------------------------
/spec/fixtures/sobject/upsert_error_response.json:
--------------------------------------------------------------------------------
1 | [{"message":"Provided external ID field does not exist or is not accessible: Namez","errorCode":"NOT_FOUND"}]
--------------------------------------------------------------------------------
/spec/fixtures/chatter/users_photo_get_id_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "smallPhotoUrl": "/profilephoto/005/T",
3 | "largePhotoUrl": "/profilephoto/005/F"
4 | }
--------------------------------------------------------------------------------
/spec/fixtures/sobject/delete_error_response.json:
--------------------------------------------------------------------------------
1 | [{"message":"Provided external ID field does not exist or is not accessible: rid","errorCode":"NOT_FOUND"}]
2 |
--------------------------------------------------------------------------------
/spec/fixtures/sobject/sobject_describe_error_response.json:
--------------------------------------------------------------------------------
1 | [ {
2 | "message" : "The requested resource does not exist",
3 | "errorCode" : "NOT_FOUND"
4 | } ]
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | # Specify your gem's dependencies in databasedotcom.gemspec
4 | gemspec
5 |
6 | gem "jruby-openssl", :platforms => :jruby
7 |
--------------------------------------------------------------------------------
/spec/fixtures/chatter/records_get_id_error_response.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "message": "The requested resource does not exist",
4 | "errorCode": "NOT_FOUND"
5 | }
6 | ]
--------------------------------------------------------------------------------
/spec/fixtures/sobject/write_error_response.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "message":"No such column 'foo' on sobject of type Bar",
4 | "errorCode":"INVALID_FIELD"
5 | }
6 | ]
--------------------------------------------------------------------------------
/spec/fixtures/sobject/search_error_response.json:
--------------------------------------------------------------------------------
1 | [ {
2 | "message" : "No search term found. The search term must be enclosed in braces.",
3 | "errorCode" : "MALFORMED_SEARCH"
4 | } ]
--------------------------------------------------------------------------------
/spec/fixtures/sobject/sobject_find_error_response.json:
--------------------------------------------------------------------------------
1 | [ {
2 | "message" : "Provided external ID field does not exist or is not accessible: 23foo",
3 | "errorCode" : "NOT_FOUND"
4 | } ]
--------------------------------------------------------------------------------
/spec/lib/chatter/like_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rspec'
2 | require 'spec_helper'
3 | require 'databasedotcom'
4 |
5 | describe Databasedotcom::Chatter::Like do
6 | it_should_behave_like("a restful resource")
7 | end
--------------------------------------------------------------------------------
/spec/lib/chatter/comment_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rspec'
2 | require 'spec_helper'
3 | require 'databasedotcom'
4 |
5 | describe Databasedotcom::Chatter::Comment do
6 | it_should_behave_like "a restful resource"
7 | end
--------------------------------------------------------------------------------
/lib/databasedotcom/chatter/like.rb:
--------------------------------------------------------------------------------
1 | require 'databasedotcom/chatter/record'
2 |
3 | module Databasedotcom
4 | module Chatter
5 | # A like on a FeedItem
6 | class Like < Record
7 | end
8 | end
9 | end
--------------------------------------------------------------------------------
/spec/fixtures/reauth_success_response.json:
--------------------------------------------------------------------------------
1 | {"id":"https://login.salesforce.com/id/foo/bar","issued_at":"1309974610026","instance_url":"https://na1.salesforce.com","signature":"sig=","access_token":"new_access_token"}
--------------------------------------------------------------------------------
/spec/fixtures/databasedotcom.yml:
--------------------------------------------------------------------------------
1 | ---
2 | client_secret: client_secret
3 | client_id: client_id
4 | debugging: true
5 | host: bro.baz
6 | version: 88
7 | ca_file: other/ca/file.cert
8 | verify_mode: OpenSSL::SSL::VERIFY_PEER
--------------------------------------------------------------------------------
/spec/fixtures/chatter/filter_feed_get_all_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "feeds": [{
3 | "label": "Contacts",
4 | "feedUrl": "/services/data/v23.0/chatter/feeds/filter/005x0000000JbFuAAK/003"
5 | }]
6 | }
--------------------------------------------------------------------------------
/spec/fixtures/chatter/records_get_id_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Admin User",
3 | "id": "005x0000000JbFuAAK",
4 | "type": "User",
5 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
6 | }
--------------------------------------------------------------------------------
/spec/lib/chatter/subscription_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rspec'
2 | require 'spec_helper'
3 | require 'databasedotcom'
4 |
5 | describe Databasedotcom::Chatter::Subscription do
6 | it_should_behave_like("a restful resource")
7 | end
--------------------------------------------------------------------------------
/spec/lib/chatter/group_membership_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rspec'
2 | require 'spec_helper'
3 | require 'databasedotcom'
4 |
5 | describe Databasedotcom::Chatter::GroupMembership do
6 | it_should_behave_like("a restful resource")
7 | end
--------------------------------------------------------------------------------
/lib/databasedotcom/chatter/comment.rb:
--------------------------------------------------------------------------------
1 | require 'databasedotcom/chatter/record'
2 |
3 | module Databasedotcom
4 | module Chatter
5 |
6 | # A comment posted on a FeedItem.
7 | class Comment < Record
8 | end
9 | end
10 | end
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'webmock/rspec'
2 | Encoding.default_external = Encoding::UTF_8 if defined?(Encoding)
3 | Dir.glob(File.join(File.dirname(__FILE__), 'lib', 'shared_behaviors', '**', '*.rb')).each do |f|
4 | require f
5 | end
6 |
--------------------------------------------------------------------------------
/lib/databasedotcom/chatter/feeds.rb:
--------------------------------------------------------------------------------
1 | require 'databasedotcom/chatter/feed'
2 |
3 | Databasedotcom::Chatter::FEED_TYPES.each do |feed_type|
4 | Databasedotcom::Chatter.const_set("#{feed_type}Feed", Class.new(Databasedotcom::Chatter::Feed))
5 | end
6 |
--------------------------------------------------------------------------------
/lib/databasedotcom/chatter/subscription.rb:
--------------------------------------------------------------------------------
1 | require 'databasedotcom/chatter/record'
2 |
3 | module Databasedotcom
4 | module Chatter
5 | # A representation of a user "following" some other entity.
6 | class Subscription < Record
7 | end
8 | end
9 | end
--------------------------------------------------------------------------------
/spec/fixtures/refresh_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "https://login.salesforce.com/id/foo/bar",
3 | "issued_at": "1309974610026",
4 | "instance_url": "https://na1.salesforce.com",
5 | "signature": "sig=",
6 | "access_token": "refreshed_access_token"
7 | }
--------------------------------------------------------------------------------
/spec/fixtures/services_data_success_response.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "version": "20.0",
4 | "url": "/services/data/v20.0",
5 | "label": "Winter '11"
6 | },
7 | {
8 | "version": "100",
9 | "url": "/services/data/v100",
10 | "label": "Spring '86"
11 | }
12 | ]
--------------------------------------------------------------------------------
/lib/databasedotcom/chatter/group_membership.rb:
--------------------------------------------------------------------------------
1 | require 'databasedotcom/chatter/record'
2 |
3 | module Databasedotcom
4 | module Chatter
5 | # A GroupMembership represents the membership of a certain User in a certain Group.
6 | class GroupMembership < Record
7 | end
8 | end
9 | end
--------------------------------------------------------------------------------
/lib/databasedotcom/core_extensions/string_extensions.rb:
--------------------------------------------------------------------------------
1 | # This extends String to add the +resourcerize+ method.
2 | class String
3 |
4 | # Dasherizes and downcases a camelcased string. Used for Feed types.
5 | def resourcerize
6 | self.gsub(/([a-z])([A-Z])/, '\1-\2').downcase
7 | end
8 |
9 | end
10 |
--------------------------------------------------------------------------------
/spec/fixtures/sobject/org_query_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "totalSize" : 1,
3 | "done" : true,
4 | "records" : [ {
5 | "attributes" : {
6 | "type" : "Organization",
7 | "url" : "/services/data/v20.0/sobjects/Organization/00Dx0000000BV7z"
8 | },
9 | "Id" : "00Dx0000000BV7z"
10 | } ]
11 | }
--------------------------------------------------------------------------------
/spec/fixtures/sobject/query_paginated_last_page_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "totalSize" : 2,
3 | "done" : true,
4 | "records" : [ {
5 | "attributes" : {
6 | "type" : "Whizbang",
7 | "url" : "/services/data/v20.0/sobjects/Whizbang/foo"
8 | },
9 | "Text_Label" : "Last Page"
10 | } ]
11 | }
--------------------------------------------------------------------------------
/spec/fixtures/omniauth_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "provider":"salesforce",
3 | "uid":"https://login.salesforce.com/id/00Dx0000000BV7z/005x00000012Q9P",
4 | "credentials":{
5 | "token":"access_token",
6 | "instance_url":"https://na1.salesforce.com",
7 | "refresh_token":"refresh_token"
8 | }
9 | }
--------------------------------------------------------------------------------
/lib/databasedotcom.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/core_ext'
2 |
3 | require 'databasedotcom/version'
4 | require 'databasedotcom/core_extensions'
5 | require 'databasedotcom/client'
6 | require 'databasedotcom/sales_force_error'
7 | require 'databasedotcom/collection'
8 | require 'databasedotcom/sobject'
9 | require 'databasedotcom/chatter'
10 |
--------------------------------------------------------------------------------
/spec/fixtures/sobject/query_paginated_first_page_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "totalSize" : 2,
3 | "done" : false,
4 | "nextRecordsUrl" : "/next/page/url",
5 | "records" : [ {
6 | "attributes" : {
7 | "type" : "Whizbang",
8 | "url" : "/services/data/v20.0/sobjects/Whizbang/foo"
9 | },
10 | "Text_Label" : "First Page"
11 | } ]
12 | }
--------------------------------------------------------------------------------
/spec/fixtures/auth_success_response.json:
--------------------------------------------------------------------------------
1 | {"id":"https://login.salesforce.com/id/00Dx0000000BV7z/005x00000012Q9P","issued_at":"1278448832702","instance_url":"https://na1.salesforce.com","signature":"0CmxinZir53Yex7nE0TD+zMpvIWYGb/bdJh6XfOH6EQ=","access_token":"00Dx0000000BV7z!AR8AQAxo9UfVkh8AlV0Gomt9Czx9LjHnSSpwBMmbRcgKFmxOtvxjTrKW19ye6PE3Ds1eQz3z8jr3W7_VbWmEu4Q8TVGSTHxs"}
--------------------------------------------------------------------------------
/spec/fixtures/sobject/search_success_response.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "attributes" : {
4 | "type" : "Whizbang",
5 | "url" : "/services/data/v23.0/sobjects/Whizbang/foo"
6 | },
7 | "Id" : "foo"
8 | },
9 | {
10 | "attributes" : {
11 | "type" : "Whizbang",
12 | "url" : "/services/data/v23.0/sobjects/Whizbang/bar"
13 | },
14 | "Id" : "bar"
15 | }
16 | ]
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler'
2 | require 'rspec/core/rake_task'
3 | Bundler::GemHelper.install_tasks
4 |
5 | RSpec::Core::RakeTask.new(:spec)
6 |
7 | desc "runs specs on all supported rubies"
8 | task :spec_rubies do
9 | system "rvm ruby-1.9.2-p180@databasedotcom,ruby-1.8.7-p357@databasedotcom,ruby-1.9.3@databasedotcom,ree@databasedotcom --create do rake spec"
10 | end
11 |
12 | task :default => :spec
13 |
--------------------------------------------------------------------------------
/spec/fixtures/sobject/recent_success_response.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "attributes" : {
4 | "type" : "Whizbang",
5 | "url" : "/services/data/v23.0/sobjects/Whizbang/foo"
6 | },
7 | "Id" : "foo",
8 | "Name" : "whatever"
9 | },
10 | {
11 | "attributes" : {
12 | "type" : "Whizbang",
13 | "url" : "/services/data/v23.0/sobjects/Whizbang/bar"
14 | },
15 | "Id" : "bar",
16 | "Name" : "whatever"
17 | }
18 | ]
--------------------------------------------------------------------------------
/lib/databasedotcom/chatter.rb:
--------------------------------------------------------------------------------
1 | require 'databasedotcom/chatter/feeds'
2 | require 'databasedotcom/chatter/filter_feed'
3 | require 'databasedotcom/chatter/feed_item'
4 | require 'databasedotcom/chatter/comment'
5 | require 'databasedotcom/chatter/like'
6 | require 'databasedotcom/chatter/user'
7 | require 'databasedotcom/chatter/group'
8 | require 'databasedotcom/chatter/group_membership'
9 | require 'databasedotcom/chatter/subscription'
10 | require 'databasedotcom/chatter/conversation'
11 | require 'databasedotcom/chatter/message'
--------------------------------------------------------------------------------
/lib/databasedotcom/chatter/filter_feed.rb:
--------------------------------------------------------------------------------
1 | module Databasedotcom
2 | module Chatter
3 | # Filter feeds contain items pertaining to both a user and another specified resource.
4 | class FilterFeed < Feed
5 |
6 | # Lists all FilterFeeds for the user with id _user_id_.
7 | def self.feeds(client, user_id="me")
8 | url = "/services/data/v#{client.version}/chatter/feeds/filter/#{user_id}"
9 | result = client.http_get(url)
10 | JSON.parse(result.body)
11 | end
12 | end
13 | end
14 | end
--------------------------------------------------------------------------------
/spec/fixtures/chatter/users_status_get_id_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK/status",
3 |
4 | "body": {
5 | "text": "I love #frogs",
6 | "messageSegments": [
7 | {
8 | "type": "Text",
9 | "text": "I love "
10 | },
11 | {
12 | "url": "/services/data/v23.0/chatter/feed-items?q=%23frogs",
13 | "tag": "frogs",
14 | "type": "Hashtag",
15 | "text": "#frogs"
16 | }
17 | ]
18 | },
19 | "parentId": "005x0000000JbFuAAK"
20 | }
--------------------------------------------------------------------------------
/spec/fixtures/chatter/likes_get_id_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "0I0x000000001XiCAI",
3 | "user": {
4 | "name": "Admin User",
5 | "title": null,
6 | "firstName": "Admin",
7 | "lastName": "User",
8 | "companyName": "Pivotal",
9 | "mySubscription": null,
10 | "photo": {
11 | "smallPhotoUrl": "/profilephoto/005/T",
12 | "largePhotoUrl": "/profilephoto/005/F"
13 | },
14 | "isChatterGuest": false,
15 | "id": "005x0000000JbFuAAK",
16 | "type": "User",
17 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
18 | },
19 | "url": "/services/data/v23.0/chatter/likes/0I0x000000001XiCAI"
20 | }
--------------------------------------------------------------------------------
/spec/lib/core_extensions/string_extensions_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rspec'
2 | require 'spec_helper'
3 | require 'databasedotcom'
4 |
5 | describe "string extensions" do
6 | describe "#resourcerize" do
7 | it "resourcerizes class names" do
8 | "FooBar".resourcerize.should == "foo-bar"
9 | "User".resourcerize.should == "user"
10 | end
11 | end
12 |
13 | describe "#constantize" do
14 | context "when string is empty" do
15 | it "returns Object" do
16 | "".constantize.should eq(Object)
17 | end
18 | end
19 |
20 | context "when string ends with ::" do
21 | it "returns Object" do
22 | "Object::".constantize.should eq(Object)
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/spec/fixtures/sobject/query_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "totalSize" : 1,
3 | "done" : true,
4 | "records" : [ {
5 | "attributes" : {
6 | "type" : "Whizbang",
7 | "url" : "/services/data/v20.0/sobjects/Whizbang/foo"
8 | },
9 | "Checkbox_Label" : false,
10 | "Text_Label" : "Hi there!",
11 | "Date_Label" : "2010-01-01",
12 | "DateTime_Label" : "2011-07-07T00:37:00.000+0000",
13 | "Picklist_Multiselect_Label" : "four;six",
14 |
15 | "ParentWhizbang__r" : {
16 | "Name": "Parent Whizbang",
17 | "Text_Label": "Hello",
18 | "attributes" : {
19 | "type" : "Whizbang",
20 | "url" : "/services/data/v20.0/sobjects/Whizbang/bar"
21 | }
22 | }
23 | } ]
24 | }
25 |
--------------------------------------------------------------------------------
/spec/lib/chatter/filter_feed_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rspec'
2 | require 'spec_helper'
3 | require 'databasedotcom'
4 |
5 | describe Databasedotcom::Chatter::FilterFeed do
6 | describe ".feeds" do
7 | before do
8 | @response = File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/filter_feed_get_all_success_response.json"))
9 | @client_mock = double("client", :version => "23")
10 | @client_mock.should_receive(:http_get).with("/services/data/v23/chatter/feeds/filter/uid").and_return(double("response", :body => @response))
11 | end
12 |
13 | it "returns a hash describing the filter feeds" do
14 | Databasedotcom::Chatter::FilterFeed.feeds(@client_mock, "uid").should be_instance_of(Hash)
15 | end
16 | end
17 | end
--------------------------------------------------------------------------------
/spec/fixtures/chatter/groups_get_id_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "All Pivotal",
3 | "description": "Get company announcements and other important updates",
4 | "visibility": "PublicAccess",
5 | "memberCount": 1,
6 | "canHaveChatterGuests": false,
7 | "mySubscription": {
8 | "id": "0FBx000000006TrGAI",
9 | "url": "/services/data/v23.0/chatter/group-memberships/0FBx000000006TrGAI"
10 | },
11 | "myRole": "GroupOwner",
12 | "photo": {
13 | "smallPhotoUrl": "/profilephoto/729x000000005HB/T",
14 | "largePhotoUrl": "/profilephoto/729x000000005HB/F"
15 | },
16 | "id": "0F9x000000002HVCAY",
17 | "type": "CollaborationGroup",
18 | "url": "/services/data/v23.0/chatter/groups/0F9x000000002HVCAY"
19 | }
--------------------------------------------------------------------------------
/spec/fixtures/chatter/group-memberships_get_id_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "0FBx000000006TrGAI",
3 | "user": {
4 | "name": "Admin User",
5 | "title": null,
6 | "firstName": "Admin",
7 | "lastName": "User",
8 | "companyName": "Pivotal",
9 | "mySubscription": null,
10 | "photo": {
11 | "smallPhotoUrl": "/profilephoto/005/T",
12 | "largePhotoUrl": "/profilephoto/005/F"
13 | },
14 | "isChatterGuest": false,
15 | "id": "005x0000000JbFuAAK",
16 | "type": "User",
17 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
18 | },
19 | "url": "/services/data/v23.0/chatter/group-memberships/0FBx000000006TrGAI",
20 | "role": "GROUP_OWNER"
21 | }
--------------------------------------------------------------------------------
/spec/fixtures/chatter/users_groups_get_id_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "total": 1,
3 | "groups": [{
4 | "name": "All Pivotal",
5 | "description": "Get company announcements and other important updates",
6 | "visibility": "PublicAccess",
7 | "memberCount": 1,
8 | "photo": {
9 | "smallPhotoUrl": "/profilephoto/729x000000005HB/T",
10 | "largePhotoUrl": "/profilephoto/729x000000005HB/F"
11 | },
12 | "canHaveChatterGuests": false,
13 | "mySubscription": null,
14 | "myRole": "GroupOwner",
15 | "id": "0F9x000000002HVCAY",
16 | "type": "CollaborationGroup",
17 | "url": "/services/data/v23.0/chatter/groups/0F9x000000002HVCAY"
18 | }],
19 | "previousPageUrl": null,
20 | "nextPageUrl": null,
21 | "currentPageUrl": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK/groups"
22 | }
--------------------------------------------------------------------------------
/lib/databasedotcom/sales_force_error.rb:
--------------------------------------------------------------------------------
1 | module Databasedotcom
2 | # An exception raised when any non successful request is made to Force.com.
3 | class SalesForceError < StandardError
4 | # the Net::HTTPResponse from the API call
5 | attr_accessor :response
6 | # the +errorCode+ from the server response body
7 | attr_accessor :error_code
8 |
9 | def initialize(response)
10 | self.response = response
11 | parsed_body = JSON.parse(response.body) rescue nil
12 | if parsed_body
13 | if parsed_body.is_a?(Array)
14 | message = parsed_body[0]["message"]
15 | self.error_code = parsed_body[0]["errorCode"]
16 | else
17 | message = parsed_body["error_description"]
18 | self.error_code = parsed_body["error"]
19 | end
20 | else
21 | message = response.body
22 | end
23 | super(message)
24 | end
25 | end
26 | end
--------------------------------------------------------------------------------
/spec/fixtures/chatter/subscriptions_get_id_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "0E8x00000000aenCAA",
3 | "url": "/services/data/v23.0/chatter/subscriptions/0E8x00000000aenCAA",
4 | "subject": {
5 | "name": "Richard Zhao",
6 | "id": "003x0000001X6w9AAC",
7 | "type": "Contact",
8 | "url": "/services/data/v23.0/chatter/records/003x0000001X6w9AAC"
9 | },
10 | "subscriber": {
11 | "name": "Admin User",
12 | "title": null,
13 | "firstName": "Admin",
14 | "lastName": "User",
15 | "companyName": "Pivotal",
16 | "mySubscription": null,
17 | "photo": {
18 | "smallPhotoUrl": "/profilephoto/005/T",
19 | "largePhotoUrl": "/profilephoto/005/F"
20 | },
21 | "isChatterGuest": false,
22 | "id": "005x0000000JbFuAAK",
23 | "type": "User",
24 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
25 | }
26 | }
--------------------------------------------------------------------------------
/spec/fixtures/chatter/groups_get_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "groups": [{
3 | "name": "All Pivotal",
4 | "description": "Get company announcements and other important updates",
5 | "visibility": "PublicAccess",
6 | "memberCount": 1,
7 | "photo": {
8 | "smallPhotoUrl": "/profilephoto/729x000000005HB/T",
9 | "largePhotoUrl": "/profilephoto/729x000000005HB/F"
10 | },
11 | "canHaveChatterGuests": false,
12 | "mySubscription": {
13 | "id": "0FBx000000006TrGAI",
14 | "url": "/services/data/v23.0/chatter/group-memberships/0FBx000000006TrGAI"
15 | },
16 | "myRole": "GroupOwner",
17 | "id": "0F9x000000002HVCAY",
18 | "type": "CollaborationGroup",
19 | "url": "/services/data/v23.0/chatter/groups/0F9x000000002HVCAY"
20 | }],
21 | "previousPageUrl": null,
22 | "nextPageUrl": null,
23 | "currentPageUrl": "/services/data/v23.0/chatter/groups"
24 | }
--------------------------------------------------------------------------------
/spec/fixtures/sobject/describe_sobjects_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "encoding" : "UTF-8",
3 | "maxBatchSize" : 200,
4 | "sobjects" : [ {
5 | "name" : "Account",
6 | "label" : "Account",
7 | "custom" : false,
8 | "keyPrefix" : "001",
9 | "labelPlural" : "Accounts",
10 | "layoutable" : true,
11 | "activateable" : false,
12 | "updateable" : true,
13 | "urls" : {
14 | "sobject" : "/services/data/v23.0/sobjects/Account",
15 | "describe" : "/services/data/v23.0/sobjects/Account/describe",
16 | "rowTemplate" : "/services/data/v23.0/sobjects/Account/{ID}"
17 | },
18 | "searchable" : true,
19 | "createable" : true,
20 | "customSetting" : false,
21 | "deletable" : true,
22 | "deprecatedAndHidden" : false,
23 | "feedEnabled" : true,
24 | "mergeable" : true,
25 | "queryable" : true,
26 | "replicateable" : true,
27 | "retrieveable" : true,
28 | "undeletable" : true,
29 | "triggerable" : true
30 | }]
31 | }
--------------------------------------------------------------------------------
/spec/fixtures/sobject/list_sobjects_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "encoding" : "UTF-8",
3 | "maxBatchSize" : 200,
4 | "sobjects" : [ {
5 | "name" : "Account",
6 | "label" : "Account",
7 | "custom" : false,
8 | "keyPrefix" : "001",
9 | "labelPlural" : "Accounts",
10 | "layoutable" : true,
11 | "activateable" : false,
12 | "updateable" : true,
13 | "urls" : {
14 | "sobject" : "/services/data/v23.0/sobjects/Account",
15 | "describe" : "/services/data/v23.0/sobjects/Account/describe",
16 | "rowTemplate" : "/services/data/v23.0/sobjects/Account/{ID}"
17 | },
18 | "searchable" : true,
19 | "createable" : true,
20 | "customSetting" : false,
21 | "deletable" : true,
22 | "deprecatedAndHidden" : false,
23 | "feedEnabled" : true,
24 | "mergeable" : true,
25 | "queryable" : true,
26 | "replicateable" : true,
27 | "retrieveable" : true,
28 | "undeletable" : true,
29 | "triggerable" : true
30 | }]
31 | }
--------------------------------------------------------------------------------
/databasedotcom.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | $:.push File.expand_path('../lib', __FILE__)
3 | require 'databasedotcom/version'
4 |
5 | Gem::Specification.new do |s|
6 | s.name = 'databasedotcom'
7 | s.version = Databasedotcom::VERSION
8 | s.platform = Gem::Platform::RUBY
9 | s.authors = ['Glenn Gillen, Danny Burkes & Richard Zhao']
10 | s.email = ['me@glenngillen.com']
11 | s.homepage = 'https://github.com/heroku/databasedotcom'
12 | s.summary = %q{A ruby wrapper for the Force.com REST API}
13 | s.description = %q{A ruby wrapper for the Force.com REST API}
14 |
15 | s.rubyforge_project = 'databasedotcom'
16 |
17 | s.files = Dir['README.md', 'MIT-LICENSE', 'lib/**/*']
18 | s.require_paths = ['lib']
19 |
20 | s.add_dependency 'multipart-post', '~> 1.1'
21 | s.add_dependency 'json'
22 | s.add_dependency 'activesupport'
23 |
24 | s.add_development_dependency 'rspec', '~> 2.6'
25 | s.add_development_dependency 'webmock'
26 | s.add_development_dependency 'rake', '>= 0.8.6'
27 | end
28 |
--------------------------------------------------------------------------------
/spec/fixtures/sobject/sobject_find_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "attributes" : {
3 | "type" : "Whizbang",
4 | "url" : "/services/data/v20.0/sobjects/Whizbang/23foo"
5 | },
6 | "Id" : "23foo",
7 | "OwnerId" : "owner_id",
8 | "IsDeleted" : false,
9 | "Name" : "My First Whizbang",
10 | "CreatedById" : "created_by_id",
11 | "LastModifiedById" : "last_modified_by_id",
12 | "Auto_Number" : "A-1",
13 | "Checkbox_Label" : true,
14 | "Currency_Label" : 23.0,
15 | "Date_Label" : "2010-01-01",
16 | "DateTime_Label" : "2011-07-07T00:37:00.000+0000",
17 | "OtherDateTime_Label" : null,
18 | "Email_Label" : "danny@example.com",
19 | "Number_Label" : 23.0,
20 | "Percent_Label" : 33.0,
21 | "Phone_Label" : "(415) 555-1212",
22 | "Picklist_Label" : "one",
23 | "Picklist_Multiselect_Label" : "four;six",
24 | "Text_Label" : "some text",
25 | "TextArea_Label" : "a text area",
26 | "TextAreaLong_Label" : "a loooooooooooooong text area",
27 | "TextAreaRich_Label" : "Rich text",
28 | "URL_Label" : "http://pivotallabs.com"
29 | }
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/spec/fixtures/chatter/groups_members_get_id_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "members": [{
3 | "id": "0FBx000000006TrGAI",
4 | "user": {
5 | "name": "Admin User",
6 | "title": null,
7 | "firstName": "Admin",
8 | "lastName": "User",
9 | "companyName": "Pivotal",
10 | "mySubscription": null,
11 | "photo": {
12 | "smallPhotoUrl": "/profilephoto/005/T",
13 | "largePhotoUrl": "/profilephoto/005/F"
14 | },
15 | "isChatterGuest": false,
16 | "id": "005x0000000JbFuAAK",
17 | "type": "User",
18 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
19 | },
20 | "url": "/services/data/v23.0/chatter/group-memberships/0FBx000000006TrGAI",
21 | "role": "GROUP_OWNER"
22 | }],
23 | "previousPageUrl": null,
24 | "nextPageUrl": null,
25 | "currentPageUrl": "/services/data/v23.0/chatter/groups/0F9x000000002HVCAY/members",
26 | "totalMemberCount": 1,
27 | "myMembership": {
28 | "id": "0FBx000000006TrGAI",
29 | "url": "/services/data/v23.0/chatter/group-memberships/0FBx000000006TrGAI"
30 | }
31 | }
--------------------------------------------------------------------------------
/spec/fixtures/chatter/users_following_get_id_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "total": 1,
3 | "previousPageUrl": null,
4 | "nextPageUrl": null,
5 | "currentPageUrl": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK/following",
6 | "following": [{
7 | "id": "0E8x00000000aenCAA",
8 | "url": "/services/data/v23.0/chatter/subscriptions/0E8x00000000aenCAA",
9 | "subject": {
10 | "name": "Richard Zhao",
11 | "id": "003x0000001X6w9AAC",
12 | "type": "Contact",
13 | "url": "/services/data/v23.0/chatter/records/003x0000001X6w9AAC"
14 | },
15 | "subscriber": {
16 | "name": "Admin User",
17 | "title": null,
18 | "firstName": "Admin",
19 | "lastName": "User",
20 | "companyName": "Pivotal",
21 | "mySubscription": null,
22 | "photo": {
23 | "smallPhotoUrl": "/profilephoto/005/T",
24 | "largePhotoUrl": "/profilephoto/005/F"
25 | },
26 | "isChatterGuest": false,
27 | "id": "005x0000000JbFuAAK",
28 | "type": "User",
29 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
30 | }
31 | }]
32 | }
--------------------------------------------------------------------------------
/lib/databasedotcom/chatter/message.rb:
--------------------------------------------------------------------------------
1 | require 'databasedotcom/chatter/record'
2 |
3 | module Databasedotcom
4 | module Chatter
5 | # A private message between two or more Users
6 | class Message < Record
7 |
8 | # Send a private message with the content _text_ to each user in the _recipients_ list.
9 | def self.send_message(client, recipients, text)
10 | url = "/services/data/v#{client.version}/chatter/users/me/messages"
11 | recipients = recipients.is_a?(Array) ? recipients : [recipients]
12 | response = client.http_post(url, nil, :text => text, :recipients => recipients.join(','))
13 | Message.new(client, response.body)
14 | end
15 |
16 | # Send a reply to the message identified by _in_reply_to_message_id_ with content _text_.
17 | def self.reply(client, in_reply_to_message_id, text)
18 | url = "/services/data/v#{client.version}/chatter/users/me/messages"
19 | response = client.http_post(url, nil, :text => text, :inReplyTo => in_reply_to_message_id)
20 | Message.new(client, response.body)
21 | end
22 |
23 | # Send a reply to this Message with content _text_.
24 | def reply(text)
25 | self.class.reply(self.client, self.id, text)
26 | end
27 | end
28 | end
29 | end
--------------------------------------------------------------------------------
/spec/fixtures/chatter/users_get_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "users": [{
3 | "name": "Admin User",
4 | "title": null,
5 | "firstName": "Admin",
6 | "lastName": "User",
7 | "companyName": "Pivotal",
8 | "mySubscription": null,
9 | "photo": {
10 | "smallPhotoUrl": "/profilephoto/005/T",
11 | "largePhotoUrl": "/profilephoto/005/F"
12 | },
13 | "isChatterGuest": false,
14 | "id": "005x0000000JbFuAAK",
15 | "type": "User",
16 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
17 | },
18 | {
19 | "name": "Chatter Expert",
20 | "title": null,
21 | "firstName": null,
22 | "lastName": "Chatter Expert",
23 | "companyName": null,
24 | "mySubscription": {
25 | "id": "005x0000000JbGVAA0",
26 | "url": "/services/data/v23.0/chatter/subscriptions/0E8x00000000aesCAA"
27 | },
28 | "photo": {
29 | "smallPhotoUrl": "/profilephoto/729x000000005Gw/T",
30 | "largePhotoUrl": "/profilephoto/729x000000005Gw/F"
31 | },
32 | "isChatterGuest": false,
33 | "id": "005x0000000JbGVAA0",
34 | "type": "User",
35 | "url": "/services/data/v23.0/chatter/users/005x0000000JbGVAA0"
36 | }],
37 | "previousPageUrl": null,
38 | "nextPageUrl": null,
39 | "currentPageUrl": "/services/data/v23.0/chatter/users"
40 | }
--------------------------------------------------------------------------------
/spec/fixtures/chatter/messages_get_id_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "03Jx000000000FYEAY",
3 | "sender": {
4 | "name": "Admin User",
5 | "title": null,
6 | "firstName": "Admin",
7 | "lastName": "User",
8 | "companyName": "Pivotal",
9 | "photo": {
10 | "smallPhotoUrl": "/profilephoto/005/T",
11 | "largePhotoUrl": "/profilephoto/005/F"
12 | },
13 | "mySubscription": null,
14 | "isChatterGuest": false,
15 | "id": "005x0000000JbFuAAK",
16 | "type": "User",
17 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
18 | },
19 | "url": "/services/data/v23.0/chatter/users/me/messages/03Jx000000000FYEAY",
20 | "body": {
21 | "text": "Yes",
22 | "messageSegments": []
23 | },
24 | "recipients": [{
25 | "name": "Chatter Expert",
26 | "title": null,
27 | "firstName": null,
28 | "lastName": "Chatter Expert",
29 | "companyName": null,
30 | "photo": {
31 | "smallPhotoUrl": "/profilephoto/729x000000005Gw/T",
32 | "largePhotoUrl": "/profilephoto/729x000000005Gw/F"
33 | },
34 | "mySubscription": null,
35 | "isChatterGuest": false,
36 | "id": "005x0000000JbGVAA0",
37 | "type": "User",
38 | "url": "/services/data/v23.0/chatter/users/005x0000000JbGVAA0"
39 | }],
40 | "sentDate": "2011-08-08T21:36:05.000Z",
41 | "conversationId": "03Mx0000000009XEAQ",
42 | "conversationUrl": "/services/data/v23.0/chatter/users/me/conversations/03Mx0000000009XEAQ"
43 | }
--------------------------------------------------------------------------------
/spec/fixtures/chatter/comments_get_id_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "parent": {
3 | "id": "005x0000000JbFuAAK",
4 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
5 | },
6 | "id": "0D7x000000005dRCAQ",
7 | "user": {
8 | "name": "Admin User",
9 | "title": null,
10 | "firstName": "Admin",
11 | "lastName": "User",
12 | "companyName": "Pivotal",
13 | "mySubscription": null,
14 | "photo": {
15 | "smallPhotoUrl": "/profilephoto/005/T",
16 | "largePhotoUrl": "/profilephoto/005/F"
17 | },
18 | "isChatterGuest": false,
19 | "id": "005x0000000JbFuAAK",
20 | "type": "User",
21 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
22 | },
23 | "clientInfo": {
24 | "applicationName": "localhost",
25 | "applicationUrl": "null"
26 | },
27 | "url": "/services/data/v23.0/chatter/comments/0D7x000000005dRCAQ",
28 | "body": {
29 | "text": "So do I like #frogs",
30 | "messageSegments": [{
31 | "type": "Text",
32 | "text": "So do I like "
33 | },
34 | {
35 | "url": "/services/data/v23.0/chatter/feed-items?q=%23frogs",
36 | "tag": "frogs",
37 | "type": "Hashtag",
38 | "text": "#frogs"
39 | }]
40 | },
41 | "createdDate": "2011-08-08T21:39:23.000+0000",
42 | "deletable": true,
43 | "feedItem": {
44 | "id": "0D5x00000001g6tCAA",
45 | "url": "/services/data/v23.0/chatter/feed-items/0D5x00000001g6tCAA"
46 | }
47 | }
--------------------------------------------------------------------------------
/lib/databasedotcom/collection.rb:
--------------------------------------------------------------------------------
1 | module Databasedotcom
2 | # A collection of Sobject or Record objects that holds a single page of results, and understands how to
3 | # retrieve the next page, if any. Inherits from Array, thus, behaves as an Enumerable.
4 |
5 | class Collection < Array
6 | attr_reader :total_size, :next_page_url, :previous_page_url, :current_page_url, :client
7 |
8 | # Creates a paginatable collection. You should never need to call this.
9 | def initialize(client, total_size, next_page_url=nil, previous_page_url=nil, current_page_url=nil) #:nodoc:
10 | @client = client
11 | @total_size = total_size
12 | @next_page_url = next_page_url
13 | @previous_page_url = previous_page_url
14 | @current_page_url = current_page_url
15 | end
16 |
17 | # Does this collection have a next page?
18 | def next_page?
19 | !!self.next_page_url
20 | end
21 |
22 | # Retrieve the next page of this collection. Returns the new collection, which is an empty collection if no next page exists
23 | def next_page
24 | self.next_page? ? @client.next_page(@next_page_url) : Databasedotcom::Collection.new(self.client, 0)
25 | end
26 |
27 | # Does this collection have a previous page?
28 | def previous_page?
29 | !!self.previous_page_url
30 | end
31 |
32 | # Retrieve the previous page of this collection. Returns the new collection, which is an empty collection if no previous page exists
33 | def previous_page
34 | self.previous_page? ? @client.previous_page(@previous_page_url) : Databasedotcom::Collection.new(self.client, 0)
35 | end
36 | end
37 | end
--------------------------------------------------------------------------------
/lib/databasedotcom/chatter/feed_item.rb:
--------------------------------------------------------------------------------
1 | require 'databasedotcom/chatter/record'
2 |
3 | module Databasedotcom
4 | module Chatter
5 |
6 | # An item in a Feed.
7 | class FeedItem < Record
8 |
9 | # Returns a Collection of comments that were posted on this FeedItem instance.
10 | def comments
11 | collection = Databasedotcom::Collection.new(self.client, self.raw_hash["comments"]["total"], self.raw_hash["comments"]["nextPageUrl"], nil, self.raw_hash["comments"]["currentPageUrl"])
12 | collection.concat(self.raw_hash["comments"]["comments"])
13 | end
14 |
15 | # Returns a Collection of likes for this FeedItem instance.
16 | def likes
17 | collection = Databasedotcom::Collection.new(self.client, self.raw_hash["likes"]["total"], self.raw_hash["likes"]["nextPageUrl"], self.raw_hash["likes"]["previousPageUrl"], self.raw_hash["likes"]["currentPageUrl"])
18 | collection.concat(self.raw_hash["likes"]["likes"])
19 | end
20 |
21 | # Like this FeedItem.
22 | def like
23 | result = self.client.http_post("/services/data/v#{self.client.version}/chatter/feed-items/#{self.id}/likes")
24 | Like.new(self.client, result.body)
25 | end
26 |
27 | # Post a Comment on this FeedItem with content _text_.
28 | def comment(text)
29 | result = self.client.http_post("/services/data/v#{self.client.version}/chatter/feed-items/#{self.id}/comments", nil, :text => text)
30 | Comment.new(self.client, result.body)
31 | end
32 |
33 | protected
34 |
35 | def self.collection_from_response(response)
36 | response["items"]
37 | end
38 | end
39 | end
40 | end
--------------------------------------------------------------------------------
/spec/fixtures/chatter/users_get_id_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "address": {
3 | "state": "CA",
4 | "country": "US",
5 | "street": "1 Market St",
6 | "city": "San Francisco",
7 | "zip": "94105"
8 | },
9 | "email": "danny@pivotallabs.com",
10 | "currentStatus": {
11 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK/status",
12 | "body": {
13 | "text": "I love #frogs",
14 | "messageSegments": [{
15 | "type": "Text",
16 | "text": "I love "
17 | },
18 | {
19 | "url": "/services/data/v23.0/chatter/feed-items?q=%23frogs",
20 | "tag": "frogs",
21 | "type": "Hashtag",
22 | "text": "#frogs"
23 | }]
24 | },
25 | "parentId": "005x0000000JbFuAAK"
26 | },
27 | "managerId": null,
28 | "followingCounts": {
29 | "total": 2,
30 | "records": 1,
31 | "people": 1
32 | },
33 | "groupCount": 1,
34 | "followersCount": 0,
35 | "managerName": null,
36 | "aboutMe": null,
37 | "isActive": true,
38 | "phoneNumbers": [],
39 | "chatterActivity": {
40 | "commentCount": 2,
41 | "commentReceivedCount": 0,
42 | "likeReceivedCount": 0,
43 | "postCount": 3
44 | },
45 | "name": "Admin User",
46 | "title": null,
47 | "firstName": "Admin",
48 | "lastName": "User",
49 | "companyName": "Pivotal",
50 | "mySubscription": null,
51 | "photo": {
52 | "smallPhotoUrl": "/profilephoto/005/T",
53 | "largePhotoUrl": "/profilephoto/005/F"
54 | },
55 | "isChatterGuest": false,
56 | "id": "005x0000000JbFuAAK",
57 | "type": "User",
58 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
59 | }
--------------------------------------------------------------------------------
/lib/databasedotcom/chatter/group.rb:
--------------------------------------------------------------------------------
1 | require 'databasedotcom/chatter/record'
2 | require 'databasedotcom/chatter/photo_methods'
3 |
4 | module Databasedotcom
5 | module Chatter
6 | # A group of Users
7 | class Group < Record
8 | include PhotoMethods
9 |
10 | # Returns a Collection of GroupMembership instances for the Group identified by _group_id_.
11 | def self.members(client, group_id)
12 | url = "/services/data/v#{client.version}/chatter/groups/#{group_id}/members"
13 | result = client.http_get(url)
14 | response = JSON.parse(result.body)
15 | collection = Databasedotcom::Collection.new(client, response["totalMemberCount"], response["nextPageUrl"], response["previousPageUrl"], response["currentPageUrl"])
16 | response["members"].each do |member|
17 | collection << GroupMembership.new(client, member)
18 | end
19 | collection
20 | end
21 |
22 | # Join the group identified by _group_id_ as the user identified by _user_id_.
23 | def self.join(client, group_id, user_id="me")
24 | url = "/services/data/v#{client.version}/chatter/groups/#{group_id}/members"
25 | response = client.http_post(url, nil, :userId => user_id)
26 | GroupMembership.new(client, response.body)
27 | end
28 |
29 | # Get a Collection of GroupMembership objects for this Group. Always makes a call to the server.
30 | def members!
31 | self.class.members(self.client, self.id)
32 | end
33 |
34 | # Get a Collection of GroupMembership objects for this Group. Returns cached data if it has been called before.
35 | def members
36 | @members ||= members!
37 | end
38 |
39 | # Join this Group as the user identified by _user_id_.
40 | def join(user_id="me")
41 | self.class.join(self.client, self.id, user_id)
42 | end
43 | end
44 | end
45 | end
--------------------------------------------------------------------------------
/spec/lib/chatter/record_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rspec'
2 | require 'spec_helper'
3 | require 'databasedotcom'
4 |
5 | describe Databasedotcom::Chatter::Record do
6 | it_should_behave_like("a restful resource")
7 |
8 | describe "with a constructed record" do
9 | before do
10 | @response = File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/records_get_id_success_response.json"))
11 | @mock_client = double("client", :version => "23")
12 | @record = Databasedotcom::Chatter::Record.new(@mock_client, @response)
13 | end
14 |
15 | describe "#client" do
16 | it "returns the client of the standard record" do
17 | @record.client.should == @mock_client
18 | end
19 | end
20 |
21 | describe "#parent" do
22 | it "returns the parent of the record" do
23 | @record.parent.should == @record.raw_hash["parent"]
24 | end
25 | end
26 |
27 | describe "#user" do
28 | it "returns the user of the record" do
29 | @record.user.should == @record.raw_hash["user"]
30 | end
31 | end
32 |
33 | describe "#delete" do
34 | it "deletes the resource" do
35 | @mock_client.should_receive(:http_delete).with("/services/data/v23/chatter/records/#{@record.id}", {})
36 | @record.delete
37 | end
38 | end
39 |
40 | describe "#reload" do
41 | it "reloads the record" do
42 | @mock_client.should_receive(:http_get).with("/services/data/v23/chatter/records/#{@record.id}", {}).and_return(double("response", :body => @response))
43 | @record.reload
44 | end
45 | end
46 |
47 | it "provides getters for common attributes" do
48 | @record.name.should == "Admin User"
49 | @record.id.should == "005x0000000JbFuAAK"
50 | @record.url.should == "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
51 | @record.type.should == "User"
52 | end
53 | end
54 | end
55 |
56 |
--------------------------------------------------------------------------------
/spec/fixtures/chatter/users_followers_get_id_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "total": 1,
3 | "followers": [{
4 | "id": "0E8x00000000aesCAA",
5 | "url": "/services/data/v23.0/chatter/subscriptions/0E8x00000000aesCAA",
6 | "subject": {
7 | "name": "Chatter Expert",
8 | "title": null,
9 | "firstName": null,
10 | "lastName": "Chatter Expert",
11 | "companyName": null,
12 | "mySubscription": {
13 | "id": "005x0000000JbGVAA0",
14 | "url": "/services/data/v23.0/chatter/subscriptions/0E8x00000000aesCAA"
15 | },
16 | "photo": {
17 | "smallPhotoUrl": "/profilephoto/729x000000005Gw/T",
18 | "largePhotoUrl": "/profilephoto/729x000000005Gw/F"
19 | },
20 | "isChatterGuest": false,
21 | "id": "005x0000000JbGVAA0",
22 | "type": "User",
23 | "url": "/services/data/v23.0/chatter/users/005x0000000JbGVAA0"
24 | },
25 | "subscriber": {
26 | "name": "Admin User",
27 | "title": null,
28 | "firstName": "Admin",
29 | "lastName": "User",
30 | "companyName": "Pivotal",
31 | "mySubscription": null,
32 | "photo": {
33 | "smallPhotoUrl": "/profilephoto/005/T",
34 | "largePhotoUrl": "/profilephoto/005/F"
35 | },
36 | "isChatterGuest": false,
37 | "id": "005x0000000JbFuAAK",
38 | "type": "User",
39 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
40 | }
41 | }],
42 | "previousPageUrl": null,
43 | "nextPageUrl": null,
44 | "currentPageUrl": "/services/data/v23.0/chatter/records/005x0000000JbGVAA0/followers",
45 | "mySubscription": {
46 | "id": "0E8x00000000aesCAA",
47 | "url": "/services/data/v23.0/chatter/subscriptions/0E8x00000000aesCAA"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/spec/lib/sales_force_error_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rspec'
2 | require 'spec_helper'
3 | require 'databasedotcom'
4 |
5 | describe Databasedotcom::SalesForceError do
6 | context "with a non-array body" do
7 | before do
8 | @response_body = File.read(File.join(File.dirname(__FILE__), "../fixtures/auth_error_response.json"))
9 | @response_json = JSON.parse(@response_body)
10 | @response = double("result", :body => @response_body)
11 | @exception = Databasedotcom::SalesForceError.new(@response)
12 | end
13 |
14 | describe "#message" do
15 | it "returns the message from the server response" do
16 | @exception.message.should == @response_json["error_description"]
17 | end
18 | end
19 |
20 | describe "#error_code" do
21 | it "returns the error code from the server response" do
22 | @exception.error_code.should == @response_json["error"]
23 | end
24 | end
25 |
26 | describe "#response" do
27 | it "returns the HTTPResponse" do
28 | @exception.response.should == @response
29 | end
30 | end
31 | end
32 |
33 | context "with a array body" do
34 | before do
35 | @response_body = File.read(File.join(File.dirname(__FILE__), "../fixtures/sobject/search_error_response.json"))
36 | @response_json = JSON.parse(@response_body)
37 | @response = double("result", :body => @response_body)
38 | @exception = Databasedotcom::SalesForceError.new(@response)
39 | end
40 |
41 | describe "#message" do
42 | it "returns the message from the server response" do
43 | @exception.message.should == @response_json[0]["message"]
44 | end
45 | end
46 |
47 | describe "#error_code" do
48 | it "returns the error code from the server response" do
49 | @exception.error_code.should == @response_json[0]["errorCode"]
50 | end
51 | end
52 |
53 | describe "#response" do
54 | it "returns the HTTPResponse" do
55 | @exception.response.should == @response
56 | end
57 | end
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/spec/fixtures/chatter/users_batch_get_mixed_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "results": [{
3 | "result": {
4 | "address": {
5 | "state": "CA",
6 | "country": "US",
7 | "street": "1 Market St",
8 | "city": "San Francisco",
9 | "zip": "94105"
10 | },
11 | "email": "danny@pivotallabs.com",
12 | "currentStatus": {
13 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK/status",
14 | "body": null,
15 | "parentId": "005x0000000JbFuAAK"
16 | },
17 | "managerId": null,
18 | "followingCounts": {
19 | "total": 1,
20 | "records": 1,
21 | "people": 0
22 | },
23 | "groupCount": 1,
24 | "followersCount": 0,
25 | "managerName": null,
26 | "aboutMe": null,
27 | "isActive": true,
28 | "phoneNumbers": [],
29 | "chatterActivity": {
30 | "commentCount": 2,
31 | "commentReceivedCount": 0,
32 | "likeReceivedCount": 0,
33 | "postCount": 3
34 | },
35 | "name": "Admin User",
36 | "title": null,
37 | "firstName": "Admin",
38 | "lastName": "User",
39 | "companyName": "Pivotal",
40 | "mySubscription": null,
41 | "photo": {
42 | "smallPhotoUrl": "/profilephoto/005/T",
43 | "largePhotoUrl": "/profilephoto/005/F"
44 | },
45 | "isChatterGuest": false,
46 | "id": "005x0000000JbFuAAK",
47 | "type": "User",
48 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
49 | },
50 | "statusCode": 200
51 | },
52 | {
53 | "result": [{
54 | "message": "The requested resource does not exist",
55 | "errorCode": "NOT_FOUND"
56 | }],
57 | "statusCode": 404
58 | }]
59 | }
--------------------------------------------------------------------------------
/spec/lib/chatter/feed_item_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rspec'
2 | require 'spec_helper'
3 | require 'databasedotcom'
4 |
5 | describe Databasedotcom::Chatter::FeedItem do
6 | it_should_behave_like("a restful resource")
7 |
8 | context "with a FeedItem object" do
9 | before do
10 | @response = JSON.parse(File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/feed-items_get_id_success_response.json")))
11 | @client_mock = double("client", :version => "23")
12 | @record = Databasedotcom::Chatter::FeedItem.new(@client_mock, @response)
13 | end
14 |
15 | describe "#comments" do
16 | it "returns a collection of comment records" do
17 | @record.comments.should be_instance_of(Databasedotcom::Collection)
18 | end
19 | end
20 |
21 | describe "#likes" do
22 | it "returns a collection of like records" do
23 | @record.likes.should be_instance_of(Databasedotcom::Collection)
24 | end
25 | end
26 |
27 | describe "#like" do
28 | it "likes the feed item" do
29 | body = JSON.parse(File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/likes_get_id_success_response.json")))
30 | @response = double("response")
31 | @response.should_receive(:body).any_number_of_times.and_return(body)
32 | @client_mock.should_receive(:http_post).with("/services/data/v23/chatter/feed-items/#{@record.id}/likes").and_return(@response)
33 | like = @record.like
34 | like.should be_instance_of(Databasedotcom::Chatter::Like)
35 | end
36 | end
37 |
38 | describe "#comment" do
39 | it "comments on the feed item" do
40 | body = JSON.parse(File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/comments_get_id_success_response.json")))
41 | @response = double("response")
42 | @response.should_receive(:body).any_number_of_times.and_return(body)
43 | @client_mock.should_receive(:http_post).with("/services/data/v23/chatter/feed-items/#{@record.id}/comments", nil, :text => "whatever").and_return(@response)
44 | comment = @record.comment("whatever")
45 | comment.should be_instance_of(Databasedotcom::Chatter::Comment)
46 | end
47 | end
48 | end
49 | end
--------------------------------------------------------------------------------
/spec/lib/chatter/feeds_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rspec'
2 | require 'spec_helper'
3 | require 'databasedotcom'
4 |
5 | Databasedotcom::Chatter::FEED_TYPES.each do |feed_type|
6 | describe "Databasedotcom::Chatter::#{feed_type}Feed" do
7 | before do
8 | @client_mock = double("client", :version => "23")
9 | end
10 |
11 | describe ".find" do
12 | before do
13 | expected_path = feed_type == "Company" ? "/services/data/v23/chatter/feeds/#{feed_type.resourcerize}/feed-items" : "/services/data/v23/chatter/feeds/#{feed_type.resourcerize}/fid/feed-items"
14 | @client_mock.should_receive(:http_get).with(expected_path).and_return(double "response", :body => {"items" => []}.to_json)
15 | end
16 |
17 | it "retrieves a #{feed_type}feed" do
18 | Databasedotcom::Chatter.const_get("#{feed_type}Feed").send(:find, @client_mock, "fid")
19 | end
20 | end
21 |
22 | describe ".post" do
23 | before do
24 | @client_mock.should_receive(:http_post).with("/services/data/v23/chatter/feeds/#{feed_type.resourcerize}/me/feed-items", nil, { :text => "text" }).and_return(double "response", :body => {"id" => "rid", "name" => "name", "type" => "posted_item", "url" => "some/url"})
25 | end
26 |
27 | it "posts text passed as a parameter and returns a new FeedItem" do
28 | result = Databasedotcom::Chatter.const_get("#{feed_type}Feed").send(:post, @client_mock, "me", :text => "text")
29 | result.should be_instance_of(Databasedotcom::Chatter::FeedItem)
30 | end
31 | end
32 |
33 | describe ".post_file" do
34 | before do
35 | @client_mock.should_receive(:http_multipart_post).with("/services/data/v23/chatter/feeds/#{feed_type.resourcerize}/me/feed-items", an_instance_of(Hash), an_instance_of(Hash)).and_return(double("response", :body => {"id" => "rid", "name" => "name", "type" => "posted_item", "url" => "some/url"}))
36 | end
37 |
38 | it "posts the file and returns a new FeedItem" do
39 | result = Databasedotcom::Chatter.const_get("#{feed_type}Feed").send(:post_file, @client_mock, "me", StringIO.new("foo"), "text/plain", "filename.txt")
40 | result.should be_instance_of(Databasedotcom::Chatter::FeedItem)
41 | end
42 | end
43 | end
44 | end
45 |
46 |
--------------------------------------------------------------------------------
/lib/databasedotcom/chatter/photo_methods.rb:
--------------------------------------------------------------------------------
1 | module Databasedotcom
2 | module Chatter
3 | # Defines methods for entities that can have photos i.e. Users, Groups.
4 | module PhotoMethods
5 | def self.included(base)
6 | base.extend ClassMethods
7 | end
8 |
9 | # Defines class methods for resources that can have photos.
10 | module ClassMethods
11 | # Returns a Hash with urls for the small and large versions of the photo for a resource.
12 | def photo(client, resource_id)
13 | url = "/services/data/v#{client.version}/chatter/#{self.resource_name}/#{resource_id}/photo"
14 | result = client.http_get(url)
15 | JSON.parse(result.body)
16 | end
17 |
18 | # Uploads a photo for a resource with id _resource_id_.
19 | #
20 | # User.upload_photo(@client, "me", File.open("SomePicture.png"), "image/png")
21 | def upload_photo(client, resource_id, io, file_type)
22 | url = "/services/data/v#{client.version}/chatter/#{self.resource_name}/#{resource_id}/photo"
23 | result = client.http_multipart_post(url, {"fileUpload" => UploadIO.new(io, file_type)})
24 | JSON.parse(result.body)
25 | end
26 |
27 | # Deletes the photo for the resource with id _resource_id_.
28 | def delete_photo(client, resource_id)
29 | client.http_delete "/services/data/v#{client.version}/chatter/#{self.resource_name}/#{resource_id}/photo"
30 | end
31 | end
32 |
33 | # Returns a Hash with urls for the small and large versions of the photo for this resource.
34 | #
35 | # User.find(@client, "me").photo #=> {"smallPhotoUrl"=>"/small/photo/url", "largePhotoUrl"=>"/large/photo/url"}
36 | def photo
37 | self.raw_hash["photo"]
38 | end
39 |
40 | # Uploads a photo for this resource.
41 | #
42 | # me = User.find(@client)
43 | # me.upload_photo(File.open("SomePicture.png"), "image/png")
44 | def upload_photo(io, file_type)
45 | self.class.upload_photo(self.client, self.id, io, file_type)
46 | end
47 |
48 | # Deletes the photo for this resource.
49 | def delete_photo
50 | self.class.delete_photo(self.client, self.id)
51 | photo
52 | end
53 | end
54 | end
55 | end
--------------------------------------------------------------------------------
/spec/lib/chatter/message_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rspec'
2 | require 'spec_helper'
3 | require 'databasedotcom'
4 |
5 | describe Databasedotcom::Chatter::Message do
6 | it_should_behave_like("a restful resource")
7 |
8 | describe ".send_message" do
9 | before do
10 | @client_mock = double("client", :version => "23")
11 | @response = double("response", :body => File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/messages_get_id_success_response.json")))
12 | end
13 |
14 | it "sends a private message" do
15 | @client_mock.should_receive(:http_post).with("/services/data/v23/chatter/users/me/messages", nil, :recipients => "rid", :text => "text").and_return(@response)
16 | Databasedotcom::Chatter::Message.send_message(@client_mock, "rid", "text").should be_instance_of(Databasedotcom::Chatter::Message)
17 | end
18 |
19 | it "supports multiple recipients" do
20 | @client_mock.should_receive(:http_post).with("/services/data/v23/chatter/users/me/messages", nil, :recipients => "rid1,rid2", :text => "text").and_return(@response)
21 | Databasedotcom::Chatter::Message.send_message(@client_mock, %w(rid1 rid2), "text").should be_instance_of(Databasedotcom::Chatter::Message)
22 | end
23 | end
24 |
25 | describe ".reply" do
26 | before do
27 | @client_mock = double("client", :version => "23")
28 | @response = double("response", :body => File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/messages_get_id_success_response.json")))
29 | end
30 |
31 | it "replies to a private message" do
32 | @client_mock.should_receive(:http_post).with("/services/data/v23/chatter/users/me/messages", nil, :inReplyTo => "mid", :text => "text").and_return(@response)
33 | Databasedotcom::Chatter::Message.reply(@client_mock, "mid", "text").should be_instance_of(Databasedotcom::Chatter::Message)
34 | end
35 | end
36 |
37 | context "with an instantiated Message" do
38 | before do
39 | msg = File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/messages_get_id_success_response.json"))
40 | @client_mock = double("client", :version => "23")
41 | @response = double("response")
42 | @message = Databasedotcom::Chatter::Message.new(@client_mock, msg)
43 | end
44 |
45 | describe "#reply" do
46 | it "replies to a message" do
47 | Databasedotcom::Chatter::Message.should_receive(:reply).with(@client_mock, @message.id, "text")
48 | @message.reply("text")
49 | end
50 | end
51 | end
52 | end
--------------------------------------------------------------------------------
/spec/lib/chatter/group_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rspec'
2 | require 'spec_helper'
3 | require 'databasedotcom'
4 |
5 | describe Databasedotcom::Chatter::Group do
6 | it_should_behave_like("a restful resource")
7 | it_should_behave_like("a resource with a photo")
8 |
9 | describe ".members" do
10 | before do
11 | @client_mock = double("client", :version => "23")
12 | @response = double("response", :body => File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/groups_members_get_id_success_response.json")))
13 | end
14 |
15 | it "gets a Collection of GroupMemberships" do
16 | @client_mock.should_receive(:http_get).with("/services/data/v23/chatter/groups/gid/members").and_return(@response)
17 | Databasedotcom::Chatter::Group.members(@client_mock, "gid").should be_instance_of(Databasedotcom::Collection)
18 | end
19 | end
20 |
21 | describe ".join" do
22 | before do
23 | @client_mock = double("client", :version => "23")
24 | @response = double("response", :body => File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/group-memberships_get_id_success_response.json")))
25 | end
26 |
27 | it "joins the group" do
28 | @client_mock.should_receive(:http_post).with("/services/data/v23/chatter/groups/gid/members", nil, :userId => "me").and_return(@response)
29 | Databasedotcom::Chatter::Group.join(@client_mock, "gid", "me").should be_instance_of(Databasedotcom::Chatter::GroupMembership)
30 | end
31 | end
32 |
33 | context "with an instantiated Group" do
34 | before do
35 | grp = File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/groups_get_id_success_response.json"))
36 | @client_mock = double("client", :version => "23")
37 | @response = double("response")
38 | @group = Databasedotcom::Chatter::Group.new(@client_mock, grp)
39 | end
40 |
41 | describe "#members!" do
42 | it "gets the members for the group" do
43 | Databasedotcom::Chatter::Group.should_receive(:members).with(@group.client, @group.id)
44 | @group.members!
45 | end
46 | end
47 |
48 | describe "#members" do
49 | it "gets the memoized members for the group" do
50 | Databasedotcom::Chatter::Group.should_receive(:members).with(@group.client, @group.id).once.and_return("foo")
51 | @group.members
52 | @group.members
53 | end
54 | end
55 |
56 | describe "#join" do
57 | it "joins the group" do
58 | Databasedotcom::Chatter::Group.should_receive(:join).with(@client_mock, @group.id, "me")
59 | @group.join
60 | end
61 | end
62 | end
63 | end
--------------------------------------------------------------------------------
/spec/fixtures/chatter/conversations_get_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "nextPageUrl": null,
3 | "currentPageUrl": "/services/data/v23.0/chatter/users/me/conversations",
4 | "conversations": [{
5 | "id": "03Mx0000000009XEAQ",
6 | "url": "/services/data/v23.0/chatter/users/me/conversations/03Mx0000000009XEAQ",
7 | "members": [{
8 | "name": "Chatter Expert",
9 | "title": null,
10 | "firstName": null,
11 | "lastName": "Chatter Expert",
12 | "companyName": null,
13 | "mySubscription": null,
14 | "photo": {
15 | "smallPhotoUrl": "/profilephoto/729x000000005Gw/T",
16 | "largePhotoUrl": "/profilephoto/729x000000005Gw/F"
17 | },
18 | "isChatterGuest": false,
19 | "id": "005x0000000JbGVAA0",
20 | "type": "User",
21 | "url": "/services/data/v23.0/chatter/users/005x0000000JbGVAA0"
22 | },
23 | {
24 | "name": "Admin User",
25 | "title": null,
26 | "firstName": "Admin",
27 | "lastName": "User",
28 | "companyName": "Pivotal",
29 | "mySubscription": null,
30 | "photo": {
31 | "smallPhotoUrl": "/profilephoto/005/T",
32 | "largePhotoUrl": "/profilephoto/005/F"
33 | },
34 | "isChatterGuest": false,
35 | "id": "005x0000000JbFuAAK",
36 | "type": "User",
37 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
38 | }],
39 | "read": false,
40 | "latestMessage": {
41 | "id": "03Jx000000000FYEAY",
42 | "sender": {
43 | "name": "Admin User",
44 | "title": null,
45 | "firstName": "Admin",
46 | "lastName": "User",
47 | "companyName": "Pivotal",
48 | "mySubscription": null,
49 | "photo": {
50 | "smallPhotoUrl": "/profilephoto/005/T",
51 | "largePhotoUrl": "/profilephoto/005/F"
52 | },
53 | "isChatterGuest": false,
54 | "id": "005x0000000JbFuAAK",
55 | "type": "User",
56 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
57 | },
58 | "url": "/services/data/v23.0/chatter/users/me/messages/03Jx000000000FYEAY",
59 | "body": {
60 | "text": "Yes",
61 | "messageSegments": []
62 | },
63 | "recipients": [],
64 | "sentDate": "2011-08-08T21:36:05.000Z",
65 | "conversationId": "03Mx0000000009XEAQ",
66 | "conversationUrl": "/services/data/v23.0/chatter/users/me/conversations/03Mx0000000009XEAQ"
67 | }
68 | }]
69 | }
--------------------------------------------------------------------------------
/spec/lib/collection_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rspec'
2 | require 'spec_helper'
3 | require 'databasedotcom'
4 |
5 | describe Databasedotcom::Collection do
6 | before do
7 | @client_mock = double("client")
8 | @collection = Databasedotcom::Collection.new(@client_mock, 10, "http://next.page.url", "http://previous.page.url", "http://current.page.url")
9 | end
10 |
11 | it "is an Array" do
12 | @collection.should be_a_kind_of(Array)
13 | end
14 |
15 | it "has a total size" do
16 | @collection.total_size.should == 10
17 | end
18 |
19 | it "has a client" do
20 | @collection.client.should == @client_mock
21 | end
22 |
23 | it "has a current page url" do
24 | @collection.current_page_url.should == "http://current.page.url"
25 | end
26 |
27 | describe "#next_page?" do
28 | it "returns true if a next page url exists" do
29 | @collection.next_page?.should be_true
30 | end
31 |
32 | it "returns false if no next page url exists" do
33 | Databasedotcom::Collection.new(@client_mock, 10).next_page?.should be_false
34 | end
35 | end
36 |
37 | describe "#next_page" do
38 | context "when a next page url exists" do
39 | it "creates a new collection" do
40 | @client_mock.should_receive(:next_page).with(@collection.next_page_url).and_return("foo")
41 | new_collection = @collection.next_page
42 | new_collection.should == "foo"
43 | end
44 | end
45 |
46 | context "when no next page url exists" do
47 | it "returns an empty collection" do
48 | @collection.stub(:next_page_url).and_return(nil)
49 | @client_mock.should_not_receive(:next_page)
50 | new_collection = @collection.next_page
51 | new_collection.should be_an_instance_of(Databasedotcom::Collection)
52 | new_collection.length.should be_zero
53 | end
54 | end
55 | end
56 |
57 | describe "#previous_page?" do
58 | it "returns true if a previous page url exists" do
59 | @collection.previous_page?.should be_true
60 | end
61 |
62 | it "returns false if no previous page url exists" do
63 | Databasedotcom::Collection.new(@client_mock, 10).previous_page?.should be_false
64 | end
65 | end
66 |
67 | describe "#previous_page" do
68 | context "when a previous page url exists" do
69 | it "creates a new collection" do
70 | @client_mock.should_receive(:previous_page).with(@collection.previous_page_url).and_return("foo")
71 | new_collection = @collection.previous_page
72 | new_collection.should == "foo"
73 | end
74 | end
75 |
76 | context "when no previous page url exists" do
77 | it "returns an empty collection" do
78 | @collection.stub(:previous_page_url).and_return(nil)
79 | @client_mock.should_not_receive(:previous_page)
80 | new_collection = @collection.previous_page
81 | new_collection.should be_an_instance_of(Databasedotcom::Collection)
82 | new_collection.length.should be_zero
83 | end
84 | end
85 | end
86 | end
--------------------------------------------------------------------------------
/spec/lib/shared_behaviors/resource_with_photo.rb:
--------------------------------------------------------------------------------
1 | shared_examples_for("a resource with a photo") do
2 | describe ".photo" do
3 | before do
4 | @response = double("response", :body => File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/#{described_class.resource_name}_photo_get_id_success_response.json")))
5 | @client_mock = double("client", :version => "23")
6 | end
7 |
8 | it "returns a hash containing photo info" do
9 | @client_mock.should_receive(:http_get).with("/services/data/v23/chatter/#{described_class.resource_name}/foo/photo").and_return(@response)
10 | photo = described_class.photo(@client_mock, "foo")
11 | photo.should be_instance_of(Hash)
12 | end
13 | end
14 |
15 | describe ".upload_photo" do
16 | before do
17 | @response = double("response", :body => File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/#{described_class.resource_name}_photo_get_id_success_response.json")))
18 | @client_mock = double("client", :version => "23")
19 | end
20 |
21 | it "uploads the photo and returns a hash containing photo info" do
22 | @client_mock.should_receive(:http_multipart_post).with("/services/data/v23/chatter/#{described_class.resource_name}/foo/photo", an_instance_of(Hash)).and_return(@response)
23 | file_mock = double("file", :read => "foo", :path => "filename")
24 | photo = described_class.upload_photo(@client_mock, "foo", file_mock, "image/gif")
25 | photo.should be_instance_of(Hash)
26 | end
27 | end
28 |
29 | describe ".delete_photo" do
30 | before do
31 | @client_mock = double("client", :version => "23")
32 | end
33 |
34 | it "deletes the photo of the specified user" do
35 | @client_mock.should_receive(:http_delete).with("/services/data/v23/chatter/#{described_class.resource_name}/foo/photo").and_return(true)
36 | described_class.delete_photo(@client_mock, "foo")
37 | end
38 | end
39 |
40 | context "with an instantiated object" do
41 | before do
42 | @response = File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/#{described_class.resource_name}_get_id_success_response.json"))
43 | @client_mock = double("client", :version => "23")
44 | @resource = described_class.new(@client_mock, @response)
45 | end
46 |
47 | describe "#photo" do
48 | it "returns a hash with the resource's photo" do
49 | @resource.photo.should == @resource.raw_hash["photo"]
50 | end
51 | end
52 |
53 | describe "#upload_photo" do
54 | it "uploads a photo for the resource" do
55 | described_class.should_receive(:upload_photo).with(@client_mock, @resource.id, "io", "image/gif")
56 | @resource.upload_photo("io", "image/gif")
57 | end
58 | end
59 |
60 | describe "#delete_photo" do
61 | it "deletes the resource's current photo" do
62 | @client_mock.should_receive(:http_delete).with("/services/data/v23/chatter/#{described_class.resource_name}/#{@resource.id}/photo").and_return(true)
63 | @resource.delete_photo
64 | end
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/lib/databasedotcom/chatter/feed.rb:
--------------------------------------------------------------------------------
1 | require 'json'
2 |
3 | module Databasedotcom
4 | module Chatter
5 | # Parent class of all feeds and inherits from Collection. This class is not intended to be instantiated. Methods should be called on subclasses, which are all are dynamically defined (except for FilterFeed). Defined feeds are *NewsFeed*, *UserProfileFeed*, *RecordFeed*, *ToFeed*, *PeopleFeed*, *GroupsFeed*, *FilesFeed*, *CompanyFeed*, and *FilterFeed*.
6 | class Feed < Collection
7 |
8 | # Returns an enumerable Feed of FeedItem objects that make up the feed with the specified _id_. Should not be called as a class method on Feed, but as a method on subclasses.
9 | #
10 | # NewsFeed.find(@client) #=> [#, #, ...]
11 | # PeopleFeed.find(@client, "userid") #=> [#, #, ...]
12 | # FilterFeed.find(@client, "me", "000") #=> [#, #, ...]
13 | #
14 | # _id_prefix_ is only applicable for FilterFeed.
15 | def self.find(client, id="me", id_prefix=nil)
16 | path_components = %w(services data)
17 | path_components << "v#{client.version}"
18 | path_components.concat(%w(chatter feeds))
19 | path_components << feed_type
20 | path_components << id unless feed_type == "company"
21 | path_components << id_prefix
22 | path_components << "feed-items"
23 | path = "/" + path_components.compact.join('/')
24 | result = client.http_get(path)
25 | response = JSON.parse(result.body)
26 | collection = self.new(client, nil, response["nextPageUrl"], response["previousPageUrl"], response["currentPageUrl"])
27 | response["items"].each do |item|
28 | collection << FeedItem.new(client, item)
29 | end
30 | collection
31 | end
32 |
33 | # Posts a FeedItem to a Feed specified by _user_id_. Should not be called as a class method on Feed, but as a method on subclasses.
34 | #
35 | # UserProfileFeed.post(@client, "me", :text => "This is a status update about Salesforce.", :url => "http://www.salesforce.com")
36 | #
37 | # Returns the newly created FeedItem.
38 | def self.post(client, user_id, parameters)
39 | url = "/services/data/v#{client.version}/chatter/feeds/#{feed_type}/#{user_id}/feed-items"
40 | response = client.http_post(url, nil, parameters)
41 | Databasedotcom::Chatter::FeedItem.new(client, response.body)
42 | end
43 |
44 | # Posts a file to a Feed specified by _user_id_. Should not be called as a class method on Feed, but as a method on subclasses.
45 | #
46 | # UserProfileFeed.post_file(@client, "me", File.open("MyFile"), "text/plain", "MyFile", :desc => "This is an uploaded text file.")
47 | #
48 | # Returns the newly created FeedItem.
49 | def self.post_file(client, user_id, io, file_type, file_name, parameters={})
50 | url = "/services/data/v#{client.version}/chatter/feeds/#{feed_type}/#{user_id}/feed-items"
51 | response = client.http_multipart_post(url, {"feedItemFileUpload" => UploadIO.new(io, file_type, file_name), "fileName" => file_name}, parameters)
52 | Databasedotcom::Chatter::FeedItem.new(client, response.body)
53 | end
54 |
55 | private
56 |
57 | def self.feed_type
58 | self.name.match(/.+::(.+)Feed$/)[1].resourcerize
59 | end
60 | end
61 |
62 | FEED_TYPES = %w(News UserProfile Record To People Groups Files Company)
63 | end
64 | end
--------------------------------------------------------------------------------
/spec/fixtures/chatter/messages_get_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "messages": [{
3 | "id": "03Jx000000000FYEAY",
4 | "sender": {
5 | "name": "Admin User",
6 | "title": null,
7 | "firstName": "Admin",
8 | "lastName": "User",
9 | "companyName": "Pivotal",
10 | "mySubscription": null,
11 | "photo": {
12 | "smallPhotoUrl": "/profilephoto/005/T",
13 | "largePhotoUrl": "/profilephoto/005/F"
14 | },
15 | "isChatterGuest": false,
16 | "id": "005x0000000JbFuAAK",
17 | "type": "User",
18 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
19 | },
20 | "url": "/services/data/v23.0/chatter/users/me/messages/03Jx000000000FYEAY",
21 | "body": {
22 | "text": "Yes",
23 | "messageSegments": []
24 | },
25 | "recipients": [{
26 | "name": "Chatter Expert",
27 | "title": null,
28 | "firstName": null,
29 | "lastName": "Chatter Expert",
30 | "companyName": null,
31 | "mySubscription": null,
32 | "photo": {
33 | "smallPhotoUrl": "/profilephoto/729x000000005Gw/T",
34 | "largePhotoUrl": "/profilephoto/729x000000005Gw/F"
35 | },
36 | "isChatterGuest": false,
37 | "id": "005x0000000JbGVAA0",
38 | "type": "User",
39 | "url": "/services/data/v23.0/chatter/users/005x0000000JbGVAA0"
40 | }],
41 | "sentDate": "2011-08-08T21:36:05.000Z",
42 | "conversationId": "03Mx0000000009XEAQ",
43 | "conversationUrl": "/services/data/v23.0/chatter/users/me/conversations/03Mx0000000009XEAQ"
44 | },
45 | {
46 | "id": "03Jx000000000FTEAY",
47 | "sender": {
48 | "name": "Chatter Expert",
49 | "title": null,
50 | "firstName": null,
51 | "lastName": "Chatter Expert",
52 | "companyName": null,
53 | "mySubscription": null,
54 | "photo": {
55 | "smallPhotoUrl": "/profilephoto/729x000000005Gw/T",
56 | "largePhotoUrl": "/profilephoto/729x000000005Gw/F"
57 | },
58 | "isChatterGuest": false,
59 | "id": "005x0000000JbGVAA0",
60 | "type": "User",
61 | "url": "/services/data/v23.0/chatter/users/005x0000000JbGVAA0"
62 | },
63 | "url": "/services/data/v23.0/chatter/users/me/messages/03Jx000000000FTEAY",
64 | "body": {
65 | "text": "Did you know that you can now send private messages that don’t show up in your Chatter feed? You can keep a conversation private between you and the people you send a message to.\n\nMessages are a great way to:\n- Ask someone a question privately\n- Talk about confidential subjects like hiring or compensation\n- Work on a top-secret project with a few select people\n\nGo to My Messages and try sending a message now!\n\nP.S. This message was automatically generated. If you reply, Chatter Expert isn’t able to reply back.",
66 | "messageSegments": []
67 | },
68 | "recipients": [{
69 | "name": "Admin User",
70 | "title": null,
71 | "firstName": "Admin",
72 | "lastName": "User",
73 | "companyName": "Pivotal",
74 | "mySubscription": null,
75 | "photo": {
76 | "smallPhotoUrl": "/profilephoto/005/T",
77 | "largePhotoUrl": "/profilephoto/005/F"
78 | },
79 | "isChatterGuest": false,
80 | "id": "005x0000000JbFuAAK",
81 | "type": "User",
82 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
83 | }],
84 | "sentDate": "2011-08-08T21:20:18.000Z",
85 | "conversationId": "03Mx0000000009XEAQ",
86 | "conversationUrl": "/services/data/v23.0/chatter/users/me/conversations/03Mx0000000009XEAQ"
87 | }],
88 | "nextPageUrl": null,
89 | "currentPageUrl": "/services/data/v23.0/chatter/users/me/messages"
90 | }
--------------------------------------------------------------------------------
/spec/fixtures/chatter/users_batch_get_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "results": [{
3 | "result": {
4 | "address": {
5 | "state": "CA",
6 | "country": "US",
7 | "street": "1 Market St",
8 | "city": "San Francisco",
9 | "zip": "94105"
10 | },
11 | "email": "danny@pivotallabs.com",
12 | "currentStatus": {
13 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK/status",
14 | "body": null,
15 | "parentId": "005x0000000JbFuAAK"
16 | },
17 | "managerId": null,
18 | "followingCounts": {
19 | "total": 1,
20 | "records": 1,
21 | "people": 0
22 | },
23 | "groupCount": 1,
24 | "followersCount": 0,
25 | "managerName": null,
26 | "aboutMe": null,
27 | "isActive": true,
28 | "phoneNumbers": [],
29 | "chatterActivity": {
30 | "commentCount": 2,
31 | "commentReceivedCount": 0,
32 | "likeReceivedCount": 0,
33 | "postCount": 3
34 | },
35 | "name": "Admin User",
36 | "title": null,
37 | "firstName": "Admin",
38 | "lastName": "User",
39 | "companyName": "Pivotal",
40 | "mySubscription": null,
41 | "photo": {
42 | "smallPhotoUrl": "/profilephoto/005/T",
43 | "largePhotoUrl": "/profilephoto/005/F"
44 | },
45 | "isChatterGuest": false,
46 | "id": "005x0000000JbFuAAK",
47 | "type": "User",
48 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
49 | },
50 | "statusCode": 200
51 | },
52 | {
53 | "result": {
54 | "address": {
55 | "state": "CA",
56 | "country": "USA",
57 | "street": "Landmark @ One Market",
58 | "city": "San Francisco",
59 | "zip": "94105"
60 | },
61 | "email": "noreply@chatter.salesforce.com",
62 | "currentStatus": {
63 | "url": "/services/data/v23.0/chatter/users/005x0000000JbGVAA0/status",
64 | "body": null,
65 | "parentId": "005x0000000JbGVAA0"
66 | },
67 | "managerId": null,
68 | "followingCounts": {
69 | "total": 0,
70 | "records": 0,
71 | "people": 0
72 | },
73 | "groupCount": 0,
74 | "followersCount": 0,
75 | "managerName": null,
76 | "aboutMe": "I’m here to help you get started with Chatter. I’ll introduce you to features, create sample posts, and suggest tips and best practices.\n\nI'm an automated user so you don't need to worry about privacy! I can't see any of your posts or files.\n\nIf I'm too noisy, have your admin or moderator deactivate me and I'll stop posting.",
77 | "isActive": true,
78 | "phoneNumbers": [],
79 | "chatterActivity": {
80 | "commentCount": 0,
81 | "commentReceivedCount": 0,
82 | "likeReceivedCount": 0,
83 | "postCount": 0
84 | },
85 | "name": "Chatter Expert",
86 | "title": null,
87 | "firstName": null,
88 | "lastName": "Chatter Expert",
89 | "companyName": null,
90 | "mySubscription": null,
91 | "photo": {
92 | "smallPhotoUrl": "/profilephoto/729x000000005Gw/T",
93 | "largePhotoUrl": "/profilephoto/729x000000005Gw/F"
94 | },
95 | "isChatterGuest": false,
96 | "id": "005x0000000JbGVAA0",
97 | "type": "User",
98 | "url": "/services/data/v23.0/chatter/users/005x0000000JbGVAA0"
99 | },
100 | "statusCode": 200
101 | }]
102 | }
--------------------------------------------------------------------------------
/lib/databasedotcom/chatter/conversation.rb:
--------------------------------------------------------------------------------
1 | require 'databasedotcom/chatter/record'
2 |
3 | module Databasedotcom
4 | module Chatter
5 | # A thread of private messages. When calling +Conversation.find+ or +Conversation.all+, you must pass +:user_id => + in the _parameters_
6 | #
7 | # Conversation.all(@client, :user_id => "me")
8 | # Conversation.find(@client, "conversationId", :user_id => "f80ad89f9d98d89dfd89")
9 | class Conversation < Record
10 |
11 | # Creates a new Conversation and sets its +id+ and +url+ to values obtained from the server response.
12 | def initialize(client, response)
13 | super
14 | @id ||= @raw_hash["conversationId"]
15 | @url ||= @raw_hash["conversationUrl"]
16 | end
17 |
18 | # Find the Conversation identified by _cid_ and archive it. Returns the updated Conversation.
19 | #
20 | # Conversation.archive(@client, "fakeid")
21 | def self.archive(client, cid)
22 | url = "/services/data/v#{client.version}/chatter/users/me/conversations/#{cid}"
23 | response = client.http_patch(url, nil, :archived => "true")
24 | Conversation.new(client, response.body)
25 | end
26 |
27 | # Find the Conversation identified by _cid_ and unarchive it. Returns the updated Conversation.
28 | #
29 | # Conversation.unarchive(@client, "fakeid")
30 | def self.unarchive(client, cid)
31 | url = "/services/data/v#{client.version}/chatter/users/me/conversations/#{cid}"
32 | response = client.http_patch(url, nil, :archived => "false")
33 | Conversation.new(client, response.body)
34 | end
35 |
36 | # Find the Conversation identified by _cid_ and mark it as read. Returns the updated Conversation.
37 | #
38 | # Conversation.mark_read(@client, "fakeid")
39 | def self.mark_read(client, cid)
40 | url = "/services/data/v#{client.version}/chatter/users/me/conversations/#{cid}"
41 | response = client.http_patch(url, nil, :read => "true")
42 | Conversation.new(client, response.body)
43 | end
44 |
45 | # Find the Conversation identified by _cid_ and mark it as unread. Returns the updated Conversation.
46 | #
47 | # Conversation.mark_unread(@client, "fakeid")
48 | def self.mark_unread(client, cid)
49 | url = "/services/data/v#{client.version}/chatter/users/me/conversations/#{cid}"
50 | response = client.http_patch(url, nil, :read => "false")
51 | Conversation.new(client, response.body)
52 | end
53 |
54 | # Gets all messages for the Conversation specified by _cid_ and the User specified by _uid_. Returns a Collection of Message objects.
55 | def self.messages(client, uid, cid)
56 | conversation = self.find(client, cid, :user_id => uid)
57 | collection = Databasedotcom::Collection.new(client, nil, conversation.raw_hash["messages"]["nextPageUrl"], conversation.raw_hash["messages"]["previousPageUrl"], conversation.raw_hash["messages"]["currentPageUrl"])
58 | conversation.raw_hash["messages"]["messages"].each do |item|
59 | collection << Message.new(client, item)
60 | end
61 | collection
62 | end
63 |
64 | # Archive this Conversation.
65 | def archive
66 | self.class.archive(self.client, self.id)
67 | end
68 |
69 | # Unarchive this Conversation.
70 | def unarchive
71 | self.class.unarchive(self.client, self.id)
72 | end
73 |
74 | # Mark this Conversation as read.
75 | def mark_read
76 | self.class.mark_read(self.client, self.id)
77 | end
78 |
79 | # Mark this Conversation as unread.
80 | def mark_unread
81 | self.class.mark_unread(self.client, self.id)
82 | end
83 |
84 | # Return a Collection of messages from this Conversation.
85 | def messages
86 | collection = Databasedotcom::Collection.new(client, nil, self.raw_hash["messages"]["nextPageUrl"], self.raw_hash["messages"]["previousPageUrl"], self.raw_hash["messages"]["currentPageUrl"])
87 | self.raw_hash["messages"]["messages"].each do |item|
88 | collection << Message.new(client, item)
89 | end
90 | collection
91 | end
92 |
93 | protected
94 |
95 | def self.search_parameter_name
96 | :Q
97 | end
98 | end
99 | end
100 | end
--------------------------------------------------------------------------------
/spec/fixtures/chatter/conversations_get_id_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "messages": {
3 | "messages": [{
4 | "id": "03Jx000000000FYEAY",
5 | "sender": {
6 | "name": "Admin User",
7 | "title": null,
8 | "firstName": "Admin",
9 | "lastName": "User",
10 | "companyName": "Pivotal",
11 | "mySubscription": null,
12 | "photo": {
13 | "smallPhotoUrl": "/profilephoto/005/T",
14 | "largePhotoUrl": "/profilephoto/005/F"
15 | },
16 | "isChatterGuest": false,
17 | "id": "005x0000000JbFuAAK",
18 | "type": "User",
19 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
20 | },
21 | "url": "/services/data/v23.0/chatter/users/me/messages/03Jx000000000FYEAY",
22 | "body": {
23 | "text": "Yes",
24 | "messageSegments": []
25 | },
26 | "recipients": [],
27 | "sentDate": "2011-08-08T21:36:05.000Z",
28 | "conversationId": "03Mx0000000009XEAQ",
29 | "conversationUrl": "/services/data/v23.0/chatter/users/me/conversations/03Mx0000000009XEAQ"
30 | },
31 | {
32 | "id": "03Jx000000000FTEAY",
33 | "sender": {
34 | "name": "Chatter Expert",
35 | "title": null,
36 | "firstName": null,
37 | "lastName": "Chatter Expert",
38 | "companyName": null,
39 | "mySubscription": null,
40 | "photo": {
41 | "smallPhotoUrl": "/profilephoto/729x000000005Gw/T",
42 | "largePhotoUrl": "/profilephoto/729x000000005Gw/F"
43 | },
44 | "isChatterGuest": false,
45 | "id": "005x0000000JbGVAA0",
46 | "type": "User",
47 | "url": "/services/data/v23.0/chatter/users/005x0000000JbGVAA0"
48 | },
49 | "url": "/services/data/v23.0/chatter/users/me/messages/03Jx000000000FTEAY",
50 | "body": {
51 | "text": "Did you know that you can now send private messages that don’t show up in your Chatter feed? You can keep a conversation private between you and the people you send a message to.\n\nMessages are a great way to:\n- Ask someone a question privately\n- Talk about confidential subjects like hiring or compensation\n- Work on a top-secret project with a few select people\n\nGo to My Messages and try sending a message now!\n\nP.S. This message was automatically generated. If you reply, Chatter Expert isn’t able to reply back.",
52 | "messageSegments": []
53 | },
54 | "recipients": [],
55 | "sentDate": "2011-08-08T21:20:18.000Z",
56 | "conversationId": "03Mx0000000009XEAQ",
57 | "conversationUrl": "/services/data/v23.0/chatter/users/me/conversations/03Mx0000000009XEAQ"
58 | }],
59 | "nextPageUrl": null,
60 | "currentPageUrl": "/services/data/v23.0/chatter/users/me/conversations/03Mx0000000009XEAQ"
61 | },
62 | "members": [{
63 | "name": "Chatter Expert",
64 | "title": null,
65 | "firstName": null,
66 | "lastName": "Chatter Expert",
67 | "companyName": null,
68 | "mySubscription": null,
69 | "photo": {
70 | "smallPhotoUrl": "/profilephoto/729x000000005Gw/T",
71 | "largePhotoUrl": "/profilephoto/729x000000005Gw/F"
72 | },
73 | "isChatterGuest": false,
74 | "id": "005x0000000JbGVAA0",
75 | "type": "User",
76 | "url": "/services/data/v23.0/chatter/users/005x0000000JbGVAA0"
77 | },
78 | {
79 | "name": "Admin User",
80 | "title": null,
81 | "firstName": "Admin",
82 | "lastName": "User",
83 | "companyName": "Pivotal",
84 | "mySubscription": null,
85 | "photo": {
86 | "smallPhotoUrl": "/profilephoto/005/T",
87 | "largePhotoUrl": "/profilephoto/005/F"
88 | },
89 | "isChatterGuest": false,
90 | "id": "005x0000000JbFuAAK",
91 | "type": "User",
92 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
93 | }],
94 | "read": false,
95 | "conversationId": "03Mx0000000009XEAQ",
96 | "conversationUrl": "/services/data/v23.0/chatter/users/me/conversations/03Mx0000000009XEAQ"
97 | }
98 |
--------------------------------------------------------------------------------
/spec/lib/shared_behaviors/token_refreshing_request.rb:
--------------------------------------------------------------------------------
1 | shared_examples_for("a request that can refresh the oauth token") do |request_method, request_method_name, request_url, success_status_code|
2 | describe "when receiving a 401 response" do
3 | before do
4 | stub_request(request_method, request_url).to_return(:body => "", :status => 401).then.to_return(:body => "", :status => success_status_code)
5 | end
6 |
7 | context "with a refresh token" do
8 | before do
9 | @client.refresh_token = "refresh"
10 | end
11 |
12 | after do
13 | @client.refresh_token = nil
14 | end
15 |
16 | context "when the refresh token flow succeeds" do
17 | before do
18 | response_body = File.read(File.join(File.dirname(__FILE__), "../../fixtures/refresh_success_response.json"))
19 | stub_request(:post, "https://bro.baz/services/oauth2/token?client_id=client_id&client_secret=client_secret&grant_type=refresh_token&refresh_token=refresh").to_return(:body => response_body, :status => 200)
20 | end
21 |
22 | it "stores the new access token" do
23 | @client.send("http_#{request_method_name}", URI.parse(request_url).path, {})
24 | @client.oauth_token.should == "refreshed_access_token"
25 | end
26 |
27 | it "retries the request" do
28 | @client.send("http_#{request_method_name}", URI.parse(request_url).path, {})
29 | WebMock.should have_requested(request_method, request_url).twice
30 | end
31 | end
32 |
33 | context "when the refresh token flow fails" do
34 | before do
35 | response_body = File.read(File.join(File.dirname(__FILE__), "../../fixtures/refresh_error_response.json"))
36 | stub_request(:post, "https://bro.baz/services/oauth2/token?client_id=client_id&client_secret=client_secret&grant_type=refresh_token&refresh_token=refresh").to_return(:body => response_body, :status => 400)
37 | end
38 |
39 | it "raises SalesForceError" do
40 | lambda {
41 | @client.send("http_#{request_method_name}", URI.parse(request_url).path, {})
42 | }.should raise_error(Databasedotcom::SalesForceError)
43 | end
44 | end
45 | end
46 |
47 | context "with a username and password" do
48 | before do
49 | @client.username = "username"
50 | @client.password = "password"
51 | end
52 |
53 | after do
54 | @client.username = @client.password = nil
55 | end
56 |
57 | context "when reauthentication succeeds" do
58 | before do
59 | response_body = File.read(File.join(File.dirname(__FILE__), "../../fixtures/reauth_success_response.json"))
60 | stub_request(:post, "https://bro.baz/services/oauth2/token?client_id=client_id&client_secret=client_secret&grant_type=password&username=username&password=password").to_return(:body => response_body, :status => 200)
61 | end
62 |
63 | it "stores the new access token" do
64 | @client.send("http_#{request_method_name}", URI.parse(request_url).path, {})
65 | @client.oauth_token.should == "new_access_token"
66 | end
67 |
68 | it "retries the request" do
69 | @client.send("http_#{request_method_name}", URI.parse(request_url).path, {})
70 | WebMock.should have_requested(request_method, request_url).twice
71 | end
72 | end
73 |
74 | context "when reauthentication fails" do
75 | before do
76 | response_body = File.read(File.join(File.dirname(__FILE__), "../../fixtures/auth_error_response.json"))
77 | stub_request(:post, "https://bro.baz/services/oauth2/token?client_id=client_id&client_secret=client_secret&grant_type=password&username=username&password=password").to_return(:body => response_body, :status => 400)
78 | end
79 |
80 | it "raises SalesForceError" do
81 | lambda {
82 | @client.send("http_#{request_method_name}", URI.parse(request_url).path, {})
83 | }.should raise_error(Databasedotcom::SalesForceError)
84 | end
85 | end
86 | end
87 |
88 | context "without a refresh token or username/password" do
89 | it "raises SalesForceError" do
90 | lambda {
91 | @client.send("http_#{request_method_name}", URI.parse(request_url).path, {})
92 | }.should raise_error(Databasedotcom::SalesForceError)
93 | end
94 | end
95 | end
96 | end
97 |
--------------------------------------------------------------------------------
/lib/databasedotcom/chatter/record.rb:
--------------------------------------------------------------------------------
1 | require 'json'
2 |
3 | module Databasedotcom
4 | module Chatter
5 | # Superclasses all Chatter resources except feeds. Some methods may not be supported by the Force.com API for certain subclasses.
6 | class Record
7 | attr_reader :raw_hash, :name, :id, :url, :type, :client
8 |
9 | # Create a new record from the returned JSON response of an API request. Sets the client, name, id, url, and type attributes. Saves the raw response as +raw_hash+.
10 | def initialize(client, response)
11 | @client = client
12 | @raw_hash = response.is_a?(Hash) ? response : JSON.parse(response)
13 | @name = @raw_hash["name"]
14 | @id = @raw_hash["id"]
15 | @url = @raw_hash["url"]
16 | @type = @raw_hash["type"]
17 | end
18 |
19 | # Find a single Record or a Collection of records by id. _resource_id_ can be a single id or a list of ids.
20 | def self.find(client, resource_id, parameters={})
21 | if resource_id.is_a?(Array)
22 | resource_ids = resource_id.join(',')
23 | url = "/services/data/v#{client.version}/chatter/#{self.resource_name}/batch/#{resource_ids}"
24 | response = JSON.parse(client.http_get(url, parameters).body)
25 | good_results = response["results"].select { |r| r["statusCode"] == 200 }
26 | collection = Databasedotcom::Collection.new(client, good_results.length)
27 | good_results.each do |result|
28 | collection << self.new(client, result["result"])
29 | end
30 | collection
31 | else
32 | path_components = ["/services/data/v#{client.version}/chatter"]
33 | if parameters.has_key?(:user_id)
34 | path_components << "users/#{parameters[:user_id]}"
35 | parameters.delete(:user_id)
36 | end
37 | path_components << "#{self.resource_name}/#{resource_id}"
38 | url = path_components.join('/')
39 | response = JSON.parse(client.http_get(url, parameters).body)
40 | self.new(client, response)
41 | end
42 | end
43 |
44 | # Return a Collection of records that match the _query_.
45 | def self.search(client, query, parameters={})
46 | self.all(client, parameters.merge(self.search_parameter_name => query))
47 | end
48 |
49 | # Return a Collection of all records.
50 | def self.all(client, parameters={})
51 | path_components = ["/services/data/v#{client.version}/chatter"]
52 | if parameters.has_key?(:user_id)
53 | path_components << "users/#{parameters[:user_id]}"
54 | parameters.delete(:user_id)
55 | end
56 | path_components << self.resource_name
57 | url = path_components.join('/')
58 | result = client.http_get(url, parameters)
59 | response = JSON.parse(result.body)
60 | collection = Databasedotcom::Collection.new(client, self.total_size_of_collection(response), response["nextPageUrl"], response["previousPageUrl"], response["currentPageUrl"])
61 | self.collection_from_response(response).each do |resource|
62 | collection << self.new(client, resource)
63 | end
64 | collection
65 | end
66 |
67 | # Delete the Record identified by _resource_id_.
68 | def self.delete(client, resource_id, parameters={})
69 | path_components = ["/services/data/v#{client.version}/chatter"]
70 | if parameters.has_key?(:user_id)
71 | path_components << "users/#{parameters[:user_id]}"
72 | parameters.delete(:user_id)
73 | end
74 | path_components << self.resource_name
75 | path_components << resource_id
76 | path = path_components.join('/')
77 | client.http_delete(path, parameters)
78 | end
79 |
80 | # A Hash representation of the User that created this Record.
81 | def user
82 | self.raw_hash["user"]
83 | end
84 |
85 | # A Hash representation of the entity that is the parent of this Record.
86 | def parent
87 | self.raw_hash["parent"]
88 | end
89 |
90 | # Delete this record.
91 | def delete(parameters={})
92 | self.class.delete(self.client, self.id, parameters)
93 | end
94 |
95 | # Reload this record.
96 | def reload
97 | self.class.find(self.client, self.id)
98 | end
99 |
100 | # The REST resource name of this Record.
101 | #
102 | # GroupMembership.resource_name #=> group-memberships
103 | def self.resource_name
104 | (self.name.split('::').last).resourcerize + "s"
105 | end
106 |
107 | protected
108 |
109 | def self.total_size_of_collection(response)
110 | response["total"] || response["totalMemberCount"]
111 | end
112 |
113 | def self.collection_from_response(response)
114 | response[self.resource_name]
115 | end
116 |
117 | def self.search_parameter_name
118 | :q
119 | end
120 | end
121 | end
122 | end
--------------------------------------------------------------------------------
/spec/fixtures/chatter/feed-items_get_id_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "parent": {
3 | "name": "Admin User",
4 | "title": null,
5 | "firstName": "Admin",
6 | "lastName": "User",
7 | "companyName": "Pivotal",
8 | "photo": {
9 | "smallPhotoUrl": "/profilephoto/005/T",
10 | "largePhotoUrl": "/profilephoto/005/F"
11 | },
12 | "mySubscription": null,
13 | "isChatterGuest": false,
14 | "id": "005x0000000JbFuAAK",
15 | "type": "User",
16 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
17 | },
18 | "id": "0D5x00000001g6tCAA",
19 | "type": "UserStatus",
20 | "clientInfo": {
21 | "applicationName": "localhost",
22 | "applicationUrl": "null"
23 | },
24 | "url": "/services/data/v23.0/chatter/feed-items/0D5x00000001g6tCAA",
25 | "body": {
26 | "text": "I love #frogs",
27 | "messageSegments": [{
28 | "type": "Text",
29 | "text": "I love "
30 | },
31 | {
32 | "url": "/services/data/v23.0/chatter/feed-items?q=%23frogs",
33 | "tag": "frogs",
34 | "type": "Hashtag",
35 | "text": "#frogs"
36 | }]
37 | },
38 | "createdDate": "2011-08-08T21:37:08.000Z",
39 | "modifiedDate": "2011-08-08T21:39:23.000Z",
40 | "photoUrl": "/profilephoto/005/T",
41 | "comments": {
42 | "total": 1,
43 | "comments": [{
44 | "parent": {
45 | "id": "005x0000000JbFuAAK",
46 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
47 | },
48 | "id": "0D7x000000005dRCAQ",
49 | "user": {
50 | "name": "Admin User",
51 | "title": null,
52 | "firstName": "Admin",
53 | "lastName": "User",
54 | "companyName": "Pivotal",
55 | "photo": {
56 | "smallPhotoUrl": "/profilephoto/005/T",
57 | "largePhotoUrl": "/profilephoto/005/F"
58 | },
59 | "mySubscription": null,
60 | "isChatterGuest": false,
61 | "id": "005x0000000JbFuAAK",
62 | "type": "User",
63 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
64 | },
65 | "clientInfo": {
66 | "applicationName": "localhost",
67 | "applicationUrl": "null"
68 | },
69 | "url": "/services/data/v23.0/chatter/comments/0D7x000000005dRCAQ",
70 | "body": {
71 | "text": "So do I like #frogs",
72 | "messageSegments": [{
73 | "type": "Text",
74 | "text": "So do I like "
75 | },
76 | {
77 | "url": "/services/data/v23.0/chatter/feed-items?q=%23frogs",
78 | "tag": "frogs",
79 | "type": "Hashtag",
80 | "text": "#frogs"
81 | }]
82 | },
83 | "createdDate": "2011-08-08T21:39:23.000+0000",
84 | "deletable": true,
85 | "feedItem": {
86 | "id": "0D5x00000001g6tCAA",
87 | "url": "/services/data/v23.0/chatter/feed-items/0D5x00000001g6tCAA"
88 | }
89 | }],
90 | "nextPageUrl": null,
91 | "currentPageUrl": "/services/data/v23.0/chatter/feed-items/0D5x00000001g6tCAA/comments"
92 | },
93 | "likes": {
94 | "total": 1,
95 | "likes": [{
96 | "id": "0I0x000000001XiCAI",
97 | "user": {
98 | "name": "Admin User",
99 | "title": null,
100 | "firstName": "Admin",
101 | "lastName": "User",
102 | "companyName": "Pivotal",
103 | "photo": {
104 | "smallPhotoUrl": "/profilephoto/005/T",
105 | "largePhotoUrl": "/profilephoto/005/F"
106 | },
107 | "mySubscription": null,
108 | "isChatterGuest": false,
109 | "id": "005x0000000JbFuAAK",
110 | "type": "User",
111 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
112 | },
113 | "url": "/services/data/v23.0/chatter/likes/0I0x000000001XiCAI"
114 | }],
115 | "previousPageUrl": null,
116 | "nextPageUrl": null,
117 | "currentPageUrl": "/services/data/v23.0/chatter/feed-items/0D5x00000001g6tCAA/likes",
118 | "myLike": {
119 | "id": "0I0x000000001XiCAI",
120 | "url": "/services/data/v23.0/chatter/likes/0I0x000000001XiCAI"
121 | }
122 | },
123 | "isLikedByCurrentUser": true,
124 | "currentUserLike": {
125 | "id": "0I0x000000001XiCAI",
126 | "url": "/services/data/v23.0/chatter/likes/0I0x000000001XiCAI"
127 | },
128 | "actor": {
129 | "name": "Admin User",
130 | "title": null,
131 | "firstName": "Admin",
132 | "lastName": "User",
133 | "companyName": "Pivotal",
134 | "photo": {
135 | "smallPhotoUrl": "/profilephoto/005/T",
136 | "largePhotoUrl": "/profilephoto/005/F"
137 | },
138 | "mySubscription": null,
139 | "isChatterGuest": false,
140 | "id": "005x0000000JbFuAAK",
141 | "type": "User",
142 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
143 | },
144 | "event": false,
145 | "attachment": null
146 | }
--------------------------------------------------------------------------------
/spec/lib/chatter/conversation_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rspec'
2 | require 'spec_helper'
3 | require 'databasedotcom'
4 |
5 | describe Databasedotcom::Chatter::Conversation do
6 | it_should_behave_like("a restful resource")
7 |
8 | describe ".archive" do
9 | before do
10 | @client_mock = double("client", :version => 23)
11 | end
12 |
13 | it "archives the conversation" do
14 | response = double("response", :body => File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/conversations_get_id_success_response.json")))
15 | @client_mock.should_receive(:http_patch).with("/services/data/v23/chatter/users/me/conversations/cid", nil, :archived => "true").and_return(response)
16 | Databasedotcom::Chatter::Conversation.archive(@client_mock, "cid").should be_instance_of(Databasedotcom::Chatter::Conversation)
17 | end
18 | end
19 |
20 | describe ".unarchive" do
21 | before do
22 | @client_mock = double("client", :version => 23)
23 | end
24 |
25 | it "unarchives the conversation" do
26 | response = double("response", :body => File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/conversations_get_id_success_response.json")))
27 | @client_mock.should_receive(:http_patch).with("/services/data/v23/chatter/users/me/conversations/cid", nil, :archived => "false").and_return(response)
28 | Databasedotcom::Chatter::Conversation.unarchive(@client_mock, "cid").should be_instance_of(Databasedotcom::Chatter::Conversation)
29 | end
30 | end
31 |
32 | describe ".mark_read" do
33 | before do
34 | @client_mock = double("client", :version => 23)
35 | end
36 |
37 | it "marks the conversation as read" do
38 | response = double("response", :body => File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/conversations_get_id_success_response.json")))
39 | @client_mock.should_receive(:http_patch).with("/services/data/v23/chatter/users/me/conversations/cid", nil, :read => "true").and_return(response)
40 | Databasedotcom::Chatter::Conversation.mark_read(@client_mock, "cid").should be_instance_of(Databasedotcom::Chatter::Conversation)
41 | end
42 | end
43 |
44 | describe ".mark_unread" do
45 | before do
46 | @client_mock = double("client", :version => 23)
47 | end
48 |
49 | it "marks the conversation as unread" do
50 | response = double("response", :body => File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/conversations_get_id_success_response.json")))
51 | @client_mock.should_receive(:http_patch).with("/services/data/v23/chatter/users/me/conversations/cid", nil, :read => "false").and_return(response)
52 | Databasedotcom::Chatter::Conversation.mark_unread(@client_mock, "cid").should be_instance_of(Databasedotcom::Chatter::Conversation)
53 | end
54 | end
55 |
56 | describe ".messages" do
57 | before do
58 | @client_mock = double("client", :version => 23)
59 | end
60 |
61 | it "gets the messages for the conversation" do
62 | response = double("response", :body => File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/conversations_get_id_success_response.json")))
63 | @client_mock.should_receive(:http_get).with("/services/data/v23/chatter/users/uid/conversations/cid", an_instance_of(Hash)).and_return(response)
64 | messages = Databasedotcom::Chatter::Conversation.messages(@client_mock, "uid", "cid")
65 | messages.should be_instance_of(Databasedotcom::Collection)
66 | messages.each do |message|
67 | message.should be_instance_of(Databasedotcom::Chatter::Message)
68 | end
69 | end
70 | end
71 |
72 | context "with a Conversation object" do
73 | before do
74 | @client_mock = double("client", :version => 23)
75 | @conversation = Databasedotcom::Chatter::Conversation.new(@client_mock, File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/conversations_get_id_success_response.json")))
76 | end
77 |
78 | describe "#initialize" do
79 | it "initializes id and url" do
80 | @conversation.id.should == @conversation.raw_hash["conversationId"]
81 | @conversation.url.should == @conversation.raw_hash["conversationUrl"]
82 | end
83 | end
84 |
85 | describe "#archive" do
86 | it "archives the conversation" do
87 | Databasedotcom::Chatter::Conversation.should_receive(:archive).with(@client_mock, @conversation.id)
88 | @conversation.archive
89 | end
90 | end
91 |
92 | describe "#unarchive" do
93 | it "unarchives the conversation" do
94 | Databasedotcom::Chatter::Conversation.should_receive(:unarchive).with(@client_mock, @conversation.id)
95 | @conversation.unarchive
96 | end
97 | end
98 |
99 | describe "#mark_read" do
100 | it "marks the conversation as read" do
101 | Databasedotcom::Chatter::Conversation.should_receive(:mark_read).with(@client_mock, @conversation.id)
102 | @conversation.mark_read
103 | end
104 | end
105 |
106 | describe "#mark_unread" do
107 | it "marks the conversation unread" do
108 | Databasedotcom::Chatter::Conversation.should_receive(:mark_unread).with(@client_mock, @conversation.id)
109 | @conversation.mark_unread
110 | end
111 | end
112 |
113 | describe "#messages" do
114 | it "gets the messages for the conversation" do
115 | messages = @conversation.messages
116 | messages.should be_an_instance_of(Databasedotcom::Collection)
117 | messages.each do |message|
118 | message.should be_an_instance_of(Databasedotcom::Chatter::Message)
119 | end
120 | end
121 | end
122 | end
123 | end
--------------------------------------------------------------------------------
/spec/lib/shared_behaviors/restful_resource.rb:
--------------------------------------------------------------------------------
1 | shared_examples_for("a restful resource") do
2 | describe "#initialize" do
3 | describe "from a JSON response" do
4 | before do
5 | @response = File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/#{described_class.resource_name}_get_id_success_response.json"))
6 | @client_mock = double("client")
7 | @record = described_class.new(@client_mock, @response)
8 | end
9 |
10 | it "is a Record" do
11 | @record.should be_a_kind_of(Databasedotcom::Chatter::Record)
12 | end
13 | end
14 | end
15 |
16 | describe ".find" do
17 | context "with a single id" do
18 | before do
19 | body = File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/#{described_class.resource_name}_get_id_success_response.json"))
20 | @client_mock = double("client", :version => "23")
21 | @response = double("response")
22 | @response.should_receive(:body).any_number_of_times.and_return(body)
23 | end
24 |
25 | it "gets a resource by id" do
26 | @client_mock.should_receive(:http_get).with("/services/data/v23/chatter/#{described_class.resource_name}/rid", {}).and_return(@response)
27 | resource = described_class.find(@client_mock, "rid")
28 | resource.should be_instance_of(described_class)
29 | end
30 |
31 | it "passes through parameters" do
32 | @client_mock.should_receive(:http_get).with("/services/data/v23/chatter/#{described_class.resource_name}/rid", {:page => "foo", :pageSize => 10}).and_return(@response)
33 | described_class.find(@client_mock, "rid", :page => "foo", :pageSize => 10)
34 | end
35 |
36 | context "with a parent user id" do
37 | it "constructs the path appropriately" do
38 | @client_mock.should_receive(:http_get).with("/services/data/v23/chatter/users/me/#{described_class.resource_name}/rid", {:page => "foo", :pageSize => 10}).and_return(@response)
39 | described_class.find(@client_mock, "rid", :page => "foo", :pageSize => 10, :user_id => "me")
40 | end
41 | end
42 | end
43 |
44 | if File.exists?(File.join(File.dirname(__FILE__), "../../fixtures/chatter/#{described_class.resource_name}_batch_get_success_response.json"))
45 | context "with an array of ids" do
46 | context "when all the ids are valid" do
47 | before do
48 | body = File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/#{described_class.resource_name}_batch_get_success_response.json"))
49 | @client_mock = double("client", :version => "23")
50 | @response = double("response")
51 | @response.should_receive(:body).any_number_of_times.and_return(body)
52 | end
53 |
54 | it "gets all the resources" do
55 | @client_mock.should_receive(:http_get).with("/services/data/v23/chatter/#{described_class.resource_name}/batch/fid,gid", {}).and_return(@response)
56 | resources = described_class.find(@client_mock, %w(fid gid))
57 | resources.should be_instance_of(Databasedotcom::Collection)
58 | resources.each do |resource|
59 | resource.should be_instance_of(described_class)
60 | end
61 | end
62 | end
63 |
64 | context "when some of the ids are not valid" do
65 | before do
66 | body = File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/#{described_class.resource_name}_batch_get_mixed_response.json"))
67 | @client_mock = double("client", :version => "23")
68 | @response = double("response")
69 | @response.should_receive(:body).any_number_of_times.and_return(body)
70 | end
71 |
72 | it "gets all the users" do
73 | @client_mock.should_receive(:http_get).with("/services/data/v23/chatter/#{described_class.resource_name}/batch/fid,gid", {}).and_return(@response)
74 | resources = described_class.find(@client_mock, %w(fid gid))
75 | resources.should be_instance_of(Databasedotcom::Collection)
76 | resources.length.should == 1
77 | end
78 | end
79 | end
80 | end
81 | end
82 |
83 | if File.exists?(File.join(File.dirname(__FILE__), "../../fixtures/chatter/#{described_class.resource_name}_get_success_response.json"))
84 | describe ".all" do
85 | before do
86 | body = File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/#{described_class.resource_name}_get_success_response.json"))
87 | @client_mock = double("client", :version => "23")
88 | @response = double("response")
89 | @response.should_receive(:body).any_number_of_times.and_return(body)
90 | end
91 |
92 | it "gets a collection of resources" do
93 | @client_mock.should_receive(:http_get).with("/services/data/v23/chatter/#{described_class.resource_name}", {}).and_return(@response)
94 | resources = described_class.all(@client_mock)
95 | resources.should be_instance_of(Databasedotcom::Collection)
96 | resources.each do |resource|
97 | resource.should be_instance_of(described_class)
98 | end
99 | end
100 |
101 | it "passes through parameters" do
102 | @client_mock.should_receive(:http_get).with("/services/data/v23/chatter/#{described_class.resource_name}", {:page => "foo", :pageSize => 10}).and_return(@response)
103 | described_class.all(@client_mock, :page => "foo", :pageSize => 10)
104 | end
105 |
106 | context "with a parent user id" do
107 | it "constructs the path appropriately" do
108 | @client_mock.should_receive(:http_get).with("/services/data/v23/chatter/users/me/#{described_class.resource_name}", {:page => "foo", :pageSize => 10}).and_return(@response)
109 | described_class.all(@client_mock, :page => "foo", :pageSize => 10, :user_id => "me")
110 | end
111 | end
112 | end
113 | end
114 |
115 | describe ".search" do
116 | it "calls .find with the appropriate parameters" do
117 | described_class.should_receive(:all).with("client", {:foo => "bar", described_class.search_parameter_name => "query"})
118 | described_class.search("client", "query", {:foo => "bar"})
119 | end
120 | end
121 |
122 | describe ".delete" do
123 | before do
124 | @client_mock = double("client", :version => "23")
125 | end
126 |
127 | it "deletes the resource specified by id" do
128 | @client_mock.should_receive(:http_delete).with("/services/data/v23/chatter/#{described_class.resource_name}/rid", {}).and_return(true)
129 | described_class.delete(@client_mock, "rid")
130 | end
131 |
132 | context "with a parent user id" do
133 | it "constructs the path appropriately" do
134 | @client_mock.should_receive(:http_delete).with("/services/data/v23/chatter/users/me/#{described_class.resource_name}/rid", {}).and_return(@response)
135 | described_class.delete(@client_mock, "rid", :user_id => "me")
136 | end
137 | end
138 | end
139 | end
--------------------------------------------------------------------------------
/lib/databasedotcom/chatter/user.rb:
--------------------------------------------------------------------------------
1 | require 'databasedotcom/chatter/record'
2 | require 'databasedotcom/chatter/photo_methods'
3 |
4 | module Databasedotcom
5 | module Chatter
6 | # Defines a User in your org.
7 | class User < Record
8 | include PhotoMethods
9 |
10 | # Returns a Collection of Subscription objects that represents all followers of the User identified by _subject_id_.
11 | def self.followers(client, subject_id="me")
12 | url = "/services/data/v#{client.version}/chatter/users/#{subject_id}/followers"
13 | result = client.http_get(url)
14 | response = JSON.parse(result.body)
15 | collection = Databasedotcom::Collection.new(client, response["total"], response["nextPageUrl"], response["previousPageUrl"], response["currentPageUrl"])
16 | response["followers"].each do |subscription|
17 | collection << Subscription.new(client, subscription)
18 | end
19 | collection
20 | end
21 |
22 | # Returns a Collection of Subscription objects that represent all entities that the User identified by _subject_id_ is following.
23 | def self.following(client, subject_id="me")
24 | url = "/services/data/v#{client.version}/chatter/users/#{subject_id}/following"
25 | result = client.http_get(url)
26 | response = JSON.parse(result.body)
27 | collection = Databasedotcom::Collection.new(client, response["total"], response["nextPageUrl"], response["previousPageUrl"], response["currentPageUrl"])
28 | response["following"].each do |subscription|
29 | collection << Subscription.new(client, subscription)
30 | end
31 | collection
32 | end
33 |
34 | # Returns a Collection of Group objects that represent all the groups that the User identified by _subject_id_ is a part of.
35 | def self.groups(client, subject_id="me")
36 | url = "/services/data/v#{client.version}/chatter/users/#{subject_id}/groups"
37 | result = client.http_get(url)
38 | response = JSON.parse(result.body)
39 | collection = Databasedotcom::Collection.new(client, response["total"], response["nextPageUrl"], response["previousPageUrl"], response["currentPageUrl"])
40 | response["groups"].each do |group|
41 | collection << Group.new(client, group)
42 | end
43 | collection
44 | end
45 |
46 | # Returns the current status of the User identified by _subject_id_.
47 | def self.status(client, subject_id="me")
48 | url = "/services/data/v#{client.version}/chatter/users/#{subject_id}/status"
49 | result = client.http_get(url)
50 | JSON.parse(result.body)
51 | end
52 |
53 | # Posts a status update as the User identified by _subject_id_ with content _text_.
54 | def self.post_status(client, subject_id, text)
55 | url = "/services/data/v#{client.version}/chatter/users/#{subject_id}/status"
56 | result = client.http_post(url, nil, :text => text)
57 | JSON.parse(result.body)
58 | end
59 |
60 | # Deletes the status of User identified by _subject_id_.
61 | def self.delete_status(client, subject_id="me")
62 | client.http_delete "/services/data/v#{client.version}/chatter/users/#{subject_id}/status"
63 | end
64 |
65 | # Creates and returns a new Subscription object that represents the User identified by _subject_id_ following the resource identified by _resource_id_.
66 | def self.follow(client, subject_id, resource_id)
67 | response = client.http_post("/services/data/v#{client.version}/chatter/users/#{subject_id}/following", nil, :subjectId => resource_id)
68 | Subscription.new(client, response.body)
69 | end
70 |
71 | # Returns a Collection of conversations that belong to the User identified by _subject_id_.
72 | def self.conversations(client, subject_id)
73 | Conversation.all(client, :user_id => subject_id)
74 | end
75 |
76 | # Returns a Collection of private messages that belong to the User identified by _subject_id_.
77 | def self.messages(client, subject_id)
78 | Message.all(client, :user_id => subject_id)
79 | end
80 |
81 | # Get a Collection of Subscription objects for this User. Always makes a call to the server.
82 | def followers!
83 | self.class.followers(self.client, self.id)
84 | end
85 |
86 | # Get a Collection of Subscription objects for this User. Returns cached data if it has been called before.
87 | def followers
88 | @followers ||= followers!
89 | end
90 |
91 | # Get a Collection of Subscription objects that represents all resources that this User is following. Always makes a call to the server.
92 | def following!
93 | self.class.following(self.client, self.id)
94 | end
95 |
96 | # Get a Collection of Subscription objects that represents all resources that this User is following. Returns cached data if it has been called before.
97 | def following
98 | @following ||= following!
99 | end
100 |
101 | # Returns this current status of this User.
102 | def status
103 | self.raw_hash["currentStatus"]
104 | end
105 |
106 | # Posts a new status with content _text_ for this User.
107 | def post_status(text)
108 | self.class.post_status(self.client, self.id, text)
109 | end
110 |
111 | # Deletes the current status of this User. Returns the deleted status.
112 | def delete_status
113 | self.class.delete_status(self.client, self.id)
114 | status
115 | end
116 |
117 | # Get a Collection of Group objects that represents all groups that this User is in. Always makes a call to the server.
118 | def groups!
119 | self.class.groups(self.client, self.id)
120 | end
121 |
122 | # Get a Collection of Group objects that represents all groups that this User is in. Returns cached data if it has been called before.
123 | def groups
124 | @groups ||= groups!
125 | end
126 |
127 | # Creates a new Subscription that represents this User following the resource with id _record_id_.
128 | def follow(record_id)
129 | self.class.follow(self.client, self.id, record_id)
130 | end
131 |
132 | # Get a Collection of Conversation objects that represents the conversations for this User. Always makes a call to the server.
133 | def conversations!
134 | self.class.conversations(self.client, self.id)
135 | end
136 |
137 | # Get a Collection of Conversation objects that represents the conversations for this User. Returns cached data if it has been called before.
138 | def conversations
139 | @conversations ||= conversations!
140 | end
141 |
142 | # Get a Collection of Message objects that represents the messages for this User. Always makes a call to the server.
143 | def messages!
144 | self.class.messages(self.client, self.id)
145 | end
146 |
147 | # Get a Collection of Message objects that represents the messages for this User. Returns cached data if it has been called before.
148 | def messages
149 | @messages ||= messages!
150 | end
151 | end
152 | end
153 | end
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # databasedotcom
2 |
3 | [](https://travis-ci.org/heroku/databasedotcom) [](https://codeclimate.com/github/heroku/databasedotcom) [](https://gemnasium.com/heroku/databasedotcom)
4 |
5 | databasedotcom is a gem to enable ruby applications to access the SalesForce REST API.
6 | If you use bundler, simply list it in your Gemfile, like so:
7 |
8 | ```
9 | gem 'databasedotcom'
10 | ```
11 |
12 | If you don't use bundler, install it by hand:
13 |
14 | ```
15 | gem install databasedotcom
16 | ```
17 |
18 | ## Documentation
19 |
20 | Reference documentation is available at [rubydoc.info](http://rubydoc.info/github/heroku/databasedotcom/master/frames)
21 |
22 | ## Source
23 |
24 | Source is available at [github](http://github.com/heroku/databasedotcom)
25 |
26 | ## Contributions
27 |
28 | To contribute, fork this repo, make changes in your fork, then send a pull request.
29 | No pull requests without accompanying tests will be accepted. To run tests in your
30 | fork, just do
31 |
32 | ```
33 | bundle install
34 | rake
35 | ```
36 |
37 | # Usage
38 | ## Initialization
39 | When you create a Databasedotcom::Client object, you need to configure it with a client
40 | id and client secret that corresponds to one of the Remote Access Applications configured
41 | within your Salesforce instance. The Salesforce UI refers to the client id as "Consumer Key",
42 | and to the client secret as "Consumer Secret".
43 |
44 | You can configure your Client object with a client id and client secret in one of several
45 | different ways:
46 |
47 | ### Configuration from the environment
48 |
49 | If configuration information is present in the environment, the new Client will take configuration
50 | information from there.
51 |
52 | ```bash
53 | export DATABASEDOTCOM_CLIENT_ID=foo
54 | export DATABASEDOTCOM_CLIENT_SECRET=bar
55 | ```
56 |
57 | Then
58 |
59 | ```ruby
60 | client = Databasedotcom::Client.new
61 | client.client_id #=> foo
62 | client.client_secret #=> bar
63 | ```
64 |
65 | ### Configuration from a YAML file
66 |
67 | If you pass the name of a YAML file when you create a Client, the new Client will read the YAML
68 | file and take the client id and client secret values from there.
69 |
70 | ```yaml
71 | # databasedotcom.yml
72 | #
73 | ---
74 | client_secret: bro
75 | client_id: baz
76 | ```
77 |
78 | Then
79 |
80 | ```ruby
81 | client = Databasedotcom::Client.new("databasedotcom.yml")
82 | client.client_id #=> bro
83 | client.client_secret #=> baz
84 | ```
85 |
86 | ### Configuration from a Hash
87 |
88 | If you pass a hash when you create a Client, the new Client will take configuration information
89 | from that Hash.
90 |
91 | ```ruby
92 | client = Databasedotcom::Client.new :client_id => "sponge", :client_secret => "bob"
93 | client.client_id #=> sponge
94 | client.client_secret #=> bob
95 | ```
96 |
97 | ### Configuration precedence
98 |
99 | Configuration information present in the environment always takes precedence over that passed in
100 | via a YAML file or a Hash.
101 |
102 | ```bash
103 | export DATABASEDOTCOM_CLIENT_ID=foo
104 | export DATABASEDOTCOM_CLIENT_SECRET=bar
105 | ```
106 |
107 | Then
108 |
109 | ```ruby
110 | client = Databasedotcom::Client.new :client_id => "sponge", :client_secret => "bob"
111 | client.client_id #=> foo
112 | client.client_secret #=> bar
113 | ```
114 |
115 | ### Usage in an application deployed on Heroku
116 |
117 | You can use the `heroku config:add` command to set environment variables:
118 |
119 | ```bash
120 | heroku config:add DATABASEDOTCOM_CLIENT_ID=foo
121 | heroku config:add DATABASEDOTCOM_CLIENT_SECRET=bar
122 | ```
123 |
124 | Then, when you create your client like:
125 |
126 | ```ruby
127 | client = Databasedotcom::Client.new
128 | ```
129 |
130 | it will use the configuration information that you set with `heroku config:add`.
131 |
132 | ### Connect to a SalesForce sandbox account
133 |
134 | Specify the `:host` option when creating your Client, e.g,
135 |
136 | ```ruby
137 | Databasedotcom::Client.new :host => "test.salesforce.com", ...
138 | ```
139 |
140 | ## Authentication
141 |
142 | The first thing you need to do with the new Client is to authenticate with Salesforce.
143 | You can do this in one of several ways:
144 |
145 | ### Authentication via an externally-acquired OAuth access token
146 |
147 | If you have acquired an OAuth access token for your Salesforce instance through some external
148 | means, you can use it. Note that you have to pass both the token and your Salesforce instance
149 | URL to the `authenticate` method:
150 |
151 | ```ruby
152 | client.authenticate :token => "my-oauth-token", :instance_url => "http://na1.salesforce.com" #=> "my-oauth-token"
153 | ```
154 |
155 | ### Authentication via Omniauth
156 |
157 | If you are using the gem within the context of a web application, and your web app is using Omniauth
158 | to do OAuth with Salesforce, you can authentication the Client direction via the Hash that Omniauth
159 | passes to your OAuth callback method, like so:
160 |
161 | ```ruby
162 | client.authenticate request.env['omniauth.auth'] #=> "the-oauth-token"
163 | ```
164 |
165 | ### Authentication via username and password
166 |
167 | You can authenticate your Client directly with Salesforce with a valid username and password for
168 | a user in your Salesforce instance. Note that, if access to your Salesforce instance requires a
169 | [security token](http://www.salesforce.com/us/developer/docs/api/Content/sforce_api_concepts_security.htm),
170 | the value that you pass for :password must be the password for the user concatenated with
171 | her security token.
172 |
173 | ```ruby
174 | client.authenticate :username => "foo@bar.com", :password => "ThePasswordTheSecurityToken" #=> "the-oauth-token"
175 | ```
176 |
177 | ## Accessing the Sobject API
178 |
179 | You can retrieve a list of Sobject defined in your Salesforce instance like so:
180 |
181 | ```ruby
182 | client.list_sobjects #=> ['User', 'Group', 'Contact']
183 | ```
184 |
185 | Once you have the name of an Sobject, the easiest way to interact with it is to first materialize it:
186 |
187 | ```ruby
188 | contact_class = client.materialize("Contact") #=> Contact
189 | ```
190 |
191 | By default, Sobject classes are materialized into the global namespace- if you want materialize into
192 | another module, you can easily do configure this:
193 |
194 | ```ruby
195 | client.sobject_module = My::Module
196 | client.materialize("Contact") #=> My::Module::Contact
197 | ```
198 |
199 | Materialized Sobject classes behave much like ActiveRecord classes:
200 |
201 | ```ruby
202 | contact = Contact.find("contact_id") #=> #
203 | contact = Contact.find_by_Name("John Smith") #=> dynamic finders!
204 | contacts = Contact.all #=> a Databasedotcom::Collection of Contact instances
205 | contacts = Contact.find_all_by_Company("IBM") #=> a Databasedotcom::Collection of matching Contacts
206 | contact.Name #=> the contact's Name attribute
207 | contact["Name"] #=> same thing
208 | contact.Name = "new name" #=> change the contact's Name attribute, in memory
209 | contact["Name"] = "new name" #=> same thing
210 | contact.save #=> save the changes to the database
211 | contact.update_attributes "Name" => "newer name",
212 | "Phone" => "4156543210" #=> change several attributes at once and save them
213 | contact.delete #=> delete the contact from the database
214 | ```
215 |
216 | See the [documentation](http://rubydoc.info/github/heroku/databasedotcom/master/frames) for full details.
217 |
218 | ## Accessing the Chatter API
219 |
220 | You can easily access Chatter feeds, group, conversations, etc.:
221 |
222 | ```ruby
223 | my_feed_items = Databasedotcom::Chatter::UserProfileFeed.find(client) #=> a Databasedotcom::Collection of FeedItems
224 |
225 | my_feed_items.each do |feed_item|
226 | feed_item.likes #=> a Databasedotcom::Collection of Like instances
227 | feed_item.comments #=> a Databasedotcom::Collection of Comment instances
228 | feed_item.raw_hash #=> the hash returned from the Chatter API describing this FeedItem
229 | feed_item.comment("This is cool") #=> create a new comment on the FeedItem
230 | feed_item.like #=> the authenticating user likes the FeedItem
231 | end
232 |
233 | me = Databasedotcom::Chatter::User.find(client, "me") #=> a User for the authenticating user
234 | me.followers #=> a Databasedotcom::Collection of Users
235 | me.post_status("what I'm doing now") #=> post a new status
236 |
237 | you = Databasedotcom::Chatter::User.find(client, "your-user-id")
238 | me.follow(you) #=> start following a user
239 | ```
240 |
241 | See the [documentation](http://rubydoc.info/github/heroku/databasedotcom/master/frames) for full details.
242 |
243 | # License
244 |
245 | This gem is licensed under the MIT License.
246 |
--------------------------------------------------------------------------------
/spec/lib/chatter/user_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rspec'
2 | require 'spec_helper'
3 | require 'databasedotcom'
4 |
5 | describe Databasedotcom::Chatter::User do
6 | it_should_behave_like("a restful resource")
7 | it_should_behave_like("a resource with a photo")
8 |
9 | describe ".followers" do
10 | before do
11 | @response = double("response", :body => File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/users_followers_get_id_success_response.json")))
12 | @client_mock = double("client", :version => "23")
13 | end
14 |
15 | it "returns a collection of followers subscriptions for a user" do
16 | @client_mock.should_receive(:http_get).with("/services/data/v23/chatter/users/foo/followers").and_return(@response)
17 | followers = Databasedotcom::Chatter::User.followers(@client_mock, "foo")
18 | followers.should be_instance_of(Databasedotcom::Collection)
19 | followers.each do |follower|
20 | follower.should be_instance_of(Databasedotcom::Chatter::Subscription)
21 | end
22 | end
23 |
24 | it "defaults user id to 'me'" do
25 | @client_mock.should_receive(:http_get).with("/services/data/v23/chatter/users/me/followers").and_return(@response)
26 | Databasedotcom::Chatter::User.followers(@client_mock)
27 | end
28 | end
29 |
30 | describe ".following" do
31 | before do
32 | @response = double("response", :body => File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/users_following_get_id_success_response.json")))
33 | @client_mock = double("client", :version => "23")
34 | end
35 |
36 | it "returns a collection of following subscriptions for a user" do
37 | @client_mock.should_receive(:http_get).with("/services/data/v23/chatter/users/foo/following").and_return(@response)
38 | followings = Databasedotcom::Chatter::User.following(@client_mock, "foo")
39 | followings.should be_instance_of(Databasedotcom::Collection)
40 | followings.each do |following|
41 | following.should be_instance_of(Databasedotcom::Chatter::Subscription)
42 | end
43 | end
44 |
45 | it "defaults user id to 'me'" do
46 | @client_mock.should_receive(:http_get).with("/services/data/v23/chatter/users/me/following").and_return(@response)
47 | Databasedotcom::Chatter::User.following(@client_mock)
48 | end
49 | end
50 |
51 | describe ".follow" do
52 | before do
53 | @response = double("response", :body => File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/subscriptions_get_id_success_response.json")))
54 | @client_mock = double("client", :version => "23")
55 | end
56 |
57 | it "follows the record specified by id and returns a new subscription" do
58 | @client_mock.should_receive(:http_post).with("/services/data/v23/chatter/users/me/following", nil, :subjectId => "rid").and_return(@response)
59 | Databasedotcom::Chatter::User.follow(@client_mock, "me", "rid").should be_instance_of(Databasedotcom::Chatter::Subscription)
60 | end
61 | end
62 |
63 | describe ".groups" do
64 | before do
65 | @response = double("response", :body => File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/users_groups_get_id_success_response.json")))
66 | @client_mock = double("client", :version => "23")
67 | end
68 |
69 | it "returns a collection of following subscriptions for a user" do
70 | @client_mock.should_receive(:http_get).with("/services/data/v23/chatter/users/foo/groups").and_return(@response)
71 | groups = Databasedotcom::Chatter::User.groups(@client_mock, "foo")
72 | groups.should be_instance_of(Databasedotcom::Collection)
73 | groups.each do |group|
74 | group.should be_instance_of(Databasedotcom::Chatter::Group)
75 | end
76 | end
77 |
78 | it "defaults user id to 'me'" do
79 | @client_mock.should_receive(:http_get).with("/services/data/v23/chatter/users/me/groups").and_return(@response)
80 | Databasedotcom::Chatter::User.groups(@client_mock)
81 | end
82 | end
83 |
84 | describe ".status" do
85 | before do
86 | @response = double("response", :body => File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/users_status_get_id_success_response.json")))
87 | @client_mock = double("client", :version => "23")
88 | end
89 |
90 | it "returns a hash containing status info" do
91 | @client_mock.should_receive(:http_get).with("/services/data/v23/chatter/users/foo/status").and_return(@response)
92 | groups = Databasedotcom::Chatter::User.status(@client_mock, "foo")
93 | groups.should be_instance_of(Hash)
94 | end
95 |
96 | it "defaults user id to 'me'" do
97 | @client_mock.should_receive(:http_get).with("/services/data/v23/chatter/users/me/status").and_return(@response)
98 | Databasedotcom::Chatter::User.status(@client_mock)
99 | end
100 | end
101 |
102 | describe ".delete_status" do
103 | before do
104 | @client_mock = double("client", :version => "23")
105 | end
106 |
107 | it "deletes the status of the specified user" do
108 | @client_mock.should_receive(:http_delete).with("/services/data/v23/chatter/users/foo/status").and_return(true)
109 | Databasedotcom::Chatter::User.delete_status(@client_mock, "foo")
110 | end
111 |
112 | it "defaults the user id to 'me'" do
113 | @client_mock.should_receive(:http_delete).with("/services/data/v23/chatter/users/me/status").and_return(true)
114 | Databasedotcom::Chatter::User.delete_status(@client_mock)
115 | end
116 | end
117 |
118 | describe ".post_status" do
119 | before do
120 | @client_mock = double("client", :version => "23")
121 | end
122 |
123 | it "post a status update for the specified user" do
124 | @client_mock.should_receive(:http_post).with("/services/data/v23/chatter/users/foo/status", nil, :text => "new status").and_return(double("response", :body => "{}"))
125 | Databasedotcom::Chatter::User.post_status(@client_mock, "foo", "new status").should be_instance_of(Hash)
126 | end
127 | end
128 |
129 | describe ".conversations" do
130 | before do
131 | @client_mock = double("client", :version => "23")
132 | end
133 |
134 | it "gets the conversations for the specified user" do
135 | Databasedotcom::Chatter::Conversation.should_receive(:all).with(@client_mock, :user_id => "foo").and_return(double("response", :body => "{}"))
136 | Databasedotcom::Chatter::User.conversations(@client_mock, "foo")
137 | end
138 | end
139 |
140 | describe ".messages" do
141 | before do
142 | @client_mock = double("client", :version => "23")
143 | end
144 |
145 | it "gets the messages for the specified user" do
146 | Databasedotcom::Chatter::Message.should_receive(:all).with(@client_mock, :user_id => "foo").and_return(double("response", :body => "{}"))
147 | Databasedotcom::Chatter::User.messages(@client_mock, "foo")
148 | end
149 | end
150 |
151 | context "with an instantiated object" do
152 | before do
153 | @response = File.read(File.join(File.dirname(__FILE__), "../../fixtures/chatter/users_get_id_success_response.json"))
154 | @client_mock = double("client", :version => "23")
155 | @user = Databasedotcom::Chatter::User.new(@client_mock, @response)
156 | end
157 |
158 | describe "#status" do
159 | it "returns the user's status" do
160 | @user.status.should == @user.raw_hash["currentStatus"]
161 | end
162 | end
163 |
164 | describe "#post_status" do
165 | it "post a status for the user" do
166 | Databasedotcom::Chatter::User.should_receive(:post_status).with(@client_mock, @user.id, "new status")
167 | @user.post_status("new status")
168 | end
169 | end
170 |
171 | describe "#delete_status" do
172 | it "deletes the user's current status and returns what was deleted" do
173 | @client_mock.should_receive(:http_delete).with("/services/data/v23/chatter/users/#{@user.id}/status").and_return(true)
174 | @user.delete_status
175 | end
176 | end
177 |
178 | describe "#followers!" do
179 | it "gets the followers" do
180 | Databasedotcom::Chatter::User.should_receive(:followers).with(@client_mock, @user.id)
181 | @user.followers!
182 | end
183 | end
184 |
185 | describe "#followers" do
186 | it "gets the memoized followers" do
187 | Databasedotcom::Chatter::User.should_receive(:followers).with(@client_mock, @user.id).once.and_return("foo")
188 | @user.followers
189 | @user.followers
190 | end
191 | end
192 |
193 | describe "#following!" do
194 | it "gets the Following subscriptions" do
195 | Databasedotcom::Chatter::User.should_receive(:following).with(@client_mock, @user.id)
196 | @user.following!
197 | end
198 | end
199 |
200 | describe "#following" do
201 | it "gets the memoized following subscriptions" do
202 | Databasedotcom::Chatter::User.should_receive(:following).with(@client_mock, @user.id).once.and_return("foo")
203 | @user.following
204 | @user.following
205 | end
206 | end
207 |
208 | describe "#groups!" do
209 | it "gets the groups" do
210 | Databasedotcom::Chatter::User.should_receive(:groups).with(@client_mock, @user.id)
211 | @user.groups!
212 | end
213 | end
214 |
215 | describe "#groups" do
216 | it "gets the memoized groups" do
217 | Databasedotcom::Chatter::User.should_receive(:groups).with(@client_mock, @user.id).once.and_return("foo")
218 | @user.groups
219 | @user.groups
220 | end
221 | end
222 |
223 | describe "#follow" do
224 | it "follows the subject" do
225 | Databasedotcom::Chatter::User.should_receive(:follow).with(@client_mock, @user.id, "rid")
226 | @user.follow("rid")
227 | end
228 | end
229 |
230 | describe "#conversations!" do
231 | it "gets the conversations" do
232 | Databasedotcom::Chatter::User.should_receive(:conversations).with(@client_mock, @user.id)
233 | @user.conversations!
234 | end
235 | end
236 |
237 | describe "#conversations" do
238 | it "gets the memoized conversations" do
239 | Databasedotcom::Chatter::User.should_receive(:conversations).with(@client_mock, @user.id).once.and_return("foo")
240 | @user.conversations
241 | @user.conversations
242 | end
243 | end
244 |
245 | describe "#messages!" do
246 | it "gets the messages" do
247 | Databasedotcom::Chatter::User.should_receive(:messages).with(@client_mock, @user.id)
248 | @user.messages!
249 | end
250 | end
251 |
252 | describe "#messages" do
253 | it "gets the memoized messages" do
254 | Databasedotcom::Chatter::User.should_receive(:messages).with(@client_mock, @user.id).once.and_return("foo")
255 | @user.messages
256 | @user.messages
257 | end
258 | end
259 | end
260 | end
--------------------------------------------------------------------------------
/lib/databasedotcom/sobject/sobject.rb:
--------------------------------------------------------------------------------
1 | module Databasedotcom
2 | module Sobject
3 | # Parent class of dynamically created sobject types. Interacts with Force.com through a Client object that is passed in during materialization.
4 | class Sobject
5 | cattr_accessor :client
6 | extend ActiveModel::Naming if defined?(ActiveModel::Naming)
7 |
8 | def ==(other)
9 | return false unless other.is_a?(self.class)
10 | self.Id == other.Id
11 | end
12 |
13 | # Returns a new Sobject. The default values for all attributes are set based on its description.
14 | def initialize(attrs = {})
15 | super()
16 | self.class.description["fields"].each do |field|
17 | if field['type'] =~ /(picklist|multipicklist)/ && picklist_option = field['picklistValues'].find { |p| p['defaultValue'] }
18 | self.send("#{field["name"]}=", picklist_option["value"])
19 | elsif field['type'] =~ /boolean/
20 | self.send("#{field["name"]}=", field["defaultValue"])
21 | else
22 | self.send("#{field["name"]}=", field["defaultValueFormula"])
23 | end
24 | end
25 | self.attributes=(attrs)
26 | end
27 |
28 | # Returns a hash representing the state of this object
29 | def attributes
30 | self.class.attributes.inject({}) do |hash, attr|
31 | hash[attr] = self.send(attr.to_sym) if self.respond_to?(attr.to_sym)
32 | hash
33 | end
34 | end
35 |
36 | # Set attributes of this object, from a hash, in bulk
37 | def attributes=(attrs)
38 | attrs.each do |key, value|
39 | self.send("#{key}=", value)
40 | end
41 | end
42 |
43 | # Returns true if the object has been persisted in the Force.com database.
44 | def persisted?
45 | !self.Id.nil?
46 | end
47 |
48 | # Returns true if this record has not been persisted in the Force.com database.
49 | def new_record?
50 | !self.persisted?
51 | end
52 |
53 | # Returns self.
54 | def to_model
55 | self
56 | end
57 |
58 | # Returns a unique object id for self.
59 | def to_key
60 | [object_id]
61 | end
62 |
63 | # Returns the Force.com Id for this instance.
64 | def to_param
65 | self.Id
66 | end
67 |
68 | # Updates the corresponding record on Force.com by setting the attribute +attr_name+ to +attr_value+.
69 | #
70 | # client.materialize("Car")
71 | # c = Car.new
72 | # c.update_attribute("Color", "Blue")
73 | def update_attribute(attr_name, attr_value)
74 | update_attributes(attr_name => attr_value)
75 | end
76 |
77 | # Updates the corresponding record on Force.com with the attributes specified by the +new_attrs+ hash.
78 | #
79 | # client.materialize("Car")
80 | # c = Car.new
81 | # c.update_attributes {"Color" => "Blue", "Year" => "2012"}
82 | def update_attributes(new_attrs)
83 | if self.client.update(self.class, self.Id, new_attrs)
84 | new_attrs = new_attrs.is_a?(Hash) ? new_attrs : JSON.parse(new_attrs)
85 | new_attrs.each do |attr, value|
86 | self.send("#{attr}=", value)
87 | end
88 | end
89 | self
90 | end
91 |
92 | # Updates the corresponding record on Force.com with the attributes of self.
93 | #
94 | # client.materialize("Car")
95 | # c = Car.find_by_Color("Yellow")
96 | # c.Color = "Green"
97 | # c.save
98 | #
99 | # _options_ can contain the following keys:
100 | #
101 | # exclusions # an array of field names (case sensitive) to exclude from save
102 | def save(options={})
103 | attr_hash = {}
104 | selection_attr = self.Id.nil? ? "createable" : "updateable"
105 | self.class.description["fields"].select { |f| f[selection_attr] }.collect { |f| f["name"] }.each { |attr| attr_hash[attr] = self.send(attr) }
106 |
107 | # allow fields to be removed on a case by case basis as some data is not allowed to be saved
108 | # (e.g. Name field on Account with record type of Person Account) despite the API listing
109 | # some fields as editable
110 | if options[:exclusions] and options[:exclusions].respond_to?(:include?) then
111 | attr_hash.delete_if { |key, value| options[:exclusions].include?(key.to_s) }
112 | end
113 |
114 | if self.Id.nil?
115 | self.Id = self.client.create(self.class, attr_hash).Id
116 | else
117 | self.client.update(self.class, self.Id, attr_hash)
118 | end
119 | end
120 |
121 | # Deletes the corresponding record from the Force.com database. Returns self.
122 | #
123 | # client.materialize("Car")
124 | # c = Car.find_by_Color("Yellow")
125 | # c.delete
126 | def delete
127 | if self.client.delete(self.class, self.Id)
128 | self
129 | end
130 | end
131 |
132 | # Reloads the record from the Force.com database. Returns self.
133 | #
134 | # client.materialize("Car")
135 | # c = Car.find_by_Color("Yellow")
136 | # c.reload
137 | def reload
138 | self.attributes = self.class.find(self.Id).attributes
139 | self
140 | end
141 |
142 | # Get a named attribute on this object
143 | def [](attr_name)
144 | self.send(attr_name) rescue nil
145 | end
146 |
147 | # Set a named attribute on this object
148 | def []=(attr_name, value)
149 | raise ArgumentError.new("No attribute named #{attr_name}") unless self.class.attributes.include?(attr_name)
150 | self.send("#{attr_name}=", value)
151 | end
152 |
153 | # Returns an Array of attribute names that this Sobject has.
154 | #
155 | # client.materialize("Car")
156 | # Car.attributes #=> ["Id", "Name", "Color", "Year"]
157 | def self.attributes
158 | self.description["fields"].collect { |f| [f["name"], f["relationshipName"]] }.flatten.compact
159 | end
160 |
161 | # Materializes the dynamically created Sobject class by adding all attribute accessors for each field as described in the description of the object on Force.com
162 | def self.materialize(sobject_name)
163 | self.cattr_accessor :description
164 | self.cattr_accessor :type_map
165 | self.cattr_accessor :sobject_name
166 |
167 | self.sobject_name = sobject_name
168 | self.description = self.client.describe_sobject(self.sobject_name)
169 | self.type_map = {}
170 |
171 | self.description["fields"].each do |field|
172 |
173 | # Register normal fields
174 | name = field["name"]
175 | register_field( field["name"], field )
176 |
177 | # Register relationship fields.
178 | if( field["type"] == "reference" and field["relationshipName"] )
179 | register_field( field["relationshipName"], field )
180 | end
181 |
182 | end
183 | end
184 |
185 | # Returns the Force.com type of the attribute +attr_name+. Raises ArgumentError if attribute does not exist.
186 | #
187 | # client.materialize("Car")
188 | # Car.field_type("Color") #=> "string"
189 | def self.field_type(attr_name)
190 | self.type_map_attr(attr_name, :type)
191 | end
192 |
193 | # Returns the label for the attribute +attr_name+. Raises ArgumentError if attribute does not exist.
194 | def self.label_for(attr_name)
195 | self.type_map_attr(attr_name, :label)
196 | end
197 |
198 | # Returns the possible picklist options for the attribute +attr_name+. If +attr_name+ is not of type picklist or multipicklist, [] is returned. Raises ArgumentError if attribute does not exist.
199 | def self.picklist_values(attr_name)
200 | self.type_map_attr(attr_name, :picklist_values)
201 | end
202 |
203 | # Returns true if the attribute +attr_name+ can be updated. Raises ArgumentError if attribute does not exist.
204 | def self.updateable?(attr_name)
205 | self.type_map_attr(attr_name, :updateable?)
206 | end
207 |
208 | # Returns true if the attribute +attr_name+ can be created. Raises ArgumentError if attribute does not exist.
209 | def self.createable?(attr_name)
210 | self.type_map_attr(attr_name, :createable?)
211 | end
212 |
213 | # Delegates to Client.find with arguments +record_id+ and self
214 | #
215 | # client.materialize("Car")
216 | # Car.find("rid") #=> #
217 | def self.find(record_id)
218 | self.client.find(self, record_id)
219 | end
220 |
221 | # Returns all records of type self as instances.
222 | #
223 | # client.materialize("Car")
224 | # Car.all #=> [#, #, #, ...]
225 | def self.all
226 | self.client.query("SELECT #{self.field_list} FROM #{self.sobject_name}")
227 | end
228 |
229 | # Returns a collection of instances of self that match the conditional +where_expr+, which is the WHERE part of a SOQL query.
230 | #
231 | # client.materialize("Car")
232 | # Car.query("Color = 'Blue'") #=> [#, #, ...]
233 | def self.query(where_expr)
234 | self.client.query("SELECT #{self.field_list} FROM #{self.sobject_name} WHERE #{where_expr}")
235 | end
236 |
237 | # Delegates to Client.search
238 | def self.search(sosl_expr)
239 | self.client.search(sosl_expr)
240 | end
241 |
242 | # Find the first record. If the +where_expr+ argument is present, it must be the WHERE part of a SOQL query
243 | def self.first(where_expr=nil)
244 | where = where_expr ? "WHERE #{where_expr} " : ""
245 | self.client.query("SELECT #{self.field_list} FROM #{self.sobject_name} #{where}ORDER BY Id ASC LIMIT 1").first
246 | end
247 |
248 | # Find the last record. If the +where_expr+ argument is present, it must be the WHERE part of a SOQL query
249 | def self.last(where_expr=nil)
250 | where = where_expr ? "WHERE #{where_expr} " : ""
251 | self.client.query("SELECT #{self.field_list} FROM #{self.sobject_name} #{where}ORDER BY Id DESC LIMIT 1").first
252 | end
253 |
254 | #Delegates to Client.upsert with arguments self, +field+, +values+, and +attrs+
255 | def self.upsert(field, value, attrs)
256 | self.client.upsert(self.sobject_name, field, value, attrs)
257 | end
258 |
259 | # Delegates to Client.delete with arguments +record_id+ and self
260 | def self.delete(record_id)
261 | self.client.delete(self.sobject_name, record_id)
262 | end
263 |
264 | # Get the total number of records
265 | def self.count
266 | self.client.query("SELECT COUNT() FROM #{self.sobject_name}").total_size
267 | end
268 |
269 | # Sobject objects support dynamic finders similar to ActiveRecord.
270 | #
271 | # client.materialize("Car")
272 | # Car.find_by_Color("Blue")
273 | # Car.find_all_by_Year("2011")
274 | # Car.find_by_Color_and_Year("Blue", "2011")
275 | # Car.find_or_create_by_Year("2011")
276 | # Car.find_or_initialize_by_Name("Foo")
277 | def self.method_missing(method_name, *args, &block)
278 | if method_name.to_s =~ /^find_(or_create_|or_initialize_)?by_(.+)$/ || method_name.to_s =~ /^find_(all_)by_(.+)$/
279 | named_attrs = $2.split('_and_')
280 | attrs_and_values_for_find = {}
281 | hash_args = args.length == 1 && args[0].is_a?(Hash)
282 | attrs_and_values_for_write = hash_args ? args[0] : {}
283 |
284 | named_attrs.each_with_index do |attr, index|
285 | value = hash_args ? args[0][attr] : args[index]
286 | attrs_and_values_for_find[attr] = value
287 | attrs_and_values_for_write[attr] = value unless hash_args
288 | end
289 |
290 | limit_clause = method_name.to_s.include?('_all_by_') ? "" : " LIMIT 1"
291 |
292 | results = self.client.query("SELECT #{self.field_list} FROM #{self.sobject_name} WHERE #{soql_conditions_for(attrs_and_values_for_find)}#{limit_clause}")
293 | results = limit_clause == "" ? results : results.first rescue nil
294 |
295 | if results.nil?
296 | if method_name.to_s =~ /^find_or_create_by_(.+)$/
297 | results = self.client.create(self, attrs_and_values_for_write)
298 | elsif method_name.to_s =~ /^find_or_initialize_by_(.+)$/
299 | results = self.new
300 | attrs_and_values_for_write.each { |attr, val| results.send("#{attr}=", val) }
301 | end
302 | end
303 |
304 | results
305 | else
306 | super
307 | end
308 | end
309 |
310 | # Delegates to Client.create with arguments +object_attributes+ and self
311 | def self.create(object_attributes)
312 | self.client.create(self, object_attributes)
313 | end
314 |
315 | # Coerce values submitted from a Rails form to the values expected by the database
316 | # returns a new hash with updated values
317 | def self.coerce_params(params)
318 | params.each do |attr, value|
319 | case self.field_type(attr)
320 | when "boolean"
321 | params[attr] = value.is_a?(String) ? value.to_i != 0 : value
322 | when "currency", "percent", "double"
323 | value = value.gsub(/[^-0-9.0-9]/, '').to_f if value.respond_to?(:gsub)
324 | params[attr] = value.to_f
325 | when "date"
326 | params[attr] = Date.parse(value) rescue Date.today
327 | when "datetime"
328 | params[attr] = DateTime.parse(value) rescue DateTime.now
329 | end
330 | end
331 | end
332 |
333 | private
334 |
335 | def self.register_field( name, field )
336 | public
337 | attr_accessor name.to_sym
338 | private
339 | self.type_map[name] = {
340 | :type => field["type"],
341 | :label => field["label"],
342 | :picklist_values => field["picklistValues"],
343 | :updateable? => field["updateable"],
344 | :createable? => field["createable"]
345 | }
346 | end
347 |
348 | def self.field_list
349 | self.description['fields'].collect { |f| f['name'] }.join(',')
350 | end
351 |
352 | def self.type_map_attr(attr_name, key)
353 | raise ArgumentError.new("No attribute named #{attr_name}") unless self.type_map.has_key?(attr_name)
354 | self.type_map[attr_name][key]
355 | end
356 |
357 | def self.soql_conditions_for(params)
358 | params.inject([]) do |arr, av|
359 | case av[1]
360 | when String
361 | value_str = "'#{av[1].gsub("'", "\\\\'")}'"
362 | when DateTime, Time
363 | value_str = av[1].strftime(RUBY_VERSION.match(/^1.8/) ? "%Y-%m-%dT%H:%M:%S.000%z" : "%Y-%m-%dT%H:%M:%S.%L%z").insert(-3, ":")
364 | when Date
365 | value_str = av[1].strftime("%Y-%m-%d")
366 | else
367 | value_str = av[1].to_s
368 | end
369 |
370 | arr << "#{av[0]} = #{value_str}"
371 | arr
372 | end.join(" AND ")
373 | end
374 | end
375 | end
376 | end
377 |
--------------------------------------------------------------------------------
/spec/fixtures/chatter/news_feed_items_get_success_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "items": [{
3 | "parent": {
4 | "name": "Admin User",
5 | "title": null,
6 | "firstName": "Admin",
7 | "lastName": "User",
8 | "companyName": "Pivotal",
9 | "mySubscription": null,
10 | "photo": {
11 | "smallPhotoUrl": "/profilephoto/005/T",
12 | "largePhotoUrl": "/profilephoto/005/F"
13 | },
14 | "isChatterGuest": false,
15 | "id": "005x0000000JbFuAAK",
16 | "type": "User",
17 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
18 | },
19 | "id": "0D5x00000001g6tCAA",
20 | "type": "UserStatus",
21 | "clientInfo": {
22 | "applicationName": "localhost",
23 | "applicationUrl": "null"
24 | },
25 | "url": "/services/data/v23.0/chatter/feed-items/0D5x00000001g6tCAA",
26 | "body": {
27 | "text": "I love #frogs",
28 | "messageSegments": [{
29 | "type": "Text",
30 | "text": "I love "
31 | },
32 | {
33 | "url": "/services/data/v23.0/chatter/feed-items?q=%23frogs",
34 | "tag": "frogs",
35 | "type": "Hashtag",
36 | "text": "#frogs"
37 | }]
38 | },
39 | "createdDate": "2011-08-08T21:37:08.000Z",
40 | "modifiedDate": "2011-08-08T21:39:23.000Z",
41 | "photoUrl": "/profilephoto/005/T",
42 | "comments": {
43 | "total": 1,
44 | "comments": [{
45 | "parent": {
46 | "id": "005x0000000JbFuAAK",
47 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
48 | },
49 | "id": "0D7x000000005dRCAQ",
50 | "user": {
51 | "name": "Admin User",
52 | "title": null,
53 | "firstName": "Admin",
54 | "lastName": "User",
55 | "companyName": "Pivotal",
56 | "mySubscription": null,
57 | "photo": {
58 | "smallPhotoUrl": "/profilephoto/005/T",
59 | "largePhotoUrl": "/profilephoto/005/F"
60 | },
61 | "isChatterGuest": false,
62 | "id": "005x0000000JbFuAAK",
63 | "type": "User",
64 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
65 | },
66 | "clientInfo": {
67 | "applicationName": "localhost",
68 | "applicationUrl": "null"
69 | },
70 | "url": "/services/data/v23.0/chatter/comments/0D7x000000005dRCAQ",
71 | "body": {
72 | "text": "So do I like #frogs",
73 | "messageSegments": [{
74 | "type": "Text",
75 | "text": "So do I like "
76 | },
77 | {
78 | "url": "/services/data/v23.0/chatter/feed-items?q=%23frogs",
79 | "tag": "frogs",
80 | "type": "Hashtag",
81 | "text": "#frogs"
82 | }]
83 | },
84 | "createdDate": "2011-08-08T21:39:23.000+0000",
85 | "deletable": true,
86 | "feedItem": {
87 | "id": "0D5x00000001g6tCAA",
88 | "url": "/services/data/v23.0/chatter/feed-items/0D5x00000001g6tCAA"
89 | }
90 | }],
91 | "nextPageUrl": null,
92 | "currentPageUrl": "/services/data/v23.0/chatter/feed-items/0D5x00000001g6tCAA/comments"
93 | },
94 | "likes": {
95 | "total": 1,
96 | "likes": [{
97 | "id": "0I0x000000001XiCAI",
98 | "user": {
99 | "name": "Admin User",
100 | "title": null,
101 | "firstName": "Admin",
102 | "lastName": "User",
103 | "companyName": "Pivotal",
104 | "mySubscription": null,
105 | "photo": {
106 | "smallPhotoUrl": "/profilephoto/005/T",
107 | "largePhotoUrl": "/profilephoto/005/F"
108 | },
109 | "isChatterGuest": false,
110 | "id": "005x0000000JbFuAAK",
111 | "type": "User",
112 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
113 | },
114 | "url": "/services/data/v23.0/chatter/likes/0I0x000000001XiCAI"
115 | }],
116 | "previousPageUrl": null,
117 | "nextPageUrl": null,
118 | "currentPageUrl": "/services/data/v23.0/chatter/feed-items/0D5x00000001g6tCAA/likes",
119 | "myLike": {
120 | "id": "0I0x000000001XiCAI",
121 | "url": "/services/data/v23.0/chatter/likes/0I0x000000001XiCAI"
122 | }
123 | },
124 | "isLikedByCurrentUser": true,
125 | "currentUserLike": {
126 | "id": "0I0x000000001XiCAI",
127 | "url": "/services/data/v23.0/chatter/likes/0I0x000000001XiCAI"
128 | },
129 | "actor": {
130 | "name": "Admin User",
131 | "title": null,
132 | "firstName": "Admin",
133 | "lastName": "User",
134 | "companyName": "Pivotal",
135 | "mySubscription": null,
136 | "photo": {
137 | "smallPhotoUrl": "/profilephoto/005/T",
138 | "largePhotoUrl": "/profilephoto/005/F"
139 | },
140 | "isChatterGuest": false,
141 | "id": "005x0000000JbFuAAK",
142 | "type": "User",
143 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
144 | },
145 | "event": false,
146 | "attachment": null
147 | },
148 | {
149 | "parent": {
150 | "name": "Admin User",
151 | "title": null,
152 | "firstName": "Admin",
153 | "lastName": "User",
154 | "companyName": "Pivotal",
155 | "mySubscription": null,
156 | "photo": {
157 | "smallPhotoUrl": "/profilephoto/005/T",
158 | "largePhotoUrl": "/profilephoto/005/F"
159 | },
160 | "isChatterGuest": false,
161 | "id": "005x0000000JbFuAAK",
162 | "type": "User",
163 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
164 | },
165 | "id": "0D5x00000001g7CCAQ",
166 | "type": "UserStatus",
167 | "clientInfo": {
168 | "applicationName": "localhost",
169 | "applicationUrl": "null"
170 | },
171 | "url": "/services/data/v23.0/chatter/feed-items/0D5x00000001g7CCAQ",
172 | "body": {
173 | "text": "Posting",
174 | "messageSegments": [{
175 | "type": "Text",
176 | "text": "Posting"
177 | }]
178 | },
179 | "createdDate": "2011-08-08T21:36:13.000Z",
180 | "modifiedDate": "2011-08-08T21:36:13.000Z",
181 | "photoUrl": "/profilephoto/005/T",
182 | "comments": {
183 | "total": 0,
184 | "comments": [],
185 | "nextPageUrl": null,
186 | "currentPageUrl": "/services/data/v23.0/chatter/feed-items/0D5x00000001g7CCAQ/comments"
187 | },
188 | "likes": {
189 | "total": 0,
190 | "likes": [],
191 | "previousPageUrl": null,
192 | "nextPageUrl": null,
193 | "currentPageUrl": "/services/data/v23.0/chatter/feed-items/0D5x00000001g7CCAQ/likes",
194 | "myLike": null
195 | },
196 | "isLikedByCurrentUser": false,
197 | "currentUserLike": null,
198 | "actor": {
199 | "name": "Admin User",
200 | "title": null,
201 | "firstName": "Admin",
202 | "lastName": "User",
203 | "companyName": "Pivotal",
204 | "mySubscription": null,
205 | "photo": {
206 | "smallPhotoUrl": "/profilephoto/005/T",
207 | "largePhotoUrl": "/profilephoto/005/F"
208 | },
209 | "isChatterGuest": false,
210 | "id": "005x0000000JbFuAAK",
211 | "type": "User",
212 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
213 | },
214 | "event": false,
215 | "attachment": null
216 | },
217 | {
218 | "parent": {
219 | "name": "Admin User",
220 | "title": null,
221 | "firstName": "Admin",
222 | "lastName": "User",
223 | "companyName": "Pivotal",
224 | "mySubscription": null,
225 | "photo": {
226 | "smallPhotoUrl": "/profilephoto/005/T",
227 | "largePhotoUrl": "/profilephoto/005/F"
228 | },
229 | "isChatterGuest": false,
230 | "id": "005x0000000JbFuAAK",
231 | "type": "User",
232 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
233 | },
234 | "id": "0D5x00000001g78CAA",
235 | "type": "UserStatus",
236 | "clientInfo": null,
237 | "url": "/services/data/v23.0/chatter/feed-items/0D5x00000001g78CAA",
238 | "body": {
239 | "text": "This is so great",
240 | "messageSegments": [{
241 | "type": "Text",
242 | "text": "This is so great"
243 | }]
244 | },
245 | "createdDate": "2011-08-08T21:07:48.000Z",
246 | "modifiedDate": "2011-08-08T21:07:48.000Z",
247 | "photoUrl": "/profilephoto/005/T",
248 | "comments": {
249 | "total": 0,
250 | "comments": [],
251 | "nextPageUrl": null,
252 | "currentPageUrl": "/services/data/v23.0/chatter/feed-items/0D5x00000001g78CAA/comments"
253 | },
254 | "likes": {
255 | "total": 0,
256 | "likes": [],
257 | "previousPageUrl": null,
258 | "nextPageUrl": null,
259 | "currentPageUrl": "/services/data/v23.0/chatter/feed-items/0D5x00000001g78CAA/likes",
260 | "myLike": null
261 | },
262 | "isLikedByCurrentUser": false,
263 | "currentUserLike": null,
264 | "actor": {
265 | "name": "Admin User",
266 | "title": null,
267 | "firstName": "Admin",
268 | "lastName": "User",
269 | "companyName": "Pivotal",
270 | "mySubscription": null,
271 | "photo": {
272 | "smallPhotoUrl": "/profilephoto/005/T",
273 | "largePhotoUrl": "/profilephoto/005/F"
274 | },
275 | "isChatterGuest": false,
276 | "id": "005x0000000JbFuAAK",
277 | "type": "User",
278 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
279 | },
280 | "event": false,
281 | "attachment": null
282 | },
283 | {
284 | "parent": {
285 | "name": "All Pivotal",
286 | "description": "Get company announcements and other important updates",
287 | "visibility": "PublicAccess",
288 | "memberCount": 1,
289 | "canHaveChatterGuests": false,
290 | "mySubscription": {
291 | "id": "0FBx000000006TrGAI",
292 | "url": "/services/data/v23.0/chatter/group-memberships/0FBx000000006TrGAI"
293 | },
294 | "myRole": "GroupOwner",
295 | "photo": {
296 | "smallPhotoUrl": "/profilephoto/729x000000005HB/T",
297 | "largePhotoUrl": "/profilephoto/729x000000005HB/F"
298 | },
299 | "id": "0F9x000000002HVCAY",
300 | "type": "CollaborationGroup",
301 | "url": "/services/data/v23.0/chatter/groups/0F9x000000002HVCAY"
302 | },
303 | "id": "0D5x00000001g77CAA",
304 | "type": "TrackedChange",
305 | "clientInfo": null,
306 | "url": "/services/data/v23.0/chatter/feed-items/0D5x00000001g77CAA",
307 | "body": {
308 | "text": "created this group.",
309 | "messageSegments": [{
310 | "type": "Text",
311 | "text": "created this group."
312 | }]
313 | },
314 | "createdDate": "2011-08-08T21:07:31.000Z",
315 | "modifiedDate": "2011-08-08T21:08:06.000Z",
316 | "photoUrl": "/profilephoto/005/T",
317 | "comments": {
318 | "total": 1,
319 | "comments": [{
320 | "parent": {
321 | "id": "0F9x000000002HVCAY",
322 | "url": "/services/data/v23.0/chatter/groups/0F9x000000002HVCAY"
323 | },
324 | "id": "0D7x000000005dMCAQ",
325 | "user": {
326 | "name": "Admin User",
327 | "title": null,
328 | "firstName": "Admin",
329 | "lastName": "User",
330 | "companyName": "Pivotal",
331 | "mySubscription": null,
332 | "photo": {
333 | "smallPhotoUrl": "/profilephoto/005/T",
334 | "largePhotoUrl": "/profilephoto/005/F"
335 | },
336 | "isChatterGuest": false,
337 | "id": "005x0000000JbFuAAK",
338 | "type": "User",
339 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
340 | },
341 | "clientInfo": {
342 | "applicationName": "localhost",
343 | "applicationUrl": "null"
344 | },
345 | "url": "/services/data/v23.0/chatter/comments/0D7x000000005dMCAQ",
346 | "body": {
347 | "text": "yes, it is",
348 | "messageSegments": [{
349 | "type": "Text",
350 | "text": "yes, it is"
351 | }]
352 | },
353 | "createdDate": "2011-08-08T21:08:06.000+0000",
354 | "deletable": true,
355 | "feedItem": {
356 | "id": "0D5x00000001g77CAA",
357 | "url": "/services/data/v23.0/chatter/feed-items/0D5x00000001g77CAA"
358 | }
359 | }],
360 | "nextPageUrl": null,
361 | "currentPageUrl": "/services/data/v23.0/chatter/feed-items/0D5x00000001g77CAA/comments"
362 | },
363 | "likes": {
364 | "total": 0,
365 | "likes": [],
366 | "previousPageUrl": null,
367 | "nextPageUrl": null,
368 | "currentPageUrl": "/services/data/v23.0/chatter/feed-items/0D5x00000001g77CAA/likes",
369 | "myLike": null
370 | },
371 | "isLikedByCurrentUser": false,
372 | "currentUserLike": null,
373 | "actor": {
374 | "name": "Admin User",
375 | "title": null,
376 | "firstName": "Admin",
377 | "lastName": "User",
378 | "companyName": "Pivotal",
379 | "mySubscription": null,
380 | "photo": {
381 | "smallPhotoUrl": "/profilephoto/005/T",
382 | "largePhotoUrl": "/profilephoto/005/F"
383 | },
384 | "isChatterGuest": false,
385 | "id": "005x0000000JbFuAAK",
386 | "type": "User",
387 | "url": "/services/data/v23.0/chatter/users/005x0000000JbFuAAK"
388 | },
389 | "event": true,
390 | "attachment": null
391 | }],
392 | "nextPageUrl": null,
393 | "currentPageUrl": "/services/data/v23.0/chatter/feeds/company/feed-items",
394 | "isModifiedUrl": null
395 | }
--------------------------------------------------------------------------------
/lib/databasedotcom/client.rb:
--------------------------------------------------------------------------------
1 | require 'net/https'
2 | require 'json'
3 | require 'net/http/post/multipart'
4 | require 'date'
5 |
6 | module Databasedotcom
7 | # Interface for operating the Force.com REST API
8 | class Client
9 | # The client id (aka "Consumer Key") to use for OAuth2 authentication
10 | attr_accessor :client_id
11 | # The client secret (aka "Consumer Secret" to use for OAuth2 authentication)
12 | attr_accessor :client_secret
13 | # The OAuth access token in use by the client
14 | attr_accessor :oauth_token
15 | # The OAuth refresh token in use by the client
16 | attr_accessor :refresh_token
17 | # The base URL to the authenticated user's SalesForce instance
18 | attr_accessor :instance_url
19 | # If true, print API debugging information to stdout. Defaults to false.
20 | attr_accessor :debugging
21 | # The host to use for OAuth2 authentication. Defaults to +login.salesforce.com+
22 | attr_accessor :host
23 | # The API version the client is using. Defaults to 23.0
24 | attr_accessor :version
25 | # A Module in which to materialize Sobject classes. Defaults to the global module (Object)
26 | attr_accessor :sobject_module
27 | # The SalesForce user id of the authenticated user
28 | attr_reader :user_id
29 | # The SalesForce username
30 | attr_accessor :username
31 | # The SalesForce password
32 | attr_accessor :password
33 | # The SalesForce organization id for the authenticated user's Salesforce instance
34 | attr_reader :org_id
35 | # The CA file configured for this instance, if any
36 | attr_accessor :ca_file
37 | # The SSL verify mode configured for this instance, if any
38 | attr_accessor :verify_mode
39 |
40 | # Returns a new client object. _options_ can be one of the following
41 | #
42 | # * A String containing the name of a YAML file formatted like:
43 | # ---
44 | # client_id:
45 | # client_secret:
46 | # host: login.salesforce.com
47 | # debugging: true
48 | # version: 23.0
49 | # sobject_module: My::Module
50 | # ca_file: some/ca/file.cert
51 | # verify_mode: OpenSSL::SSL::VERIFY_PEER
52 | # * A Hash containing the following keys:
53 | # client_id
54 | # client_secret
55 | # host
56 | # debugging
57 | # version
58 | # sobject_module
59 | # ca_file
60 | # verify_mode
61 | # If the environment variables DATABASEDOTCOM_CLIENT_ID, DATABASEDOTCOM_CLIENT_SECRET, DATABASEDOTCOM_HOST,
62 | # DATABASEDOTCOM_DEBUGGING, DATABASEDOTCOM_VERSION, DATABASEDOTCOM_SOBJECT_MODULE, DATABASEDOTCOM_CA_FILE, and/or
63 | # DATABASEDOTCOM_VERIFY_MODE are present, they override any other values provided
64 | def initialize(options = {})
65 | if options.is_a?(String)
66 | @options = YAML.load_file(options)
67 | @options["verify_mode"] = @options["verify_mode"].constantize if @options["verify_mode"] && @options["verify_mode"].is_a?(String)
68 | else
69 | @options = options
70 | end
71 | @options.symbolize_keys!
72 |
73 | if ENV['DATABASE_COM_URL']
74 | url = URI.parse(ENV['DATABASE_COM_URL'])
75 | url_options = Hash[url.query.split("&").map{|q| q.split("=")}].symbolize_keys!
76 | self.host = url.host
77 | self.client_id = url_options[:oauth_key]
78 | self.client_secret = url_options[:oauth_secret]
79 | self.username = url_options[:user]
80 | self.password = url_options[:password]
81 | else
82 | self.client_id = ENV['DATABASEDOTCOM_CLIENT_ID'] || @options[:client_id]
83 | self.client_secret = ENV['DATABASEDOTCOM_CLIENT_SECRET'] || @options[:client_secret]
84 | self.host = ENV['DATABASEDOTCOM_HOST'] || @options[:host] || "login.salesforce.com"
85 | end
86 |
87 | self.debugging = ENV['DATABASEDOTCOM_DEBUGGING'] || @options[:debugging]
88 | self.version = ENV['DATABASEDOTCOM_VERSION'] || @options[:version]
89 | self.version = self.version.to_s if self.version
90 | self.sobject_module = ENV['DATABASEDOTCOM_SOBJECT_MODULE'] || @options[:sobject_module]
91 | self.ca_file = ENV['DATABASEDOTCOM_CA_FILE'] || @options[:ca_file]
92 | self.verify_mode = ENV['DATABASEDOTCOM_VERIFY_MODE'] || @options[:verify_mode]
93 | self.verify_mode = self.verify_mode.to_i if self.verify_mode
94 | end
95 |
96 | # Authenticate to the Force.com API. _options_ is a Hash, interpreted as follows:
97 | #
98 | # * If _options_ contains the keys :username and :password, those credentials are used to authenticate. In this case, the value of :password may need to include a concatenated security token, if required by your Salesforce org
99 | # * If _options_ contains the key :provider, it is assumed to be the hash returned by Omniauth from a successful web-based OAuth2 authentication
100 | # * If _options_ contains the keys :token and :instance_url, those are assumed to be a valid OAuth2 token and instance URL for a Salesforce account, obtained from an external source. _options_ may also optionally contain the key :refresh_token
101 | #
102 | # Raises SalesForceError if an error occurs
103 | def authenticate(options = nil)
104 | if user_and_pass?(options)
105 | req = https_request(self.host)
106 | user = self.username || options[:username]
107 | pass = self.password || options[:password]
108 | path = encode_path_with_params('/services/oauth2/token', :grant_type => 'password', :client_id => self.client_id, :client_secret => self.client_secret, :username => user, :password => pass)
109 | log_request("https://#{self.host}/#{path}")
110 | result = req.post(path, "")
111 | log_response(result)
112 | raise SalesForceError.new(result) unless result.is_a?(Net::HTTPOK)
113 | self.username = user
114 | self.password = pass
115 | parse_auth_response(result.body)
116 | elsif options.is_a?(Hash)
117 | if options.has_key?("provider")
118 | parse_user_id_and_org_id_from_identity_url(options["uid"])
119 | self.instance_url = options["credentials"]["instance_url"]
120 | self.oauth_token = options["credentials"]["token"]
121 | self.refresh_token = options["credentials"]["refresh_token"]
122 | else
123 | raise ArgumentError unless options.has_key?(:token) && options.has_key?(:instance_url)
124 | self.instance_url = options[:instance_url]
125 | self.oauth_token = options[:token]
126 | self.refresh_token = options[:refresh_token]
127 | end
128 | end
129 |
130 | self.version = "22.0" unless self.version
131 |
132 | self.oauth_token
133 | end
134 |
135 | # The SalesForce organization id for the authenticated user's Salesforce instance
136 | def org_id
137 | @org_id ||= query_org_id # lazy query org_id when not set by login response
138 | end
139 |
140 | # Returns an Array of Strings listing the class names for every type of _Sobject_ in the database. Raises SalesForceError if an error occurs.
141 | def list_sobjects
142 | result = http_get("/services/data/v#{self.version}/sobjects")
143 | if result.is_a?(Net::HTTPOK)
144 | JSON.parse(result.body)["sobjects"].collect { |sobject| sobject["name"] }
145 | elsif result.is_a?(Net::HTTPBadRequest)
146 | raise SalesForceError.new(result)
147 | end
148 | end
149 |
150 | # Dynamically defines classes for Force.com class names. _classnames_ can be a single String or an Array of Strings. Returns the class or Array of classes defined.
151 | #
152 | # client.materialize("Contact") #=> Contact
153 | # client.materialize(%w(Contact Company)) #=> [Contact, Company]
154 | #
155 | # The classes defined by materialize derive from Sobject, and have getters and setters defined for all the attributes defined by the associated Force.com Sobject.
156 | def materialize(classnames)
157 | classes = (classnames.is_a?(Array) ? classnames : [classnames]).collect do |clazz|
158 | original_classname = clazz
159 | clazz = original_classname[0,1].capitalize + original_classname[1..-1]
160 | unless const_defined_in_module(module_namespace, clazz)
161 | new_class = module_namespace.const_set(clazz, Class.new(Databasedotcom::Sobject::Sobject))
162 | new_class.client = self
163 | new_class.materialize(original_classname)
164 | new_class
165 | else
166 | module_namespace.const_get(clazz)
167 | end
168 | end
169 |
170 | classes.length == 1 ? classes.first : classes
171 | end
172 |
173 | # Returns an Array of Hashes listing the properties for every type of _Sobject_ in the database. Raises SalesForceError if an error occurs.
174 | def describe_sobjects
175 | result = http_get("/services/data/v#{self.version}/sobjects")
176 | JSON.parse(result.body)["sobjects"]
177 | end
178 |
179 | # Returns a description of the Sobject specified by _class_name_. The description includes all fields and their properties for the Sobject.
180 | def describe_sobject(class_name)
181 | result = http_get("/services/data/v#{self.version}/sobjects/#{class_name}/describe")
182 | JSON.parse(result.body)
183 | end
184 |
185 | # Returns an instance of the Sobject specified by _class_or_classname_ (which can be either a String or a Class) populated with the values of the Force.com record specified by _record_id_.
186 | # If given a Class that is not defined, it will attempt to materialize the class on demand.
187 | #
188 | # client.find(Account, "recordid") #=> #
189 | def find(class_or_classname, record_id)
190 | class_or_classname = find_or_materialize(class_or_classname)
191 | result = http_get("/services/data/v#{self.version}/sobjects/#{class_or_classname.sobject_name}/#{record_id}")
192 | response = JSON.parse(result.body)
193 | new_record = class_or_classname.new
194 | class_or_classname.description["fields"].each do |field|
195 | set_value(new_record, field["name"], response[key_from_label(field["label"])] || response[field["name"]], field["type"])
196 | end
197 | new_record
198 | end
199 |
200 | # Returns a Collection of Sobjects of the class specified in the _soql_expr_, which is a valid SOQL[http://www.salesforce.com/us/developer/docs/api/Content/sforce_api_calls_soql.htm] expression. The objects will only be populated with the values of attributes specified in the query.
201 | #
202 | # client.query("SELECT Name FROM Account") #=> [#, # ...]
203 | def query(soql_expr)
204 | result = http_get("/services/data/v#{self.version}/query", :q => soql_expr)
205 | collection_from(result.body)
206 | end
207 |
208 | # Returns a Collection of Sobject instances form the results of the SOSL[http://www.salesforce.com/us/developer/docs/api/Content/sforce_api_calls_sosl.htm] search.
209 | #
210 | # client.search("FIND {bar}") #=> [#, # ...]
211 | def search(sosl_expr)
212 | result = http_get("/services/data/v#{self.version}/search", :q => sosl_expr)
213 | collection_from(result.body)
214 | end
215 |
216 | # Used by Collection objects. Returns a Collection of Sobjects from the specified URL path that represents the next page of paginated results.
217 | def next_page(path)
218 | result = http_get(path)
219 | collection_from(result.body)
220 | end
221 |
222 | # Used by Collection objects. Returns a Collection of Sobjects from the specified URL path that represents the previous page of paginated results.
223 | def previous_page(path)
224 | result = http_get(path)
225 | collection_from(result.body)
226 | end
227 |
228 | # Returns a new instance of _class_or_classname_ (which can be passed in as either a String or a Class) with the specified attributes.
229 | #
230 | # client.create("Car", {"Color" => "Blue", "Year" => "2011"}) #=> #
231 | def create(class_or_classname, object_attrs)
232 | class_or_classname = find_or_materialize(class_or_classname)
233 | json_for_assignment = coerced_json(object_attrs, class_or_classname)
234 | result = http_post("/services/data/v#{self.version}/sobjects/#{class_or_classname.sobject_name}", json_for_assignment)
235 | new_object = class_or_classname.new
236 | JSON.parse(json_for_assignment).each do |property, value|
237 | set_value(new_object, property, value, class_or_classname.type_map[property][:type])
238 | end
239 | id = JSON.parse(result.body)["id"]
240 | set_value(new_object, "Id", id, "id")
241 | new_object
242 | end
243 |
244 | # Updates the attributes of the record of type _class_or_classname_ and specified by _record_id_ with the values of _new_attrs_ in the Force.com database. _new_attrs_ is a hash of attribute => value
245 | #
246 | # client.update("Car", "rid", {"Color" => "Red"})
247 | def update(class_or_classname, record_id, new_attrs)
248 | class_or_classname = find_or_materialize(class_or_classname)
249 | json_for_update = coerced_json(new_attrs, class_or_classname)
250 | http_patch("/services/data/v#{self.version}/sobjects/#{class_or_classname.sobject_name}/#{record_id}", json_for_update)
251 | end
252 |
253 | # Attempts to find the record on Force.com of type _class_or_classname_ with attribute _field_ set as _value_. If found, it will update the record with the _attrs_ hash.
254 | # If not found, it will create a new record with _attrs_.
255 | #
256 | # client.upsert(Car, "Color", "Blue", {"Year" => "2012"})
257 | def upsert(class_or_classname, field, value, attrs)
258 | clazz = find_or_materialize(class_or_classname)
259 | json_for_update = coerced_json(attrs, clazz)
260 | http_patch("/services/data/v#{self.version}/sobjects/#{clazz.sobject_name}/#{field}/#{value}", json_for_update)
261 | end
262 |
263 | # Deletes the record of type _class_or_classname_ with id of _record_id_. _class_or_classname_ can be a String or a Class.
264 | #
265 | # client.delete(Car, "rid")
266 | def delete(class_or_classname, record_id)
267 | clazz = find_or_materialize(class_or_classname)
268 | http_delete("/services/data/v#{self.version}/sobjects/#{clazz.sobject_name}/#{record_id}")
269 | end
270 |
271 | # Returns a Collection of recently touched items. The Collection contains Sobject instances that are fully populated with their correct values.
272 | def recent
273 | result = http_get("/services/data/v#{self.version}/recent")
274 | collection_from(result.body)
275 | end
276 |
277 | # Returns an array of trending topic names.
278 | def trending_topics
279 | result = http_get("/services/data/v#{self.version}/chatter/topics/trending")
280 | result = JSON.parse(result.body)
281 | result["topics"].collect { |topic| topic["name"] }
282 | end
283 |
284 | # Performs an HTTP GET request to the specified path (relative to self.instance_url). Query parameters are included from _parameters_. The required
285 | # +Authorization+ header is automatically included, as are any additional headers specified in _headers_. Returns the HTTPResult if it is of type
286 | # HTTPSuccess- raises SalesForceError otherwise.
287 | def http_get(path, parameters={}, headers={})
288 | with_encoded_path_and_checked_response(path, parameters) do |encoded_path|
289 | https_request.get(encoded_path, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
290 | end
291 | end
292 |
293 |
294 | # Performs an HTTP DELETE request to the specified path (relative to self.instance_url). Query parameters are included from _parameters_. The required
295 | # +Authorization+ header is automatically included, as are any additional headers specified in _headers_. Returns the HTTPResult if it is of type
296 | # HTTPSuccess- raises SalesForceError otherwise.
297 | def http_delete(path, parameters={}, headers={})
298 | with_encoded_path_and_checked_response(path, parameters, {:expected_result_class => Net::HTTPNoContent}) do |encoded_path|
299 | https_request.delete(encoded_path, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
300 | end
301 | end
302 |
303 | # Performs an HTTP POST request to the specified path (relative to self.instance_url). The body of the request is taken from _data_.
304 | # Query parameters are included from _parameters_. The required +Authorization+ header is automatically included, as are any additional
305 | # headers specified in _headers_. Returns the HTTPResult if it is of type HTTPSuccess- raises SalesForceError otherwise.
306 | def http_post(path, data=nil, parameters={}, headers={})
307 | with_encoded_path_and_checked_response(path, parameters, {:data => data}) do |encoded_path|
308 | https_request.post(encoded_path, data, {"Content-Type" => data ? "application/json" : "text/plain", "Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
309 | end
310 | end
311 |
312 | # Performs an HTTP PATCH request to the specified path (relative to self.instance_url). The body of the request is taken from _data_.
313 | # Query parameters are included from _parameters_. The required +Authorization+ header is automatically included, as are any additional
314 | # headers specified in _headers_. Returns the HTTPResult if it is of type HTTPSuccess- raises SalesForceError otherwise.
315 | def http_patch(path, data=nil, parameters={}, headers={})
316 | with_encoded_path_and_checked_response(path, parameters, {:data => data}) do |encoded_path|
317 | https_request.send_request("PATCH", encoded_path, data, {"Content-Type" => data ? "application/json" : "text/plain", "Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
318 | end
319 | end
320 |
321 | # Performs an HTTP POST request to the specified path (relative to self.instance_url), using Content-Type multiplart/form-data.
322 | # The parts of the body of the request are taken from parts_. Query parameters are included from _parameters_. The required
323 | # +Authorization+ header is automatically included, as are any additional headers specified in _headers_.
324 | # Returns the HTTPResult if it is of type HTTPSuccess- raises SalesForceError otherwise.
325 | def http_multipart_post(path, parts, parameters={}, headers={})
326 | with_encoded_path_and_checked_response(path, parameters) do |encoded_path|
327 | https_request.request(Net::HTTP::Post::Multipart.new(encoded_path, parts, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers)))
328 | end
329 | end
330 |
331 | private
332 |
333 | def with_encoded_path_and_checked_response(path, parameters, options = {})
334 | ensure_expected_response(options[:expected_result_class]) do
335 | with_logging(encode_path_with_params(path, parameters), options) do |encoded_path|
336 | yield(encoded_path)
337 | end
338 | end
339 | end
340 |
341 | def with_logging(encoded_path, options)
342 | log_request(encoded_path, options)
343 | response = yield encoded_path
344 | log_response(response)
345 | response
346 | end
347 |
348 | def ensure_expected_response(expected_result_class)
349 | response = yield
350 |
351 | unless response.is_a?(expected_result_class || Net::HTTPSuccess)
352 | if response.is_a?(Net::HTTPUnauthorized)
353 | if self.refresh_token
354 | response = with_encoded_path_and_checked_response("/services/oauth2/token", { :grant_type => "refresh_token", :refresh_token => self.refresh_token, :client_id => self.client_id, :client_secret => self.client_secret}, :host => self.host) do |encoded_path|
355 | response = https_request(self.host).post(encoded_path, nil)
356 | if response.is_a?(Net::HTTPOK)
357 | parse_auth_response(response.body)
358 | end
359 | response
360 | end
361 | elsif self.username && self.password
362 | response = with_encoded_path_and_checked_response("/services/oauth2/token", { :grant_type => "password", :username => self.username, :password => self.password, :client_id => self.client_id, :client_secret => self.client_secret}, :host => self.host) do |encoded_path|
363 | response = https_request(self.host).post(encoded_path, nil)
364 | if response.is_a?(Net::HTTPOK)
365 | parse_auth_response(response.body)
366 | end
367 | response
368 | end
369 | end
370 |
371 | if response.is_a?(Net::HTTPSuccess)
372 | response = yield
373 | end
374 | end
375 |
376 | raise SalesForceError.new(response) unless response.is_a?(expected_result_class || Net::HTTPSuccess)
377 | end
378 |
379 | response
380 | end
381 |
382 | def https_request(host=nil)
383 | Net::HTTP.new(host || URI.parse(self.instance_url).host, 443).tap do |http|
384 | http.use_ssl = true
385 | http.ca_file = self.ca_file if self.ca_file
386 | http.verify_mode = self.verify_mode if self.verify_mode
387 | end
388 | end
389 |
390 | def encode_path_with_params(path, parameters={})
391 | [URI.escape(path), encode_parameters(parameters)].reject{|el| el.empty?}.join('?')
392 | end
393 |
394 | def encode_parameters(parameters={})
395 | (parameters || {}).collect { |k, v| "#{uri_escape(k)}=#{uri_escape(v)}" }.join('&')
396 | end
397 |
398 | def log_request(path, options={})
399 | base_url = options[:host] ? "https://#{options[:host]}" : self.instance_url
400 | puts "***** REQUEST: #{path.include?(':') ? path : URI.join(base_url, path)}#{options[:data] ? " => #{options[:data]}" : ''}" if self.debugging
401 | end
402 |
403 | def uri_escape(str)
404 | URI.escape(str.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
405 | end
406 |
407 | def log_response(result)
408 | puts "***** RESPONSE: #{result.class.name} -> #{result.body}" if self.debugging
409 | end
410 |
411 | def find_or_materialize(class_or_classname)
412 | if class_or_classname.is_a?(Class)
413 | clazz = class_or_classname
414 | else
415 | match = class_or_classname.match(/(?:(.+)::)?(\w+)$/)
416 | preceding_namespace = match[1]
417 | classname = match[2]
418 | raise ArgumentError if preceding_namespace && preceding_namespace != module_namespace.name
419 | clazz = module_namespace.const_get(classname.to_sym) rescue nil
420 | clazz ||= self.materialize(classname)
421 | end
422 | clazz
423 | end
424 |
425 | def module_namespace
426 | _module = self.sobject_module
427 | _module = _module.constantize if _module.is_a? String
428 | _module || Object
429 | end
430 |
431 | def collection_from(response)
432 | response = JSON.parse(response)
433 | collection_from_hash( response )
434 | end
435 |
436 | # Converts a Hash of object data into a concrete SObject
437 | def record_from_hash(data)
438 | attributes = data.delete('attributes')
439 | new_record = find_or_materialize(attributes["type"]).new
440 | data.each do |name, value|
441 | field = new_record.description['fields'].find do |field|
442 | key_from_label(field["label"]) == name || field["name"] == name || field["relationshipName"] == name
443 | end
444 |
445 | # Field not found
446 | if field == nil
447 | break
448 | end
449 |
450 | # If reference/lookup field data was fetched, recursively build the child record and apply
451 | if value.is_a?(Hash) and field['type'] == 'reference' and field["relationshipName"]
452 | relation = record_from_hash( value )
453 | set_value( new_record, field["relationshipName"], relation, 'reference' )
454 |
455 | # Apply the raw value for all other field types
456 | else
457 | set_value(new_record, field["name"], value, field["type"]) if field
458 | end
459 | end
460 | new_record
461 | end
462 |
463 | def collection_from_hash(data)
464 | array_response = data.is_a?(Array)
465 | if array_response
466 | records = data.collect { |rec| self.find(rec["attributes"]["type"], rec["Id"]) }
467 | else
468 | records = data["records"].collect do |record|
469 | record_from_hash( record )
470 | end
471 | end
472 |
473 | Databasedotcom::Collection.new(self, array_response ? records.length : data["totalSize"], array_response ? nil : data["nextRecordsUrl"]).concat(records)
474 | end
475 |
476 | def set_value(record, attr, value, attr_type)
477 | value_to_set = value
478 |
479 | case attr_type
480 | when "datetime"
481 | value_to_set = DateTime.parse(value) rescue nil
482 |
483 | when "date"
484 | value_to_set = Date.parse(value) rescue nil
485 |
486 | when "multipicklist"
487 | value_to_set = value.split(";") rescue []
488 | end
489 |
490 | record.send("#{attr}=", value_to_set)
491 | end
492 |
493 | def coerced_json(attrs, clazz)
494 | if attrs.is_a?(Hash)
495 | coerced_attrs = {}
496 | attrs.keys.each do |key|
497 | case clazz.field_type(key.to_s)
498 | when "multipicklist"
499 | coerced_attrs[key] = (attrs[key] || []).join(';')
500 | when "datetime"
501 | begin
502 | attrs[key] = DateTime.parse(attrs[key]) if attrs[key].is_a?(String)
503 | coerced_attrs[key] = attrs[key].strftime(RUBY_VERSION.match(/^1.8/) ? "%Y-%m-%dT%H:%M:%S.000%z" : "%Y-%m-%dT%H:%M:%S.%L%z")
504 | rescue
505 | nil
506 | end
507 | when "date"
508 | if attrs[key]
509 | coerced_attrs[key] = attrs[key].respond_to?(:strftime) ? attrs[key].strftime("%Y-%m-%d") : attrs[key]
510 | else
511 | coerced_attrs[key] = nil
512 | end
513 | else
514 | coerced_attrs[key] = attrs[key]
515 | end
516 | end
517 | coerced_attrs.to_json
518 | else
519 | attrs
520 | end
521 | end
522 |
523 | def key_from_label(label)
524 | label.gsub(' ', '_')
525 | end
526 |
527 | def user_and_pass?(options)
528 | (self.username && self.password) || (options && options[:username] && options[:password])
529 | end
530 |
531 | def parse_user_id_and_org_id_from_identity_url(identity_url)
532 | m = identity_url.match(/\/id\/([^\/]+)\/([^\/]+)$/)
533 | @org_id = m[1] rescue nil
534 | @user_id = m[2] rescue nil
535 | end
536 |
537 | def parse_auth_response(body)
538 | json = JSON.parse(body)
539 | parse_user_id_and_org_id_from_identity_url(json["id"])
540 | self.instance_url = json["instance_url"]
541 | self.oauth_token = json["access_token"]
542 | end
543 |
544 | def query_org_id
545 | query("select id from Organization")[0]["Id"]
546 | end
547 |
548 | def const_defined_in_module(mod, const)
549 | mod.method(:const_defined?).arity == 1 ? mod.const_defined?(const) : mod.const_defined?(const, false)
550 | end
551 | end
552 | end
553 |
--------------------------------------------------------------------------------