docusigner 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ .bundle/
2
+ *.swp
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'http://rubygems.org'
2
+ gemspec
3
+ gem 'rake'
data/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # Docusigner
2
+
3
+ This gem is meant to be a simple ActiveResource based interface to DocuSign's REST api. Where applicable, objects know about their association relationships.
4
+
5
+ You can read more about DocuSign's REST API:
6
+
7
+ * [Developer Center](http://www.docusign.com/developers-center)
8
+ * [API Documentation (PDF)](http://www.docusign.com/sites/default/files/REST_API_Guide_v2.pdf)
9
+
10
+ *SPECIAL NOTE*: I do not yet have access to the production server, so please consider this a beta. It has only been tested against the demo server and against the expected responses provided by the above API guide.
11
+
12
+
13
+ ## Requirements
14
+
15
+ * `reactive_resource`
16
+ * `multipart_post`
17
+
18
+ This library will handle multipart post requests to DocuSign and provide the correct headers for the individual posts. The `multipart_post` gem is required for its internals, but not used as it did not allow the headers DocuSign expects.
19
+
20
+ ## Setup
21
+
22
+ You can use either the X-DocuSign-Authentication header or an OAuth2 bearer token to access the API. Configuration for your app is simple.
23
+
24
+ ### X-DocuSign-Authentication
25
+
26
+ Docusigner::Base.authentication = {
27
+ :username => "your_username_here",
28
+ :password => "your_password_here",
29
+ :integrator_key => "your_integrator_key_here"
30
+ }
31
+
32
+ ### OAuth2
33
+
34
+ Docusigner::Base.token = "your_api_token"
35
+
36
+ Additionally, you can easily request (or revoke) a token through the API.
37
+
38
+ # request an OAuth2 token
39
+ token = Docusigner::Oauth2.token("username", "password", "integrator_key")
40
+
41
+ # revoke an OAuth2 token
42
+ Docusigner::Oauth2.revoke("token")
43
+
44
+ ### Domain
45
+
46
+ By default, the API points to the development platform at https://demo.docusign.net. Changing to the live site is simple:
47
+
48
+ Docusigner::Base.site = "https://www.docusign.net/restapi/v2"
49
+
50
+ ## Usage
51
+
52
+ Once you've configured the client, accessing resources is easy. This client is based off of [reactive_resource](http://github.com/justinweiss/reactive_resource) which is built off of [active_resource](http://api.rubyonrails.org/classes/ActiveResource/Base.html). Code should look similar to using ActiveRecord objects.
53
+
54
+ ### Examples:
55
+
56
+ #### Fetch basic account information
57
+
58
+ # find the account
59
+ account = Docusigner::Account.find(1234)
60
+
61
+ # access basic attributes
62
+ account.id
63
+ => 1234
64
+ account.name
65
+ => "My account name"
66
+
67
+ # list templates
68
+ account.templates
69
+ => [#<Docusigner::Template>, #<Docusigner::Template>]
70
+
71
+ #### Create an envelope
72
+
73
+ envelope = Docusigner::Envelope.new({
74
+ :account_id => 1234,
75
+ :emailSubject => "Fee Agreement",
76
+ :emailBlurb => "Please sign the attached document"
77
+ :recipients => {
78
+ :signers => [
79
+ {
80
+ :email => "signer@gmail.com",
81
+ :name => "Bob Smith",
82
+ :recipientId => 1,
83
+ :clientUserId => 123, # if you want to do
84
+ :tabs => {
85
+ # can add tabs here
86
+ },
87
+ }
88
+ ]
89
+ },
90
+ :documents => [
91
+ {
92
+ :name => "Fee Agreement",
93
+ :documentId => 333,
94
+ }
95
+ ],
96
+ :status => Docusigner::Envelope::Status::SENT
97
+ })
98
+ envelope.add_document(File.open("/path/to/document.pdf"), 333)
99
+ envelope.save
100
+
101
+ For the most part, the complex data structures expected as parameters can be expressed with nested hashes when creating elements.
102
+
103
+ ## Contributing
104
+
105
+ If you would like to contribute, please fork my [repository](http://github.com/chingor13/docusigner) and send me a pull request. Please include tests.
106
+
107
+ I have not implemented every API endpoint, as I do not have enough time right now.
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ require 'bundler'
2
+ require 'rake/testtask'
3
+ require 'rdoc/task'
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ task :default => :test
7
+ task :build => :test
8
+
9
+ Rake::TestTask.new do |t|
10
+ t.libs << "test"
11
+ t.test_files = FileList['test/**/*_test.rb']
12
+ t.verbose = true
13
+ end
14
+
15
+ Rake::RDocTask.new do |rd|
16
+ rd.main = "README.rdoc"
17
+ rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
18
+ rd.rdoc_dir = 'doc'
19
+ end
@@ -0,0 +1,19 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "docusigner"
3
+ s.version = "0.0.1"
4
+ s.description = "Unofficial gem for accessing the DocuSign REST API"
5
+ s.summary = "Unofficial gem for accessing the DocuSign REST API"
6
+ s.add_dependency "reactive_resource", ">= 0.7.2"
7
+ s.add_dependency "multipart-post"
8
+
9
+ s.add_development_dependency "shoulda"
10
+ s.add_development_dependency "webmock"
11
+
12
+ s.author = "Jeff Ching"
13
+ s.email = "jeff@chingr.com"
14
+ s.homepage = "http://github.com/chingor13/docusigner"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.require_paths = ["lib"]
19
+ end
@@ -0,0 +1,27 @@
1
+ module Docusigner
2
+ class Account < Docusigner::Base
3
+ attr_accessor :id
4
+
5
+ has_one :billing_plan
6
+ has_one :settings, :class_name => "Docusigner::Settings"
7
+
8
+ has_many :brands
9
+ has_many :custom_fields
10
+
11
+ # define this manually because the index action requires a from_date
12
+ def envelopes(from_date, options = {})
13
+ Docusigner::Envelope.find(:all, :params => options.reverse_merge({:account_id => id, :from_date => from_date}))
14
+ end
15
+
16
+ has_many :folders
17
+ has_many :groups
18
+ has_many :templates
19
+ has_many :users
20
+
21
+ def self.find_single(id, options)
22
+ super(id, options).tap do |r|
23
+ r.id = id
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,93 @@
1
+ require 'reactive_resource'
2
+ require 'docusigner/multipart'
3
+ module Docusigner
4
+ class Base < ReactiveResource::Base
5
+ self.site = "https://demo.docusign.net/restapi/v2"
6
+ self.format = :json
7
+ self.include_root_in_json = false
8
+
9
+ # allow you to attach documents
10
+ include Docusigner::Multipart::Resource
11
+
12
+ class << self
13
+ # we want to inherit headers for authentication
14
+ def headers
15
+ @headers ||= begin
16
+ superclass.respond_to?(:headers) ? superclass.headers.dup : {}
17
+ end
18
+ end
19
+
20
+ def connection(refresh = false)
21
+ if defined?(@connection) || self == Docusigner::Base
22
+ @connection = Docusigner::Connection.new(site, format) if refresh || @connection.nil? || !@connection.is_a?(Docusigner::Connection)
23
+ @connection.timeout = timeout if timeout
24
+ @connection.ssl_options = ssl_options if ssl_options
25
+ @connection
26
+ else
27
+ superclass.connection
28
+ end
29
+ end
30
+
31
+ def token=(token)
32
+ headers['Authorization'] = "Bearer #{token}"
33
+ end
34
+
35
+ def authorization=(options = {})
36
+ headers['X-DocuSign-Authentication'] = "<DocuSignCredentials><Username>%{username}</Username><Password>%{password}</Password><IntegratorKey>%{integrator_key}</IntegratorKey></DocuSignCredentials>" % options
37
+ end
38
+
39
+ private
40
+
41
+ # we want to automatically set the foreign keys if they are provided
42
+ def instantiate_record(record, prefix_options = {})
43
+ super(record, prefix_options).tap do |r|
44
+ prefix_options.each do |k, v|
45
+ if r.respond_to?("#{k}=")
46
+ r.send("#{k}=", v)
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ # some of DocuSign's responses contain metadata about the response (e.g. number of records returned)
53
+ def instantiate_collection(data, prefix_options = {})
54
+ if data.is_a?(Hash)
55
+ # if the data has the collection name as a root element, use that to build the records
56
+ if data.has_key?(collection_name)
57
+ super(data[collection_name], prefix_options)
58
+ else
59
+ instantiate_flattened_collection(data, prefix_options)
60
+ end
61
+ else
62
+ super(data, prefix_options)
63
+ end
64
+ end
65
+
66
+ def instantiate_flattened_collection(data, prefix_options)
67
+ flattened = []
68
+ data.each do |type, array|
69
+ array.each do |obj|
70
+ flattened << instantiate_record(obj.merge(:type => type), prefix_options)
71
+ end if array.is_a?(Array)
72
+ end
73
+ flattened
74
+ end
75
+ end
76
+
77
+ # the json should skip the root element
78
+ def to_json(opts = {})
79
+ as_json.to_json
80
+ end
81
+
82
+ protected
83
+
84
+ # we want any generated resources for generated models to extend from this base class
85
+ # mainly because we don't want the include the root in the json representation
86
+ def create_resource_for(resource_name)
87
+ resource = self.class.const_set(resource_name, Class.new(Docusigner::Base))
88
+ resource.prefix = self.class.prefix
89
+ resource.site = self.class.site
90
+ resource
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,8 @@
1
+ module Docusigner
2
+ class BillingPlan < Docusigner::Base
3
+ singleton
4
+
5
+ belongs_to :account
6
+ end
7
+ end
8
+
@@ -0,0 +1,5 @@
1
+ module Docusigner
2
+ class Brand < Docusigner::Base
3
+ belongs_to :account
4
+ end
5
+ end
@@ -0,0 +1,24 @@
1
+ module Docusigner
2
+ class Connection < ActiveResource::Connection
3
+ def post(path, body = '', headers = {})
4
+ if body.is_a?(Array)
5
+ with_auth do
6
+ req = Docusigner::Multipart::Post.new(path, body, build_request_headers(headers, :post, self.site.merge(path)))
7
+ handle_response(http.request(req))
8
+ end
9
+ else
10
+ super(path, body, headers)
11
+ end
12
+ end
13
+
14
+ def put(path, body = '', headers = {})
15
+ if body.is_a?(Array)
16
+ req = Docusigner::Multipart::Put.new(path, body, headers)
17
+ with_auth { request(:request, req, build_request_headers(headers, :put, self.site.merge(path))) }
18
+ else
19
+ super(path, body, headers)
20
+ end
21
+ end
22
+ end
23
+ end
24
+
@@ -0,0 +1,5 @@
1
+ module Docusigner
2
+ class CustomField < Docusigner::Base
3
+ belongs_to :account
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Docusigner
2
+ class Document < Docusigner::Base
3
+ belongs_to :envelope
4
+ end
5
+ end
@@ -0,0 +1,55 @@
1
+ module Docusigner
2
+ class Envelope < Docusigner::Base
3
+ module Status
4
+ CREATED = "created"
5
+ DELETED = "deleted"
6
+ SENT = "sent"
7
+ DELIVERED = "delivered"
8
+ SIGNED = "signed"
9
+ COMPLETED = "completed"
10
+ DECLINED = "declined"
11
+ VOIDED = "voided"
12
+ TIMED_OUT = "timedout"
13
+ AUTHORITATIVE_COPY = "authoritativecopy"
14
+ TRANSFER_COMPLETED = "transfercompleted"
15
+ TEMPLATE = "template"
16
+ CORRECT = "correct"
17
+ end
18
+
19
+ belongs_to :account
20
+
21
+ has_many :documents
22
+ has_many :recipients
23
+
24
+ def id
25
+ attributes["envelopeId"]
26
+ end
27
+
28
+ def send!
29
+ update_attribute(:status, Docusigner::Envelope::Status::SENT)
30
+ end
31
+
32
+ def void!(reason)
33
+ update_attributes({
34
+ :status => Docusigner::Envelope::Status::VOIDED,
35
+ :voidReason => reason
36
+ })
37
+ end
38
+
39
+ def recipient_url(params = {})
40
+ resp = post("views/recipient", prefix_options, params.to_json)
41
+ self.class.format.decode(resp.body)
42
+ end
43
+
44
+ def sender_url(params = {})
45
+ resp = post("views/sender", prefix_options, params.to_json)
46
+ self.class.format.decode(resp.body)
47
+ end
48
+
49
+ def correct_url(params = {})
50
+ resp = post("views/correct", prefix_options, params.to_json)
51
+ self.class.format.decode(resp.body)
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,5 @@
1
+ module Docusigner
2
+ class Folder < Docusigner::Base
3
+ belongs_to :account
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Docusigner
2
+ class Group < Docusigner::Base
3
+ belongs_to :account
4
+ end
5
+ end
@@ -0,0 +1,16 @@
1
+ module Docusigner
2
+ class LoginInformation < Docusigner::Base
3
+ singleton
4
+
5
+ class << self
6
+ def change_password(email, current_password, new_password, options = {})
7
+ body = {
8
+ "currentPassword" => current_password,
9
+ "email" => email,
10
+ "newPassword" => new_password
11
+ }.merge(options).to_json
12
+ resp = put(:password, {}, body)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,122 @@
1
+ require 'net/http/post/multipart'
2
+ require 'active_support/all'
3
+
4
+ # this class leverages some parts of the multipart-post gem,
5
+ # but we need to change some headers
6
+ module Docusigner
7
+ module Multipart
8
+ module Multipartable
9
+ DEFAULT_BOUNDARY = "-----------RubyMultipartPost"
10
+ def initialize(path, params, headers={}, opts = {})
11
+ boundary = opts[:boundary] || DEFAULT_BOUNDARY
12
+ super(path, headers)
13
+ parts = params.map {|v| Docusigner::Multipart::Parts.build(boundary, v, opts)}
14
+ parts << ::Parts::EpiloguePart.new(boundary)
15
+ ios = parts.map{|p| p.to_io }
16
+ self.set_content_type("multipart/form-data", { "boundary" => boundary })
17
+ self.content_length = parts.inject(0) {|sum,i| sum + i.length }
18
+ self.body_stream = CompositeReadIO.new(*ios)
19
+ end
20
+ end
21
+
22
+ module Parts
23
+ def self.build(boundary, value, opts = {})
24
+ if value.is_a?(Array)
25
+ self.build(boundary, value.first, opts.merge(value.last))
26
+ elsif value.is_a?(String)
27
+ DataPart.new(boundary, value, opts)
28
+ elsif value.is_a?(UploadIO)
29
+ DocumentPart.new(boundary, value, opts)
30
+ else
31
+ DataPart.new(boundary, value, opts)
32
+ end
33
+ end
34
+
35
+ class DataPart < StringIO
36
+ def initialize(boundary, data, opts = {})
37
+ @format = opts[:format] || :json
38
+ @content_type = {
39
+ :json => "application/json",
40
+ :xml => "application/xml"
41
+ }[@format]
42
+ super(build(boundary, data, opts))
43
+ end
44
+ def to_io
45
+ self
46
+ end
47
+ protected
48
+ def build(boundary, value, opts = {})
49
+ [
50
+ "--#{boundary}",
51
+ "Content-Type: #{@content_type}",
52
+ "Content-Disposition: form-data",
53
+ "",
54
+ value,
55
+ ""
56
+ ].join("\r\n")
57
+ end
58
+ end
59
+
60
+ class DocumentPart
61
+ attr_reader :length
62
+ def initialize(boundary, upload_io, opts = {})
63
+ @upload_io = upload_io
64
+ head = build(boundary, @upload_io, opts)
65
+ foot = "\r\n"
66
+ @output_io = CompositeReadIO.new(StringIO.new(head), @upload_io.io, StringIO.new(foot))
67
+ @length = head.length + file_length + foot.length
68
+ end
69
+ def to_io
70
+ @output_io
71
+ end
72
+ protected
73
+
74
+ def file_length
75
+ @file_length ||= @upload_io.respond_to?(:length) ? @upload_io.length : File.size(@upload_io.local_path)
76
+ end
77
+
78
+ def build(boundary, io, opts = {})
79
+ [
80
+ "--#{boundary}",
81
+ %(Content-Type: #{io.content_type}),
82
+ %(Content-Disposition: file; filename="#{opts[:name]}"; documentId=#{opts[:document_id]}),
83
+ %(Content-Length: #{file_length}),
84
+ "",
85
+ ""
86
+ ].join("\r\n")
87
+ end
88
+ end
89
+ end
90
+
91
+ class Post < Net::HTTP::Post
92
+ include Docusigner::Multipart::Multipartable
93
+ end
94
+
95
+ class Put < Net::HTTP::Put
96
+ include Docusigner::Multipart::Multipartable
97
+ end
98
+
99
+ module Resource
100
+ def add_document(file, document_id)
101
+ @documents ||= []
102
+ @documents << [file, {:document_id => document_id}]
103
+ end
104
+
105
+ def encode
106
+ if documents.present?
107
+ [super, *documents]
108
+ else
109
+ super
110
+ end
111
+ end
112
+
113
+ protected
114
+
115
+ def documents
116
+ @documents ||= []
117
+ end
118
+ end
119
+
120
+ end
121
+ end
122
+
@@ -0,0 +1,24 @@
1
+ module Docusigner
2
+ class Oauth2 < Docusigner::Base
3
+ singleton
4
+
5
+ class << self
6
+ def token(username, password, integrator_key)
7
+ @headers = {
8
+ "Accept" => "application/json",
9
+ "Content-Type" => "application/x-www-form-urlencoded"
10
+ }
11
+ body = "grant_type=password&client_id=#{integrator_key}&username=#{username}&password=#{password}&scope=api"
12
+ resp = post(:token, {}, body)
13
+ format.decode(resp.body)["access_token"]
14
+ end
15
+
16
+ def revoke(token)
17
+ @headers = {
18
+ "Authorization" => "Bearer #{token}"
19
+ }
20
+ post(:revoke, {}, "")
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ module Docusigner
2
+ class Recipient < Docusigner::Base
3
+ module Status
4
+ CREATED = "created"
5
+ SENT = "sent"
6
+ DELIVERED = "delivered"
7
+ SIGNED = "signed"
8
+ DECLINED = "declined"
9
+ COMPLETED = "completed"
10
+ FAX_PENDING = "faxpending"
11
+ AUTORESPONDED = "autoresponded"
12
+ end
13
+
14
+ belongs_to :envelope
15
+ has_many :tabs
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ module Docusigner
2
+ class Settings < Docusigner::Base
3
+ singleton
4
+
5
+ belongs_to :account
6
+
7
+ def [](setting)
8
+ as = accountSettings.detect{|as| as.name == setting}
9
+ as ? as.attributes["value"] : nil
10
+ end
11
+
12
+ class << self
13
+ def instantiate_record(record, prefix_options)
14
+ super({
15
+ :accountSettings => record,
16
+ :account_id => prefix_options[:account_id]
17
+ }, prefix_options)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ module Docusigner
2
+ class Tab < Docusigner::Base
3
+ belongs_to :recipient
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Docusigner
2
+ class Template < Docusigner::Base
3
+ belongs_to :account
4
+ end
5
+ end
@@ -0,0 +1,29 @@
1
+ module Docusigner
2
+ class User < Docusigner::Base
3
+ belongs_to :account
4
+
5
+ # DocuSign does not permit this endpoint
6
+ def update
7
+ raise "Not permitted"
8
+ end
9
+
10
+ # the create endpoint requires attributes to be nested under newUsers
11
+ def as_json
12
+ { "newUsers" => [super] }
13
+ end
14
+
15
+ protected
16
+
17
+ def id_from_response(response)
18
+ json = JSON.parse(response.body)
19
+ json["newUsers"].first["userId"]
20
+ end
21
+
22
+ def load(attributes, remove_root = false)
23
+ if attributes.is_a?(Array)
24
+ attributes = attributes.first
25
+ end
26
+ super(attributes, remove_root)
27
+ end
28
+ end
29
+ end
data/lib/docusigner.rb ADDED
@@ -0,0 +1,25 @@
1
+ require 'docusigner/base'
2
+ module Docusigner
3
+ # extensions
4
+ autoload :Connection, "docusigner/connection"
5
+ autoload :Multipart, "docusigner/multipart"
6
+
7
+ # REST models
8
+ autoload :Account, "docusigner/account"
9
+ autoload :BillingPlan, "docusigner/billing_plan"
10
+ autoload :Brand, "docusigner/brand"
11
+ autoload :CustomField, "docusigner/custom_field"
12
+ autoload :Document, "docusigner/document"
13
+ autoload :Envelope, "docusigner/envelope"
14
+ autoload :Folder, "docusigner/folder"
15
+ autoload :Group, "docusigner/group"
16
+ autoload :LoginInformation, "docusigner/login_information"
17
+ autoload :Recipient, "docusigner/recipient"
18
+ autoload :Settings, "docusigner/settings"
19
+ autoload :Tab, "docusigner/tab"
20
+ autoload :Template, "docusigner/template"
21
+ autoload :User, "docusigner/user"
22
+
23
+ # other models
24
+ autoload :Oauth2, "docusigner/oauth2"
25
+ end
data/test/test.pdf ADDED
Binary file