hoopla_salesforce 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,114 @@
1
+ hoopla_salesforce
2
+ =================
3
+
4
+ A nitfy bundle of dev and deployment tools for working on the (Sales)force.com platform.
5
+
6
+ Features
7
+ ========
8
+
9
+ 1. Generates new empty projects
10
+ 2. Deploys your project with Rake
11
+ 3. Generates *-meta.xml for any files that are missing it
12
+ 4. Runs tests on deploy and prints results in ANSI color
13
+ 5. Auto-packages folders in src/resource as staticresources
14
+ 6. Undeploys your project (destructiveChanges.xml is auto-generated)
15
+ 7. Pre-processes any VisualForce or Apex files with ERB
16
+ 8. Generates static test pages from VisualForce pages
17
+ 9. Runs tests via the Web UI (as results often differ from deploy tests)
18
+ 10. Makes pancakes (pending)
19
+
20
+ Usage
21
+ =====
22
+
23
+ Install it with:
24
+
25
+ (sudo) gem install hoopla_salesforce
26
+
27
+ There are a lot of features in here, so the rest of this document will go over how they work.
28
+
29
+ Generating new projects
30
+ -----------------------
31
+
32
+ Just run:
33
+
34
+ hoopla_salesforce init myproject
35
+
36
+ This will create an folder called myproject that looks like this:
37
+
38
+ - myproject
39
+ |- Rakefile
40
+ |- lib/
41
+ \- src/
42
+ |- applications/
43
+ |- classes/
44
+ |- objects/
45
+ |- pages/
46
+ |- resources/
47
+ |- tabs/
48
+ |- triggers/
49
+ \- package.xml
50
+
51
+ Once this is done, drop the enterprise.xml and metadata.xml for your org into `lib/` and update the `Rakefile` to reflect your username, password and security token. Now your project is ready to deploy. Run `rake -T` to show the available rake tasks.
52
+
53
+ Deploy your projects with rake
54
+ ------------------------------
55
+
56
+ Deploy your project using `rake hsf:deploy:development`. Or if you change the environment name of your DeployTask, use whatever you specified in place of 'development'.
57
+
58
+ If you need to see the full deployment output run it with `FULL_OUTPUT=true`. Currently coverage information is only available in the full output. The default output will include colorized information about what's been added, updated, tests run, any failures or compilation problems.
59
+
60
+ Generating meta.xml files
61
+ -------------------------
62
+
63
+ During deployment, we'll generate any missing meta.xml files for your assets. This currently supports generation for classes, pages, documents, static resources, and triggers. If you already have meta.xml files for your resources, the gem will skip those and include your files for deployment.
64
+
65
+ Autopackaging of static resources
66
+ ---------------------------------
67
+
68
+ As part of deployment, any folders in src/resources will get zipped up as static resources. So for example if you had a folder `src/resources/Performance` the contents of this folder will get zipped into `src/static/resources/Performance.resource`. This makes dealing with zipped CSS/JavaScript much easier.
69
+
70
+ Undeploying your project
71
+ ------------------------
72
+
73
+ To undeploy your project run `rake hsf:undeploy:development`. This will scan your src folder and generate the appropriate destructiveChanges.xml file used by Salesforce to undeploy your code.
74
+
75
+ Note that undeployed fields are still left in the "deleted fields" section of the site. Once you have two copies of a deleted field, you can't re-deploy that field without first cleaning up the deleted fields via the WebUI. Automatic erasing of fields is a planned feature to avoid this headache.
76
+
77
+ Pre-processing with ERB
78
+ -----------------------
79
+
80
+ If you append `.erb` to any file in your project, it will get processed through Erubis prior to deployment. This makes it possible to DRY up a lot of your XML, abstract common patterns and even write simple Apex macros.
81
+
82
+ Just before the templates are run, the deployer will look for `lib/template_helper.rb` and load that. In this file you can mix your own methods into the template processors. The following example allows you to use `<%= name 'MyObject' %>` in an object definition to avoid writing the XML boilerplate that's required by salesforce.
83
+
84
+ module GenericHelper
85
+ def object(&block)
86
+ code = <<-XML.margin
87
+ <?xml version="1.0" encoding="UTF-8"?>
88
+ <CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
89
+ <deploymentStatus>Deployed</deploymentStatus>
90
+ <sharingModel>ReadWrite</sharingModel>
91
+ #{capture &block}
92
+ </CustomObject>
93
+ XML
94
+ end
95
+ end
96
+ HooplaSalesforce::TemplateProcessor::Generic.send(:include, GenericHelper)
97
+
98
+ Note that you must currently mix your module into the processors at the bottom of the file. The currently available processors are:
99
+
100
+ * `HooplaSalesforce::TemplateProcessor::VisualForce` - used for processing page files
101
+ * `HooplaSalesforce::TemplateProcessor::TestPage` - used for generating static test pages
102
+ * `HooplaSalesforce::TemplateProcessor::Generic` - used for any other files
103
+
104
+ Generating Static Test Pages
105
+ ----------------------------
106
+
107
+ (documentation pending)
108
+
109
+ Running tests
110
+ -------------
111
+
112
+ Use WEB_TEST=true to run via web ui. Default will run during deployment. Use TEST_NAMES=Test1,Test2,etc to run specific tests (or TEST_NAMES="" to skip tests).
113
+
114
+ (more documentation pending)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'hoopla_salesforce/commands'
5
+ rescue LoadError
6
+ require 'rubygems'
7
+ require 'hoopla_salesforce/commands'
8
+ end
@@ -0,0 +1,21 @@
1
+ require 'hoopla_salesforce/info'
2
+ require 'commander/import'
3
+ require 'hoopla_salesforce/ext/commander'
4
+
5
+ program :name, HooplaSalesforce.name
6
+ program :version, HooplaSalesforce.version
7
+ program :description, HooplaSalesforce.summary
8
+
9
+ default_command :help
10
+
11
+ command "init" do |c|
12
+ c.syntax = '[options] <directory>'
13
+ c.summary = 'Creates a skeleton app.'
14
+ c.description = 'Creates a skeleton Salesforce.com app in the specified directory.'
15
+
16
+ c.action do |args, options|
17
+ directory = args.first or raise "Directory name required."
18
+ require 'hoopla_salesforce/skeleton'
19
+ HooplaSalesforce::Skeleton.new(directory).create
20
+ end
21
+ end
@@ -45,7 +45,7 @@ module HooplaSalesforce
45
45
  @header = { "wsdl:SessionHeader" => { "wsdl:sessionId" => response[:session_id] } }
