metaforce 0.5.3 → 1.0.0a

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. data/.gitignore +1 -0
  2. data/.rspec +1 -0
  3. data/Gemfile +1 -11
  4. data/LICENSE +22 -0
  5. data/README.md +91 -96
  6. data/Rakefile +6 -14
  7. data/examples/example.rb +51 -0
  8. data/lib/metaforce/abstract_client.rb +76 -0
  9. data/lib/metaforce/client.rb +27 -0
  10. data/lib/metaforce/config.rb +41 -19
  11. data/lib/metaforce/job/crud.rb +13 -0
  12. data/lib/metaforce/job/deploy.rb +87 -0
  13. data/lib/metaforce/job/retrieve.rb +92 -0
  14. data/lib/metaforce/job.rb +183 -0
  15. data/lib/metaforce/login.rb +39 -0
  16. data/lib/metaforce/manifest.rb +18 -93
  17. data/lib/metaforce/metadata/client/crud.rb +86 -0
  18. data/lib/metaforce/metadata/client/file.rb +113 -0
  19. data/lib/metaforce/metadata/client.rb +7 -225
  20. data/lib/metaforce/services/client.rb +45 -86
  21. data/lib/metaforce/version.rb +1 -1
  22. data/lib/metaforce.rb +27 -7
  23. data/metaforce.gemspec +19 -16
  24. data/spec/fixtures/package.xml +1 -1
  25. data/spec/fixtures/payload.zip +0 -0
  26. data/spec/fixtures/requests/{describe_layout → foo}/invalid_session.xml +0 -0
  27. data/spec/fixtures/requests/send_email/success.xml +1 -0
  28. data/spec/lib/client_spec.rb +34 -0
  29. data/spec/lib/config_spec.rb +8 -50
  30. data/spec/lib/job/deploy_spec.rb +53 -0
  31. data/spec/lib/job/retrieve_spec.rb +28 -0
  32. data/spec/lib/job_spec.rb +95 -0
  33. data/spec/lib/login_spec.rb +18 -0
  34. data/spec/lib/manifest_spec.rb +22 -168
  35. data/spec/lib/metadata/client_spec.rb +84 -179
  36. data/spec/lib/metaforce_spec.rb +20 -0
  37. data/spec/lib/services/client_spec.rb +22 -35
  38. data/spec/spec_helper.rb +24 -3
  39. data/spec/support/client.rb +38 -0
  40. data/wsdl/26.0/metadata.xml +4750 -0
  41. data/wsdl/26.0/partner.xml +3340 -0
  42. metadata +114 -77
  43. data/Guardfile +0 -9
  44. data/bin/metaforce +0 -6
  45. data/lib/metaforce/core_extensions/string.rb +0 -31
  46. data/lib/metaforce/core_extensions.rb +0 -1
  47. data/lib/metaforce/custom_actions.rb +0 -29
  48. data/lib/metaforce/error.rb +0 -3
  49. data/lib/metaforce/login_details.rb +0 -28
  50. data/lib/metaforce/metadata/crud.rb +0 -103
  51. data/lib/metaforce/metadata/file.rb +0 -74
  52. data/lib/metaforce/metadata/transaction.rb +0 -100
  53. data/lib/metaforce/metadata.rb +0 -4
  54. data/lib/metaforce/rake/deploy.rb +0 -35
  55. data/lib/metaforce/rake/retrieve.rb +0 -39
  56. data/lib/metaforce/rake/tests.rb +0 -62
  57. data/lib/metaforce/rake.rb +0 -43
  58. data/lib/metaforce/services.rb +0 -1
  59. data/lib/metaforce/tasks/README.md +0 -62
  60. data/lib/metaforce/tasks/metaforce.rake +0 -5
  61. data/lib/metaforce/thor/metaforce.rb +0 -117
  62. data/lib/metaforce/types.rb +0 -249
  63. data/spec/.gitignore +0 -1
  64. data/spec/fixtures/sample/Rakefile +0 -2
  65. data/spec/fixtures/sample/metaforce.yml +0 -13
  66. data/spec/fixtures/sample/src/classes/TestClass.cls +0 -2
  67. data/spec/fixtures/sample/src/classes/TestClass.cls-meta.xml +0 -5
  68. data/spec/fixtures/sample/src/package.xml +0 -8
  69. data/spec/lib/core_extensions/string_spec.rb +0 -23
  70. data/spec/lib/metadata/crud_spec.rb +0 -66
  71. data/spec/lib/metadata/file_spec.rb +0 -17
  72. data/spec/lib/metadata/transaction_spec.rb +0 -68
@@ -0,0 +1,92 @@
1
+ module Metaforce
2
+ class Job::Retrieve < Job
3
+
4
+ # Public: Instantiate a new retrieve job.
5
+ #
6
+ # Examples
7
+ #
8
+ # job = Metaforce::Job::Retrieve.new(client)
9
+ # # => #<Metaforce::Job::Retrieve @id=nil>
10
+ #
11
+ # Returns self.
12
+ def initialize(client, options={})
13
+ super(client)
14
+ @options = options
15
+ end
16
+
17
+ # Public: Perform the job.
18
+ #
19
+ # Examples
20
+ #
21
+ # job = Metaforce::Job::Retrieve.new(client)
22
+ # job.perform
23
+ # # => #<Metaforce::Job::Retrieve @id='1234'>
24
+ #
25
+ # Returns self.
26
+ def perform
27
+ @id = client._retrieve(@options).id
28
+ super
29
+ end
30
+
31
+ # Public: Get the detailed status of the retrieve.
32
+ #
33
+ # Examples
34
+ #
35
+ # job.result
36
+ # # => { :id => '1234', :zip_file => '<base64 encoded content>', ... }
37
+ #
38
+ # Returns the RetrieveResult (http://www.salesforce.com/us/developer/docs/api_meta/Content/meta_retrieveresult.htm).
39
+ def result
40
+ client.status(id, :retrieve)
41
+ end
42
+
43
+ # Public: Decodes the content of the returned zip file.
44
+ #
45
+ # Examples
46
+ #
47
+ # job.zip_file
48
+ # # => '<binary content>'
49
+ #
50
+ # Returns the decoded content.
51
+ def zip_file
52
+ Base64.decode64(result.zip_file)
53
+ end
54
+
55
+ # Public: Unzips the returned zip file to the location.
56
+ #
57
+ # destination - Path to extract the contents to.
58
+ #
59
+ # Examples
60
+ #
61
+ # job.extract_to('./path')
62
+ # # => #<Metaforce::Job::Retrieve @id='1234'>
63
+ #
64
+ # Returns self.
65
+ def extract_to(destination)
66
+ return on_complete { |job| job.extract_to(destination) } unless @id
67
+ Zip::ZipFile.open(tmp_zip_file) do |zip|
68
+ zip.each do |f|
69
+ path = File.join(destination, f.name)
70
+ FileUtils.mkdir_p(File.dirname(path))
71
+ zip.extract(f, path) { true }
72
+ end
73
+ end
74
+ self
75
+ end
76
+
77
+ private
78
+
79
+ # Internal: Writes the zip file content to a temporary location so it can
80
+ # be extracted.
81
+ def tmp_zip_file
82
+ @tmp_zip_file ||= begin
83
+ file = Tempfile.new('retrieve')
84
+ file.write(zip_file)
85
+ path = file.path
86
+ file.close
87
+ path
88
+ end
89
+ end
90
+
91
+ end
92
+ end
@@ -0,0 +1,183 @@
1
+ require 'zip/zip'
2
+ require 'base64'
3
+
4
+ module Metaforce
5
+ class Job
6
+ autoload :Deploy, 'metaforce/job/deploy'
7
+ autoload :Retrieve, 'metaforce/job/retrieve'
8
+ autoload :CRUD, 'metaforce/job/crud'
9
+
10
+ # Public: The id of the AsyncResult returned from Salesforce for
11
+ # this job.
12
+ attr_reader :id
13
+
14
+ # Public: Instantiate a new job. Doesn't actually do anything until
15
+ # .perform is called.
16
+ #
17
+ # Examples
18
+ #
19
+ # job = Metaforce::Job.new(client)
20
+ # # => #<Metaforce::Job @id=nil>
21
+ #
22
+ # Returns self.
23
+ def initialize(client)
24
+ @_callbacks = Hash.new { |h,k| h[k] = [] }
25
+ @client = client
26
+ end
27
+
28
+ # Public: Perform the job.
29
+ #
30
+ # Examples
31
+ #
32
+ # job = Metaforce::Job.new
33
+ # job.perform
34
+ # # => #<Metaforce::Job @id=nil>
35
+ #
36
+ # Returns self.
37
+ def perform
38
+ start_heart_beat
39
+ self
40
+ end
41
+
42
+ # Public: Register a block to be called when the job has completed.
43
+ #
44
+ # Yields the job.
45
+ #
46
+ # &block - Proc or Lambda to be run when the job completes.
47
+ #
48
+ # Examples
49
+ #
50
+ # job.on_complete do |job|
51
+ # puts "Job ##{job.id} completed!"
52
+ # end
53
+ #
54
+ # Returns self.
55
+ def on_complete(&block)
56
+ @_callbacks[:on_complete] << block
57
+ self
58
+ end
59
+
60
+ # Public: Register a block to be called when if the job fails.
61
+ #
62
+ # Yields the job.
63
+ #
64
+ # &block - Proc or Lambda to be run when the job fails.
65
+ #
66
+ # Examples
67
+ #
68
+ # job.on_error do |job|
69
+ # puts "Job ##{job.id} failed!"
70
+ # end
71
+ #
72
+ # Returns self.
73
+ def on_error(&block)
74
+ @_callbacks[:on_error] << block
75
+ self
76
+ end
77
+
78
+ # Public: Queries the job status from the API.
79
+ #
80
+ # Examples
81
+ #
82
+ # job.status
83
+ # # => { :id => '1234', :done => false, ... }
84
+ #
85
+ # Returns the AsyncResult (http://www.salesforce.com/us/developer/docs/api_meta/Content/meta_asyncresult.htm).
86
+ def status
87
+ client.status(id)
88
+ end
89
+
90
+ # Public: Returns true if the job has completed.
91
+ #
92
+ # Examples
93
+ #
94
+ # job.done
95
+ # # => true
96
+ #
97
+ # Returns true if the job has completed, false otherwise.
98
+ def done?
99
+ status.done
100
+ end
101
+
102
+ # Public: Returns the state if the job has finished processing.
103
+ #
104
+ # Examples
105
+ #
106
+ # job.state
107
+ # # => 'Completed'
108
+ #
109
+ # Returns the state if the job is done, false otherwise.
110
+ def state
111
+ done? && status.state
112
+ end
113
+
114
+ # Public: Check if the job is in a given state.
115
+ #
116
+ # Examples
117
+ #
118
+ # job.queued?
119
+ # # => false
120
+ #
121
+ # Returns true or false.
122
+ #
123
+ # Signature
124
+ #
125
+ # queued?
126
+ # in_progress?
127
+ # completed?
128
+ # error?
129
+ %w[Queued InProgress Completed Error].each do |state|
130
+ define_method :"#{state.underscore}?" do; self.state == state end
131
+ end
132
+
133
+ def inspect
134
+ "#<#{self.class} @id=#{@id.inspect}>"
135
+ end
136
+
137
+ class << self
138
+
139
+ # Internal: Disable threading in tests.
140
+ def disable_threading!
141
+ self.class_eval do
142
+ def start_heart_beat
143
+ loop do
144
+ trigger_callbacks && break if completed? || error?
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ end
151
+
152
+ private
153
+ attr_reader :client
154
+
155
+ # Internal: Starts a heart beat in a thread, which polls the job status
156
+ # until it has completed or timed out.
157
+ def start_heart_beat
158
+ Thread.abort_on_exception = true
159
+ @heart_beat ||= Thread.new do
160
+ delay = 1
161
+ loop do
162
+ sleep (delay = delay * 2)
163
+ trigger_callbacks && Thread.stop if completed? || error?
164
+ end
165
+ end
166
+ end
167
+
168
+ def trigger_callbacks
169
+ @_callbacks[callback_type].each do |block|
170
+ block.call(self)
171
+ end
172
+ end
173
+
174
+ def callback_type
175
+ if completed?
176
+ :on_complete
177
+ elsif error?
178
+ :on_error
179
+ end
180
+ end
181
+
182
+ end
183
+ end
@@ -0,0 +1,39 @@
1
+ module Metaforce
2
+ class Login
3
+ def initialize(username, password, security_token=nil)
4
+ @username, @password, @security_token = username, password, security_token
5
+ end
6
+
7
+ # Public: Perform the login request.
8
+ #
9
+ # Returns a hash with the session id and server urls.
10
+ def login
11
+ response = client.request(:login) do
12
+ soap.body = {
13
+ :username => username,
14
+ :password => password
15
+ }
16
+ end
17
+ response.body[:login_response][:result]
18
+ end
19
+
20
+ private
21
+
22
+ # Internal: Savon client.
23
+ def client
24
+ @client ||= Savon.client(Metaforce.configuration.partner_wsdl) do |wsdl|
25
+ wsdl.endpoint = Metaforce.configuration.endpoint
26
+ end.tap { |client| client.http.auth.ssl.verify_mode = :none }
27
+ end
28
+
29
+ # Internal: Usernamed passed in from options.
30
+ def username
31
+ @username
32
+ end
33
+
34
+ # Internal: Password + Security Token combined.
35
+ def password
36
+ [@password, @security_token].join('')
37
+ end
38
+ end
39
+ end
@@ -1,11 +1,11 @@
1
1
  require 'nokogiri'