46
46
  end
47
47
 
48
- def deploy(zipfile, options)
48
+ def deploy(zipfile, options = {})
49
49
  login
50
50
 
51
51
  data = Base64.encode64(File.read(zipfile))
@@ -0,0 +1,54 @@
1
+ require 'erubis'
2
+
3
+ module HooplaSalesforce
4
+ class OutputBuffer < String
5
+ alias :append= :<<
6
+ alias :safe_concat :<<
7
+ end
8
+
9
+ module CaptureHelper
10
+ def capture
11
+ @output_buffer, original_buffer = HooplaSalesforce::OutputBuffer.new, @output_buffer
12
+ yield
13
+ @output_buffer, buffer = original_buffer, @output_buffer
14
+ buffer
15
+ end
16
+ end
17
+
18
+ class Eruby < Erubis::Eruby
19
+ def add_preamble(src)
20
+ src << "@output_buffer = HooplaSalesforce::OutputBuffer.new;"
21
+ end
22
+
23
+ def add_text(src, text)
24
+ return if text.empty?
25
+ src << "@output_buffer.safe_concat('" << escape_text(text) << "');"
26
+ end
27
+
28
+ BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/
29
+
30
+ def add_expr_literal(src, code)
31
+ if code =~ BLOCK_EXPR
32
+ src << '@output_buffer.append= ' << code
33
+ else
34
+ src << '@output_buffer.append= (' << code << ');'
35
+ end
36
+ end
37
+
38
+ def add_stmt(src, code)
39
+ if code =~ BLOCK_EXPR
40
+ src << code
41
+ else
42
+ super
43
+ end
44
+ end
45
+
46
+ def add_expr_escaped(src, code)
47
+ src << '@output_buffer.append= ' << escaped_expr(code) << ';'
48
+ end
49
+
50
+ def add_postamble(src)
51
+ src << '@output_buffer.to_s'
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,7 @@
1
+ # Automatically include the executable and command name
2
+ # in the syntax statment
3
+ class Commander::Command
4
+ def syntax=(syntax)
5
+ @syntax = "#{HooplaSalesforce.name} #{@name} #{syntax}"
6
+ end
7
+ end
@@ -0,0 +1,14 @@
1
+ class String
2
+ def margin
3
+ spaces = match(/^\s*/)
4
+ gsub(/^#{spaces[0]}/, '')
5
+ end
6
+
7
+ def camelize(first_letter_in_uppercase = true)
8
+ if first_letter_in_uppercase
9
+ gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
10
+ else
11
+ first.downcase + camelize(lower_case_and_underscored_word)[1..-1]
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,19 @@
1
+ module HooplaSalesforce
2
+ class << self
3
+ def name
4
+ "hoopla_salesforce"
5
+ end
6
+
7
+ def summary
8
+ "Helpers for building Salesforce.com projects"
9
+ end
10
+
11
+ def description
12
+ summary
13
+ end
14
+
15
+ def version
16
+ "0.0.4"
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,130 @@
1
+ require 'hoopla_salesforce/utils'
2
+ require 'nokogiri'
3
+
4
+ module HooplaSalesforce
5
+ class PackageGenerator
6
+ include Utils
7
+
8
+ attr_reader :processed_src
9
+ attr_reader :api_version
10
+
11
+ # Right now this only works with making a destructiveChanges.xml
12
+ # Need to add support namespace, etc before using to generate package.xml
13
+ def initialize(processed_src, api_version)
14
+ @processed_src = processed_src
15
+ @api_version = api_version
16
+ end
17
+
18
+ def supported_types
19
+ %w(apex_class apex_page custom_tab custom_application apex_trigger custom_field
20
+ static_resource)
21
+ end
22
+
23
+ def map_files(glob, &block)
24
+ Dir["#{processed_src}/#{glob}"].map(&block)
25
+ end
26
+
27
+ def members_for_apex_class
28
+ map_files("classes/*.cls") do |klass|
29
+ data = File.read(klass)
30
+ extract_class_name(data)
31
+ end
32
+ end
33
+
34
+ def members_for_apex_trigger
35
+ map_files("triggers/*.trigger") do |trigger|
36
+ data = File.read(trigger)
37
+ extract_trigger_name(data)
38
+ end
39
+ end
40
+
41
+ def members_for_apex_page
42
+ map_files("pages/*.page") do |page|
43
+ File.basename(page, '.page')
44
+ end
45
+ end
46
+
47
+ def members_for_custom_tab
48
+ map_files("tabs/*.tab") do |tab|
49
+ File.basename(tab, '.tab')
50
+ end
51
+ end
52
+
53
+ def members_for_custom_application
54
+ map_files("applications/*.app") do |app|
55
+ File.read(app).match(/<fullName>([^<]*)<\/fullName>/)[1]
56
+ end
57
+ end
58
+
59
+ def members_for_custom_field
60
+ map_files("objects/*.object") do |object|
61
+ obj_name = File.basename(object, ".object")
62
+ data = File.read(object)
63
+ xml_doc = Nokogiri::XML(data)
64
+ xml_doc.search("fullName").map { |f| [obj_name, f.text].join('.') }
65
+ end.flatten
66
+ end
67
+
68
+ def members_for_static_resource
69
+ map_files("staticresources/*.resource") do |resource|
70
+ File.basename(resource, ".resource")
71
+ end
72
+ end
73
+
74
+ def empty_package_xml
75
+ <<-EOS.margin
76
+ <?xml version="1.0" encoding="UTF-8"?>
77
+ <Package xmlns="http://soap.sforce.com/2006/04/metadata">
78
+ <version>#{api_version}</version>
79
+ </Package>
80
+ EOS
81
+ end
82
+
83
+ def destructive_changes_xml
84
+ <<-EOS.margin
85
+ <?xml version="1.0" encoding="UTF-8"?>
86
+ <Package xmlns="http://soap.sforce.com/2006/04/metadata">
87
+ #{package_types}
88
+ <version>#{api_version}</version>
89
+ </Package>
90
+ EOS
91
+ end
92
+
93
+ def package_types
94
+ supported_types.map do |type|
95
+ members = send("members_for_#{type}")
96
+ unless members.empty?
97
+ members_xml = members.map { |m| "<members>#{m}</members>" }.join("\n")
98
+ <<-EOS.margin
99
+ <types>
100
+ #{members_xml}
101
+ <name>#{type.camelize}</name>
102
+ </types>
103
+ EOS
104
+ end
105
+ end.join("\n")
106
+ end
107
+
108
+ def generate_destructive_changes
109
+ write_xml "package.xml", empty_package_xml
110
+ write_xml "destructiveChanges.xml", destructive_changes_xml
111
+ remove_processed_files_for_undeploy
112
+ end
113
+
114
+ def write_xml(file, data)
115
+ File.open("#{processed_src}/#{file}", 'w') do |pkg|
116
+ pkg.print data
117
+ end
118
+ end
119
+
120
+ def protected_files_for_destruction
121
+ %W(#{processed_src}/package.xml #{processed_src}/destructiveChanges.xml)
122
+ end
123
+
124
+ def remove_processed_files_for_undeploy
125
+ Dir["#{processed_src}/*"].each do |file|
126
+ FileUtils.rm_rf(file) unless protected_files_for_destruction.include?(file)
127
+ end
128
+ end
129
+ end
130
+ end
@@ -1,4 +1,7 @@
1
1
  require 'hoopla_salesforce/rake/base_task'
2
+ require 'hoopla_salesforce/ext/string'
3
+ require 'hoopla_salesforce/template_processor'
4
+ require 'hoopla_salesforce/utils'
2
5
 
3
6
  module HooplaSalesforce
4
7
  module Rake
@@ -11,6 +14,8 @@ module HooplaSalesforce
11
14
  # - Zips up any folders found in src/resources as src/staticresources/#{folder}.resource.
12
15
  # This allows you to only keep the raw assets in your project
13
16
  class DeployTask < BaseTask
17
+ include Utils
18
+
14
19
  # Your project root. Defaults to 'src'
15
20
  attr_accessor :src
16
21
 
@@ -23,50 +28,195 @@ module HooplaSalesforce
23
28
  # the namespaced package) and a dev org (that does not have one).
24
29
  #
25
30
  # default - nil
26
- attr_accessor :namespace
31
+ attr_accessor :package_namespace
27
32
 
28
33
  # The directory in which to store processed source files.
29
34
  # Defaults to #{src}-processed
30
35
  attr_accessor :processed_src
31
36
 
32
- def initialize(name=:deploy)
33
- @deploy_file = "deploy.zip"
34
- @src = 'src'
35
- @namespace = nil
37
+ # Which file to load before processing templates. Default: lib/template_helper.rb
38
+ # To have this file inject methods into the HooplaSalesforce::TemplateProcessor,
39
+ # be sure to include your module in HooplaSalesforce::TemplateProcessor::VisualForce or
40
+ # HooplaSalesforce::TemplateProcessor::TestPage at the bottom of your template_helper
41
+ # file.
42
+ attr_accessor :template_helper
43
+
44
+ def initialize(name=:production)
45
+ @deploy_file = "deploy.zip"
46
+ @src = 'src'
47
+ @package_namespace = nil
48
+ @template_helper = 'lib/template_helper.rb'
36
49
  super
37
- @namespace += "__" if @namespace
38
- @processed_src ||= "#{src}-processed"
50
+ @package_namespace += "__" if @package_namespace
51
+ @processed_src ||= "#{src}-processed"
52
+ end
53
+
54
+ def clean_namespace
55
+ package_namespace.sub(/__$/, '') if package_namespace
39
56
  end
40
57
 
41
58
  def define
42
- desc "Deploy to salesforce"
43
- task name do
44
- process_source
45
- make_resources
46
- make_zipfile
47
- require 'hoopla_salesforce/deployer'
48
- HooplaSalesforce::Deployer.new(username, password, token, enterprise_wsdl, metadata_wsdl).deploy(deploy_file, deploy_options)
59
+ if name.is_a? Hash
60
+ task_name = name.keys.first
61
+ dependencies = name[task_name]
62
+ else
63
+ task_name = name
64
+ dependencies = []
65
+ end
66
+
67
+ namespace :hsf do
68
+ namespace :undeploy do
69
+ desc "Undeploy all components in #{src} from salesforce"
70
+ task task_name => dependencies do
71
+ process_source
72
+ require 'hoopla_salesforce/package_generator'
73
+ PackageGenerator.new(processed_src, api_version).generate_destructive_changes
74
+ make_zipfile
75
+ deploy_zipfile
76
+ end
77
+ end
78
+
79
+ namespace :deploy do
80
+ desc "Deploy to salesforce"
81
+ task task_name => dependencies do
82
+ process_source
83
+ make_meta_xmls
84
+ make_zipfile
85
+ deploy_zipfile
86
+ end
87
+ end
88
+
89
+ desc "Renders any page templates as test pages in #{processed_src}/pages-test"
90
+ task :testpages => dependencies do
91
+ mkdir_p "#{src}/pages-test"
92
+ make_pages do |template|
93
+ HooplaSalesforce::TemplateProcessor::TestPage.new(src, template)
94
+ end
95
+ end
49
96
  end
50
97
  end
51
98
 
52
- def deploy_options
53
- testNames = Dir["#{src}/classes/*.cls"].inject([]) do |names, f|
54
- body = File.read(f)
55
- if body =~ /(testMethod|@isTest)/ && match = body.match(/\bclass\s+(\w+)\s*\{\s*/)
56
- names << match[1]
57
- else
58
- names
99
+ def test_names
100
+ if ENV['TEST_NAMES']
101
+ ENV['TEST_NAMES'].split(',')
102
+ else
103
+ Dir["#{processed_src}/classes/*.cls"].inject([]) do |names, f|
104
+ body = File.read(f)
105
+ if body =~ /(testMethod|@isTest)/ && name = extract_class_name(body)
106
+ names << name
107
+ else
108
+ names
109
+ end
59
110
  end
60
111
  end
112
+ end
61
113
 
62
- if testNames.empty?
114
+ def test_options
115
+ if test_names.empty?
63
116
  { "wsdl:runAllTests" => true }
64
117
  else
65
- testNames.map! { |n| namespace.sub(/__$/, '.') + n } if namespace
118
+ test_names.map! { |n| "#{clean_namespace}.#{n}" } if package_namespace
66
119
  { "wsdl:runTests" => testNames }
67
120
  end
68
121
  end
69
122
 
123
+ def api_version
124
+ return @api_version if @api_version
125
+ File.read("#{src}/package.xml") =~ /<version>(.*)<\/version>/i
126
+ @api_version = $1
127
+ end
128
+
129
+ def make_pages
130
+ require template_helper if File.exist?(template_helper)
131
+
132
+ Dir["#{src}/pages/*.page.erb"].each do |template|
133
+ yield template
134
+ end
135
+ end
136
+
137
+ def preprocess_files
138
+ require template_helper if File.exist?(template_helper)
139
+
140
+ Dir["#{processed_src}/**/*.erb"].each do |template|
141
+ yield template
142
+ end
143
+ end
144
+
145
+ def make_meta(glob)
146
+ Dir["#{processed_src}/#{glob}"].each do |file|
147
+ meta = "#{file}-meta.xml"
148
+ next if file =~ /-meta\.xml$/ || File.exist?(meta)
149
+ File.open(meta, 'w') do |f|
150
+ f.print yield(file)
151
+ end
152
+ end
153
+ end
154
+
155
+ def make_meta_xmls
156
+ make_meta "classes/*.cls" do |klass|
157
+ <<-EOS.margin
158
+ <?xml version="1.0" encoding="UTF-8"?>
159
+ <ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
160
+ <apiVersion>#{api_version}</apiVersion>
161
+ </ApexClass>
162
+ EOS
163
+ end
164
+
165
+ make_meta "pages/*.page" do |page|
166
+ <<-EOS.margin
167
+ <?xml version="1.0" encoding="UTF-8"?>
168
+ <ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
169
+ <apiVersion>#{api_version}</apiVersion>
170
+ <label>#{File.basename(page, '.page')}</label>
171
+ </ApexClass>
172
+ EOS
173
+ end
174
+
175
+ make_meta "documents/**/*" do |doc|
176
+ if File.directory?(doc)
177
+ m = <<-EOS.margin
178
+ <?xml version="1.0" encoding="UTF-8"?>
179
+ <DocumentFolder xmlns="http://soap.sforce.com/2006/04/metadata">
180
+ <name>#{File.basename(doc)}</name>
181
+ <accessType>Public</accessType>
182
+ <publicFolderAccess>ReadOnly</publicFolderAccess>
183
+ </DocumentFolder>
184
+ EOS
185
+ else
186
+ <<-EOS.margin
187
+ <?xml version="1.0" encoding="UTF-8"?>
188
+ <Document xmlns="http://soap.sforce.com/2006/04/metadata">
189
+ <internalUseOnly>false</internalUseOnly>
190
+ <name>#{File.basename(doc)}</name>
191
+ <public>true</public>
192
+ <description>A document</description>
193
+ </Document>
194
+ EOS
195
+ end
196
+ end
197
+
198
+ make_meta "staticresources/*.resource" do |resource|
199
+ <<-EOS.margin
200
+ <?xml version="1.0" encoding="UTF-8"?>
201
+ <StaticResource xmlns="http://soap.sforce.com/2006/04/metadata">
202
+ <cacheControl>Private</cacheControl>
203
+ <contentType>application/zip</contentType>
204
+ <description>A static resource</description>
205
+ </StaticResource>
206
+ EOS
207
+ end
208
+
209
+ make_meta "triggers/*.trigger" do |trigger|
210
+ <<-EOS.margin
211
+ <?xml version="1.0" encoding="UTF-8"?>
212
+ <ApexTrigger xmlns="http://soap.sforce.com/2006/04/metadata">
213
+ <apiVersion>#{api_version}</apiVersion>
214
+ <status>Active</status>
215
+ </ApexTrigger>
216
+ EOS
217
+ end
218
+ end
219
+
70
220
  def make_resources
71
221
  require 'zip/zip'
72
222
 
@@ -83,7 +233,8 @@ module HooplaSalesforce
83
233
  end
84
234
  end
85
235
 
86
- cp "#{f}.resource-meta.xml", "#{resourcefile}-meta.xml"
236
+ source_meta = "#{f}.resource-meta.xml"
237
+ cp source_meta, "#{resourcefile}-meta.xml" if File.exist?(source_meta)
87
238
  end
88
239
  end
89
240
 
@@ -97,17 +248,39 @@ module HooplaSalesforce
97
248
  end
98
249
  end
99
250
 
251
+ def deploy_zipfile
252
+ require 'hoopla_salesforce/deployer'
253
+ deployer = HooplaSalesforce::Deployer.new(username, password, token, enterprise_wsdl, metadata_wsdl)
254
+ if ENV['WEB_TESTS']
255
+ require 'hoopla_salesforce/web_agent'
256
+ deployer.deploy(deploy_file)
257
+ WebAgent.new(username, password).run_tests(test_names, clean_namespace)
258
+ else
259
+ deployer.deploy(deploy_file, test_options)
260
+ end
261
+ end
262
+
100
263
  def process_source
101
264
  rm_rf processed_src
102
265
  cp_r src, processed_src
103
266
  Dir["#{processed_src}/**/*"].each do |f|
104
267
  next if File.directory?(f)
105
- system %Q|ruby -i -n -e 'print $_.gsub("__NAMESPACE__", "#{namespace}")' "#{f}"|
268
+ system %Q|ruby -i -n -e 'print $_.gsub("__NAMESPACE__", "#{package_namespace}")' "#{f}"|
269
+ end
270
+
271
+ if package_namespace
272
+ system %Q|ruby -i -n -e 'print $_.gsub("#{package_namespace}", "#{clean_namespace}")' "#{processed_src}/package.xml"|
273
+ end
274
+
275
+ make_resources
276
+ make_pages do |template|
277
+ HooplaSalesforce::TemplateProcessor::VisualForce.new(processed_src, template)
278
+ rm template.sub(src, processed_src)
106
279
  end
107
280
 
108
- if namespace
109
- clean_namespace = namespace.sub(/__$/, '')
110
- system %Q|ruby -i -n -e 'print $_.gsub("#{namespace}", "#{clean_namespace}")' "#{processed_src}/package.xml"|
281
+ preprocess_files do |template|
282
+ HooplaSalesforce::TemplateProcessor::Generic.new(processed_src, template)
283
+ rm template
111
284
  end
112
285
  end
113
286
  end
@@ -20,10 +20,12 @@ module HooplaSalesforce
20
20
  end
21
21
 
22
22
  def define
23
- desc "Retrieve all apex classes from salesforce.com"
24
- task name do
25
- require 'hoopla_salesforce/deployer'
26
- HooplaSalesforce::Deployer.new(username, password, token, enterprise_wsdl, metadata_wsdl).retrieve(request)
23
+ namespace :hsf do
24
+ desc "Retrieve all apex classes from salesforce.com"
25
+ task name do
26
+ require 'hoopla_salesforce/deployer'
27
+ HooplaSalesforce::Deployer.new(username, password, token, enterprise_wsdl, metadata_wsdl).retrieve(request)
28
+ end
27
29
  end
28
30
  end
29
31
  end
@@ -0,0 +1,121 @@
1
+ require 'fileutils'
2
+ require 'hoopla_salesforce/ext/string'
3
+
4
+ module HooplaSalesforce
5
+ class Skeleton
6
+ include FileUtils
7
+
8
+ attr_reader :directory
9
+
10
+ def initialize(directory)
11
+ @directory = directory
12
+ end
13
+
14
+ def mkdir_p(dir='')
15
+ super directory + dir
16
+ end
17
+
18
+ def create
19
+ puts "Creating a skeleton in #{directory}"
20
+ %w(applications classes objects pages resources tabs triggers).each do |dir|
21
+ mkdir_p "/src/#{dir}"
22
+ end
23
+ mkdir_p '/lib'
24
+ make_package_xml
25
+ make_rakefile
26
+ make_gitignore
27
+ followup_instructions
28
+ end
29
+
30
+ def make_package_xml
31
+ File.open("#{directory}/src/package.xml", 'w') do |pkg|
32
+ pkg.print <<-EOS.margin
33
+ <?xml version="1.0" encoding="UTF-8"?>
34
+ <Package xmlns="http://soap.sforce.com/2006/04/metadata">
35
+ <fullName></fullName>
36
+ <apiAccessLevel>Unrestricted</apiAccessLevel>
37
+ <description></description>
38
+ <namespacePrefix>__NAMESPACE__</namespacePrefix>
39
+ <types>
40
+ <members>*</members>
41
+ <name>ApexClass</name>
42
+ </types>
43
+ <types>
44
+ <members>*</members>
45
+ <name>ApexPage</name>
46
+ </types>
47
+ <types>
48
+ <members>*</members>
49
+ <name>ApexTrigger</name>
50
+ </types>
51
+ <types>
52
+ <members>*</members>
53
+ <name>CustomApplication</name>
54
+ </types>
55
+ <types>
56
+ <members>*</members>
57
+ <name>CustomObject</name>
58
+ </types>
59
+ <types>
60
+ <members>*</members>
61
+ <name>CustomField</name>
62
+ </types>
63
+ <types>
64
+ <members>*</members>
65
+ <name>ValidationRule</name>
66
+ </types>
67
+ <types>
68
+ <members>*</members>
69
+ <name>CustomTab</name>
70
+ </types>
71
+ <types>
72
+ <members>*</members>
73
+ <name>StaticResource</name>
74
+ </types>
75
+ <version>18.0</version>
76
+ </Package>
77
+ EOS
78
+ end
79
+ end
80
+
81
+ def make_rakefile
82
+ File.open("#{directory}/Rakefile", 'w') do |rakefile|
83
+ rakefile.print <<-EOS.margin
84
+ require 'hoopla_salesforce/rake'
85
+
86
+ HooplaSalesforce.enterprise_wsdl = "lib/enterprise.xml"
87
+ HooplaSalesforce.metadata_wsdl = "lib/metadata.xml"
88
+
89
+ namespace :deploy do
90
+ HooplaSalesforce::Rake::DeployTask.new(:development) do |t|
91
+ t.username = "you@development.org"
92
+ t.password = "yourpassword"
93
+ t.token = "your security token"
94
+ t.namespace = ""
95
+ end
96
+ end
97
+ EOS
98
+ end
99
+ end
100
+
101
+ def make_gitignore
102
+ File.open("#{directory}/.gitignore", 'w') do |gitignore|
103
+ gitignore.print <<-EOS.margin
104
+ src-processed
105
+ deploy.zip
106
+ EOS
107
+ end
108
+ end
109
+
110
+ def followup_instructions
111
+ puts
112
+ puts "-" * 80
113
+ puts " All done. Now you need to download your WSDL files into your project:"
114
+ puts
115
+ puts " Enterprise WSDL: #{directory}/lib/enterprise.xml"
116
+ puts " Metadata WSDL: #{directory}/lib/metadata.xml"
117
+ puts
118
+ puts "-" * 80
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,120 @@
1
+ require 'hoopla_salesforce/eruby'
2
+
3
+ module HooplaSalesforce
4
+ class TemplateProcessor
5
+ class Base
6
+ include HooplaSalesforce::CaptureHelper
7
+
8
+ attr_reader :src, :base, :file
9
+ def initialize(src, file)
10
+ @src = src
11
+ @base = File.basename(file, '.erb')
12
+ @file = file
13
+ template = Eruby.new(File.read(file))
14
+
15
+ File.open(output_file, 'w') do |f|
16
+ f.print template.result(binding)
17
+ end
18
+ end
19
+
20
+ def each_resource_file(files, extension)
21
+ files.map do |file|
22
+ file += ".#{extension}" unless extension =~ /\.#{extension}$/
23
+ yield file
24
+ end.join("\n")
25
+ end
26
+ end
27
+
28
+ class Generic < Base
29
+ def output_file
30
+ file.sub(/\.erb$/, '')
31
+ end
32
+ end
33
+
34
+ class VisualForce < Base
35
+ def page(opts={})
36
+ params = opts.map { |key, val| %Q|#{key}="#{val}"| }.join(" ")
37
+ "<apex:page #{params}>"
38
+ end
39
+
40
+ def end_page
41
+ "</apex:page>"
42
+ end
43
+
44
+ def resource_url(file)
45
+ resource, file = file.split('/', 2)
46
+ "{!URLFOR($Resource.#{resource}, '/#{file}')}"
47
+ end
48
+
49
+ def stylesheet_include_tag(*files)
50
+ each_resource_file(files, "css") do |file|
51
+ %Q|<apex:stylesheet value="#{resource_url(file)}" />|
52
+ end
53
+ end
54
+
55
+ def javascript_include_tag(*files)
56
+ each_resource_file(files, "js") do |file|
57
+ %Q|<script type="text/javascript" src="#{resource_url(file)}"></script>|
58
+ end
59
+ end
60
+
61
+ def as_json_array(collection, var)
62
+ <<-EOS.margin
63
+ [<apex:repeat value="{!#{collection}}" var="#{var}" rows="1">
64
+ #{send("#{var}_json")}
65
+ </apex:repeat>
66
+ <apex:repeat value="{!#{collection}}" var="#{var}" first="1">
67
+ ,#{send("#{var}_json")}
68
+ </apex:repeat>]
69
+ EOS
70
+ end
71
+
72
+ def output_file
73
+ "#{src}/pages/#{base}"
74
+ end
75
+ end
76
+
77
+ class TestPage < Base
78
+ def page(opts={})
79
+ <<-EOS.margin
80
+ <html>
81
+ <head>
82
+ <title>Test Page: #{opts[:controller]}</title>
83
+ </head>
84
+ <body>
85
+ EOS
86
+ end
87
+
88
+ def end_page
89
+ <<-EOS.margin
90
+ </body>
91
+ </html>
92
+ EOS
93
+ end
94
+
95
+ def resource_url(file)
96
+ "../resources/#{file}"
97
+ end
98
+
99
+ def stylesheet_include_tag(*files)
100
+ each_resource_file(files, "css") do |file|
101
+ %Q|<link rel="stylesheet" type="text/css" href="#{resource_url(file)}" />|
102
+ end
103
+ end
104
+
105
+ def javascript_include_tag(*files)
106
+ each_resource_file(files, "js") do |file|
107
+ %Q|<script type="text/javascript" src="#{resource_url(file)}"></script>|
108
+ end
109
+ end
110
+
111
+ def as_json_array(collection, var)
112
+ send("#{var}_json")
113
+ end
114
+
115
+ def output_file
116
+ "#{src}/pages-test/#{base}.html"
117
+ end
118
+ end
119
+ end
120
+ end
@@ -33,7 +33,7 @@ module HooplaSalesforce
33
33
  test_failures = []
34
34
  coverage_warnings = []
35
35
 
36
- result[:messages].each do |message|
36
+ sanitize(result[:messages]).each do |message|
37
37
  if message[:success]
38
38
  status = " "
39
39
  status = "U" if message[:changed]
@@ -65,6 +65,7 @@ module HooplaSalesforce
65
65
  test_result = result[:run_test_result]
66
66
  failures = test_result[:num_failures].to_i
67
67
  passes = test_result[:num_tests_run].to_i - failures
68
+ passes = 0 if passes < 0
68
69
  print "#{indent}Passes: #{green}#{passes}#{end_color} "
69
70
  print "Failures: #{red}#{failures}#{end_color} "
70
71
  puts "Duration: #{test_result[:total_time]}"
@@ -73,7 +74,7 @@ module HooplaSalesforce
73
74
  # :failures is only an array if we have more than 1. Fun...
74
75
  sanitize(test_result[:failures]).each do |failure|
75
76
  message = "#{indent}#{red}#{failure[:name]}.#{failure[:method_name]}: #{failure[:message]}\n"
76
- message += failure[:stack_trace].split("\n").map{ |l| indent * 2 + l }.join("\n")
77
+ message += failure[:stack_trace].split("\n").map{ |l| indent * 2 + l }.join("\n") if failure[:stack_trace]
77
78
  message += end_color
78
79
  test_failures << message
79
80
  end
@@ -0,0 +1,11 @@
1
+ module HooplaSalesforce
2
+ module Utils
3
+ def extract_class_name(body)
4
+ (match = body.match(/\bclass\s+(\w+)\s*\{\s*/)) && match[1]
5
+ end
6
+
7
+ def extract_trigger_name(body)
8
+ (match = body.match(/\btrigger\s+(\w+)\s/)) && match[1]
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,71 @@
1
+ require 'mechanize'
2
+ require 'cgi'
3
+
4
+ module HooplaSalesforce
5
+ class WebAgent
6
+ LOGIN_URL = 'https://login.salesforce.com'
7
+ MAX_CLASSES = 10_000
8
+
9
+ attr_reader :username, :password, :agent
10
+
11
+ def initialize(username, password)
12
+ @username = username
13
+ @password = password
14
+ @agent = Mechanize.new
15
+ login
16
+ end
17
+
18
+ def login
19
+ agent.get(LOGIN_URL).form_with(:name => 'login') do |login|
20
+ login.un = username
21
+ login.pw = password
22
+ end.submit
23
+ end
24
+
25
+ def test_run_url(test, namespace)
26
+ "/setup/build/runApexTest.apexp?class_id=#{test[:id]}&class_name=#{test[:name]}&ns_prefix=#{namespace}"
27
+ end
28
+
29
+ def test_results_dir
30
+ "test-results"
31
+ end
32
+
33
+ def test_results_file(test_name)
34
+ "#{test_results_dir}/#{test_name}.html"
35
+ end
36
+
37
+ def get_all_test_links
38
+ page = agent.get('/01p')
39
+
40
+ if page.search('.next')
41
+ expand_link = page.search('.fewerMore a').first['href']
42
+ enough_rows = expand_link.sub(/rowsperpage=\d+/, "rowsperpage=#{MAX_CLASSES}")
43
+ page = agent.get(enough_rows)
44
+ end
45
+
46
+ page.search('.dataCell[scope="row"] a')
47
+ end
48
+
49
+ # Runs the given tests from the Web UI
50
+ # namespace is the package namespace without the __
51
+ def run_tests(test_names, namespace = '')
52
+ test_links = get_all_test_links
53
+
54
+ tests = test_names.map do |test_name|
55
+ { :id => test_links.detect{ |a| a.text == test_name }['href'][1..-1],
56
+ :name => test_name }
57
+ end
58
+
59
+ mkdir_p test_results_dir
60
+ tests.each do |test|
61
+ results_page = agent.get(test_run_url(test, namespace))
62
+ results_table = results_page.search('.outer table td').first
63
+ File.open(test_results_file(test[:name]), 'w') do |f|
64
+ f.print results_table.inner_html
65
+ end
66
+
67
+ puts "#{test[:name]} complete, results in #{test_results_file(test[:name])}."
68
+ end
69
+ end
70
+ end
71
+ end
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 0
8
- - 3
9
- version: 0.0.3
8
+ - 4
9
+ version: 0.0.4
10
10
  platform: ruby
11
11
  authors:
12
12
  - Trotter Cashion
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-08-11 00:00:00 -04:00
18
+ date: 2010-08-27 00:00:00 -04:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -46,21 +46,74 @@ dependencies:
46
46
  version: 0.9.4
47
47
  type: :runtime
48
48
  version_requirements: *id002
49
- description: No really, these helpers are awesome
49
+ - !ruby/object:Gem::Dependency
50
+ name: commander
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ segments:
57
+ - 4
58
+ - 0
59
+ - 3
60
+ version: 4.0.3
61
+ type: :runtime
62
+ version_requirements: *id003
63
+ - !ruby/object:Gem::Dependency
64
+ name: erubis
65
+ prerelease: false
66
+ requirement: &id004 !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ segments:
71
+ - 2
72
+ - 6
73
+ - 6
74
+ version: 2.6.6
75
+ type: :runtime
76
+ version_requirements: *id004
77
+ - !ruby/object:Gem::Dependency
78
+ name: mechanize
79
+ prerelease: false
80
+ requirement: &id005 !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ segments:
85
+ - 1
86
+ - 0
87
+ - 0
88
+ version: 1.0.0
89
+ type: :runtime
90
+ version_requirements: *id005
91
+ description: Helpers for building Salesforce.com projects
50
92
  email: dev@hoopla.net
51
- executables: []
52
-
93
+ executables:
94
+ - hoopla_salesforce
53
95
  extensions: []
54
96
 
55
97
  extra_rdoc_files: []
56
98
 
57
99
  files:
100
+ - README.md
101
+ - bin/hoopla_salesforce
58
102
  - lib/hoopla_salesforce/rake.rb
59
103
  - lib/hoopla_salesforce/rake/base_task.rb
60
104
  - lib/hoopla_salesforce/rake/deploy_task.rb
61
105
  - lib/hoopla_salesforce/rake/retrieve_task.rb
62
- - lib/hoopla_salesforce/version.rb
106
+ - lib/hoopla_salesforce/eruby.rb
107
+ - lib/hoopla_salesforce/web_agent.rb
108
+ - lib/hoopla_salesforce/utils.rb
63
109
  - lib/hoopla_salesforce/text_reporter.rb
110
+ - lib/hoopla_salesforce/skeleton.rb
111
+ - lib/hoopla_salesforce/template_processor.rb
112
+ - lib/hoopla_salesforce/package_generator.rb
113
+ - lib/hoopla_salesforce/commands.rb
114
+ - lib/hoopla_salesforce/info.rb
115
+ - lib/hoopla_salesforce/ext/string.rb
116
+ - lib/hoopla_salesforce/ext/commander.rb
64
117
  - lib/hoopla_salesforce/deployer.rb
65
118
  - lib/hoopla_salesforce.rb
66
119
  has_rdoc: true
@@ -92,6 +145,6 @@ rubyforge_project: nowarning
92
145
  rubygems_version: 1.3.6
93
146
  signing_key:
94
147
  specification_version: 3
95
- summary: Some awesome helpers for the salesforce API
148
+ summary: Helpers for building Salesforce.com projects
96
149
  test_files: []
97
150
 
@@ -1,3 +0,0 @@
1
- module HooplaSalesforce
2
- VERSION = "0.0.3"
3
- end