2
- require 'metaforce/types'
2
+ require 'active_support/core_ext'
3
3
 
4
4
  module Metaforce
5
- class Manifest
5
+ class Manifest < Hash
6
6
 
7
- # Initializes a new instance of a manifest (package.xml) file.
8
- #
7
+ # Public: Initializes a new instance of a manifest (package.xml) file.
8
+ #
9
9
  # It can either take a hash:
10
10
  # {
11
11
  # :apex_class => [
@@ -35,71 +35,17 @@ module Metaforce
35
35
  # </types>
36
36
  # <version>23.0</version>
37
37
  # </Package>
38
- #
38
+ #
39
39
  def initialize(components={})
40
- # Map component type => folder
40
+ self.replace Hash.new { |h,k| h[k] = [] }
41
41
  if components.is_a?(Hash)
42
- @components = components
42
+ self.merge!(components)
43
43
  elsif components.is_a?(String)
44
- @components = {}
45
44
  self.parse(components)
46
45
  end
47
46
  end
48
47
 
49
- # Adds components to the package
50
- #
51
- # manifest.add :apex_class, 'SomeClass'
52
- def add(type, members=nil)
53
- unless members.nil?
54
- @components[type] = [] if @components[type].nil?
55
- members = [members] if members.is_a?(String)
56
- members.each do |member|
57
- member = member.gsub(/.*\//, '').gsub(/\..*/, '');
58
- @components[type].push(member)
59
- end
60
- end
61
- self
62
- end
63
-
64
- # Removes components from the package
65
- #
66
- # manifest.remove :apex_class, 'SomeClass'
67
- def remove(type, members=nil)
68
- unless members.nil?
69
- members = [members] if members.is_a?(String)
70
- members.each do |member|
71
- member = member.gsub(/.*\//, '').gsub(/\..*/, '');
72
- @components[type].delete(member)
73
- end
74
- end
75
- if @components[type].empty?
76
- @components.delete(type)
77
- end
78
- self
79
- end
80
-
81
- # Filters the components based on a list of files
82
- #
83
- # manifest.only(['classes/SomeClass'])
84
- def only(files)
85
- components = @components
86
- @components = {}
87
- files.each do |file|
88
- parts = file.split('/').last(2)
89
- folder = parts[0]
90
- file = parts[1].gsub(/.*\//, '').gsub(/\..*/, '')
91
- components.each_key do |type|
92
- if Metaforce::Metadata::Types.folder(type) =~ /#{folder}/i
93
- unless components[type].index(file).nil?
94
- self.add(type, file);
95
- end
96
- end
97
- end
98
- end
99
- self
100
- end
101
-
102
- # Returns a string containing a package.xml file
48
+ # Public: Returns a string containing a package.xml file
103
49
  #
104
50
  # <?xml version="1.0"?>
105
51
  # <Package xmlns="http://soap.sforce.com/2006/04/metadata">
@@ -120,13 +66,13 @@ module Metaforce
120
66
  # </Package>
121
67
  def to_xml
122
68
  xml_builder = Nokogiri::XML::Builder.new do |xml|
123
- xml.Package("xmlns" => "http://soap.sforce.com/2006/04/metadata") {
124
- @components.each do |key, members|
69
+ xml.Package('xmlns' => 'http://soap.sforce.com/2006/04/metadata') {
70
+ self.each do |key, members|
125
71
  xml.types {
126
72
  members.each do |member|
127
73
  xml.members member
128
74
  end
129
- xml.name Metaforce::Metadata::Types.name(key)
75
+ xml.name key.to_s.camelize
130
76
  }
131
77
  end
132
78
  xml.version Metaforce.configuration.api_version
@@ -135,43 +81,22 @@ module Metaforce
135
81
  xml_builder.to_xml
136
82
  end
137
83
 
138
- # Returns the underlying hash structure
139
- #
140
- # {
141
- # :apex_class => [
142
- # "TestController",
143
- # "TestClass"
144
- # ],
145
- # :apex_component => [
146
- # "SiteLogin"
147
- # ]
148
- # }
149
- def to_hash
150
- @components
151
- end
152
-
153
- # Used internall my Metaforce::Metadata::Client
84
+ # Public: Converts the manifest into a format that can be used by the
85
+ # metadata api.
154
86
  def to_package
155
- components = []
156
- @components.each do |type, members|
157
- name = Metaforce::Metadata::Types.name(type)
158
- components.push :members => members, :name => name
87
+ self.map do |type, members|
88
+ { :members => members, :name => type.to_s.camelize }
159
89
  end
160
- components
161
90
  end
162
91
 
163
- # Parses a package.xml file
92
+ # Public: Parses a package.xml file
164
93
  def parse(file)
165
94
  document = Nokogiri::XML(file).remove_namespaces!
166
95
  document.xpath('//types').each do |type|
167
96
  name = type.xpath('name').first.content
168
- key = Metaforce::Metadata::Types.key(name);
97
+ key = name.underscore.to_sym
169
98
  type.xpath('members').each do |member|
170
- if @components[key].is_a?(Array)
171
- @components[key].push(member.content)
172
- else
173
- @components[key] = [member.content]
174
- end
99
+ self[key] << member.content
175
100
  end
176
101
  end
177
102
  self
@@ -0,0 +1,86 @@
1
+ module Metaforce
2
+ module Metadata
3
+ class Client
4
+ module CRUD
5
+
6
+ # Public: Create metadata
7
+ #
8
+ # Examples
9
+ #
10
+ # client._create(:apex_page, :full_name => 'TestPage', label: 'Test page', :content => '<apex:page>foobar</apex:page>')
11
+ def _create(type, metadata={})
12
+ type = type.to_s.camelize
13
+ request :create do |soap|
14
+ soap.body = {
15
+ :metadata => prepare(metadata)
16
+ }.merge(attributes!(type))
17
+ end
18
+ end
19
+
20
+ # Public: Delete metadata
21
+ #
22
+ # Examples
23
+ #
24
+ # client._delete(:apex_component, 'Component')
25
+ def _delete(type, *args)
26
+ type = type.to_s.camelize
27
+ metadata = args.map { |full_name| {:full_name => full_name} }
28
+ request :delete do |soap|
29
+ soap.body = {
30
+ :metadata => metadata
31
+ }.merge(attributes!(type))
32
+ end
33
+ end
34
+
35
+ # Public: Update metadata
36
+ #
37
+ # Examples
38
+ #
39
+ # client._update(:apex_page, 'OldPage', :full_name => 'NewPage')
40
+ def _update(type, current_name, metadata={})
41
+ type = type.to_s.camelize
42
+ request :update do |soap|
43
+ soap.body = {
44
+ :metadata => {
45
+ :current_name => current_name,
46
+ :metadata => prepare(metadata),
47
+ :attributes! => { :metadata => { 'xsi:type' => "ins0:#{type}" } }
48
+ }
49
+ }
50
+ end
51
+ end
52
+
53
+ def create(*args)
54
+ Job::CRUD.new(self, :_create, args)
55
+ end
56
+
57
+ def update(*args)
58
+ Job::CRUD.new(self, :_update, args)
59
+ end
60
+
61
+ def delete(*args)
62
+ Job::CRUD.new(self, :_delete, args)
63
+ end
64
+
65
+ private
66
+
67
+ def attributes!(type)
68
+ {:attributes! => { 'ins0:metadata' => { 'xsi:type' => "ins0:#{type}" } }}
69
+ end
70
+
71
+ # Internal: Prepare metadata by base64 encoding any content keys.
72
+ def prepare(metadata)
73
+ metadata = [metadata] unless metadata.is_a? Array
74
+ metadata.each { |m| encode_content(m) }
75
+ metadata
76
+ end
77
+
78
+ # Internal: Base64 encodes any :content keys.
79
+ def encode_content(metadata)
80
+ metadata[:content] = Base64.encode64(metadata[:content]) if metadata.has_key?(:content)
81
+ end
82
+
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,113 @@
1
+ module Metaforce
2
+ module Metadata
3
+ class Client
4
+ module File
5
+
6
+ # Public: Specify an array of component types to list.
7
+ #
8
+ # Examples
9
+ #
10
+ # # Get a list of apex classes on the server and output the names of each
11
+ # client.list_metadata('ApexClass').collect { |t| t.full_name }
12
+ # #=> ["al__SObjectPaginatorListenerForTesting", "al__IndexOutOfBoundsException", ... ]
13
+ #
14
+ # # Get a list of apex components and apex classes
15
+ # client.list_metadata('CustomObject', 'ApexComponent')
16
+ # #=> ["ContractContactRole", "Solution", "Invoice_Statements__c", ... ]
17
+ def list_metadata(*args)
18
+ queries = args.map(&:to_s).map(&:camelize).map { |t| {:type => t} }
19
+ request :list_metadata do |soap|
20
+ soap.body = { :queries => queries }
21
+ end
22
+ end
23
+
24
+ # Public: Describe the organization's metadata.
25
+ #
26
+ # version - API version (default: latest).
27
+ #
28
+ # Examples
29
+ #
30
+ # # List the names of all metadata types
31
+ # client.describe.metadata_objects.collect { |t| t.xml_name }
32
+ # #=> ["CustomLabels", "StaticResource", "Scontrol", "ApexComponent", ... ]
33
+ def describe(version=nil)
34
+ request :describe_metadata do |soap|
35
+ soap.body = { :api_version => version } unless version.nil?
36
+ end
37
+ end
38
+
39
+ # Public: Checks the status of an async result.
40
+ #
41
+ # ids - A list of ids to check.
42
+ # type - either :deploy or :retrieve
43
+ #
44
+ # Examples
45
+ #
46
+ # client.status('04sU0000000Wx6KIAS')
47
+ # #=> {:done=>true, :id=>"04sU0000000Wx6KIAS", :state=>"Completed", ...}
48
+ def status(ids, type=nil)
49
+ method = :check_status
50
+ method = :"check_#{type}_status" if type
51
+ ids = [ids] unless ids.respond_to?(:each)
52
+ request method do |soap|
53
+ soap.body = { :ids => ids }
54
+ end
55
+ end
56
+
57
+ # Public: Deploy code to Salesforce.
58
+ #
59
+ # zip_file - The base64 encoded contents of the zip file.
60
+ # options - Hash of DeployOptions.
61
+ #
62
+ # Returns the AsyncResult
63
+ def _deploy(zip_file, options={})
64
+ request :deploy do |soap|
65
+ soap.body = { :zip_file => zip_file, :deploy_options => options }
66
+ end
67
+ end
68
+
69
+ # Public: Retrieve code from Salesforce.
70
+ #
71
+ # Returns the AsyncResult
72
+ def _retrieve(options={})
73
+ request :retrieve do |soap|
74
+ soap.body = { :retrieve_request => options }
75
+ end
76
+ end
77
+
78
+ # Public: Deploy code to Salesforce.
79
+ #
80
+ # path - A path to a zip file, or a directory to deploy.
81
+ # options - Deploy options.
82
+ #
83
+ # Examples
84
+ #
85
+ # client.deploy(File.expand_path('./src'))
86
+ def deploy(path, options={})
87
+ Job::Deploy.new(self, path, options)
88
+ end
89
+
90
+ def retrieve(options={})
91
+ Job::Retrieve.new(self, options)
92
+ end
93
+
94
+ # Public: Retrieves files specified in the manifest file (A package.xml
95
+ # file).
96
+ def retrieve_unpackaged(manifest, options={})
97
+ package = if manifest.is_a?(Metaforce::Manifest)
98
+ manifest
99
+ elsif manifest.is_a?(String)
100
+ Metaforce::Manifest.new(File.open(manifest).read)
101
+ end
102
+ options = {
103
+ :api_version => Metaforce.configuration.api_version,
104
+ :single_package => true,
105
+ :unpackaged => { :types => package.to_package }
106
+ }.merge(options)
107
+ retrieve(options)
108
+ end
109
+
110
+ end
111
+ end
112
+ end
113
+ end