seiun 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f5007ec6a09aa9bafcfb5ca763bf3f79dd84a2ba
4
+ data.tar.gz: 770f1d6e68f386243d032fdd31e86ea789dc8dbb
5
+ SHA512:
6
+ metadata.gz: a569196984d50e6345426fc477d099e2274e31334f5e206d079da235e097a15bdf5544a7258f1b08800f101888d68f6a4293ceae8071523cb6c8d93926eaf3c9
7
+ data.tar.gz: b83f143bd8923414748e2959943da767da60b93c653fa6e45352b352f1deaca1457537e935347ac8e4a0a08c97bfae077566c97ce914d013d0bdbf94a29d26b7
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.3.0
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ before_install: gem install bundler -v 1.11.2
@@ -0,0 +1,49 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, and in the interest of
4
+ fostering an open and welcoming community, we pledge to respect all people who
5
+ contribute through reporting issues, posting feature requests, updating
6
+ documentation, submitting pull requests or patches, and other activities.
7
+
8
+ We are committed to making participation in this project a harassment-free
9
+ experience for everyone, regardless of level of experience, gender, gender
10
+ identity and expression, sexual orientation, disability, personal appearance,
11
+ body size, race, ethnicity, age, religion, or nationality.
12
+
13
+ Examples of unacceptable behavior by participants include:
14
+
15
+ * The use of sexualized language or imagery
16
+ * Personal attacks
17
+ * Trolling or insulting/derogatory comments
18
+ * Public or private harassment
19
+ * Publishing other's private information, such as physical or electronic
20
+ addresses, without explicit permission
21
+ * Other unethical or unprofessional conduct
22
+
23
+ Project maintainers have the right and responsibility to remove, edit, or
24
+ reject comments, commits, code, wiki edits, issues, and other contributions
25
+ that are not aligned to this Code of Conduct, or to ban temporarily or
26
+ permanently any contributor for other behaviors that they deem inappropriate,
27
+ threatening, offensive, or harmful.
28
+
29
+ By adopting this Code of Conduct, project maintainers commit themselves to
30
+ fairly and consistently applying these principles to every aspect of managing
31
+ this project. Project maintainers who do not follow or enforce the Code of
32
+ Conduct may be permanently removed from the project team.
33
+
34
+ This code of conduct applies both within project spaces and in public spaces
35
+ when an individual is representing the project or its community.
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
38
+ reported by contacting a project maintainer at naoki.watanabe@ikina.org. All
39
+ complaints will be reviewed and investigated and will result in a response that
40
+ is deemed necessary and appropriate to the circumstances. Maintainers are
41
+ obligated to maintain confidentiality with regard to the reporter of an
42
+ incident.
43
+
44
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
45
+ version 1.3.0, available at
46
+ [http://contributor-covenant.org/version/1/3/0/][version]
47
+
48
+ [homepage]: http://contributor-covenant.org
49
+ [version]: http://contributor-covenant.org/version/1/3/0/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in seiun.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Naoki Watanabe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # Seiun
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/seiun`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'seiun'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install seiun
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/seiun. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
36
+
37
+
38
+ ## License
39
+
40
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
41
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,9 @@
1
+ # To exec rspec and connect salesforce.com
2
+ # Enter your account below
3
+
4
+ salesforce:
5
+ username: yourname@email.com
6
+ password: your_password
7
+ security_token: your_security_token
8
+ customer_key: customer_key_on_this_application
9
+ consumer_secret: customer_secret_on_this_application
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "seiun"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/lib/seiun.rb ADDED
@@ -0,0 +1,25 @@
1
+ require 'rexml/document'
2
+ require 'rexml/parsers/baseparser'
3
+ require 'rexml/parsers/streamparser'
4
+ require 'rexml/streamlistener'
5
+ require "seiun/version"
6
+ require "seiun/utils"
7
+ require "seiun/xml_generators/base"
8
+ require "seiun/xml_generators/batch_xml"
9
+ require "seiun/xml_generators/job_xml"
10
+ require "seiun/xml_parsers/base"
11
+ require "seiun/xml_parsers/batch_xml"
12
+ require "seiun/xml_parsers/job_xml"
13
+ require "seiun/xml_parsers/record_xml"
14
+ require "seiun/xml_parsers/result_xml"
15
+ require "seiun/xml_parsers/stream_listener"
16
+ require "seiun/callback/extends"
17
+ require "seiun/callback/record_wrapper"
18
+ require "seiun/callback/wrapper"
19
+ require "seiun/connection"
20
+ require "seiun/job"
21
+ require "seiun/queue"
22
+ require "seiun/client"
23
+
24
+ module Seiun
25
+ end
@@ -0,0 +1,27 @@
1
+ module Seiun
2
+ module Callback
3
+ module Extends
4
+ module ClassMethods
5
+ [ :hashalize, :after_build_xml, :before_request, :after_response, :ssl_verify_none,
6
+ :mock_response_create_job, :mock_response_close_job, :mock_response_add_query, :mock_response_add_batch,
7
+ :mock_response_get_job_details, :mock_response_get_batch_details, :mock_response_get_result, :mock_response_get_query_result
8
+ ].each do |callback_point|
9
+ define_method "seiun_#{callback_point}" do |method_name|
10
+ @seiun_callbacks ||= {}
11
+ @seiun_callbacks[callback_point] = method_name
12
+ end
13
+ end
14
+
15
+ def seiun_callbacks
16
+ @seiun_callbacks
17
+ end
18
+ end
19
+
20
+ extend ClassMethods
21
+
22
+ def self.included(klass)
23
+ klass.extend ClassMethods
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,31 @@
1
+ module Seiun
2
+ module Callback
3
+ class RecordWrapper
4
+ def initialize(record)
5
+ @record = record
6
+ end
7
+
8
+ def to_hash
9
+ if callback_defined?(:hashalize)
10
+ @record.__send__(:hashalize)
11
+ elsif @record.respond_to?(:to_hash)
12
+ @record.to_hash
13
+ else
14
+ @record
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def callback_defined?(name)
21
+ !!callbacks[name]
22
+ end
23
+
24
+ def callbacks
25
+ return {} unless @record.class.respond_to?(:seiun_callbacks)
26
+ return {} unless @record.class.seiun_callbacks.is_a?(Hash)
27
+ @record.class.seiun_callbacks
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,81 @@
1
+ module Seiun
2
+ module Callback
3
+ class Wrapper
4
+ def initialize(callback_class)
5
+ @class = callback_class
6
+ end
7
+
8
+ def after_build_xml(xml)
9
+ return unless callback_defined?(:after_build_xml)
10
+ @class.__send__(callbacks[:after_build_xml], xml)
11
+ end
12
+
13
+ def before_request(request_type, path, request_body)
14
+ return unless callback_defined?(:before_request)
15
+ @class.__send__(callbacks[:before_request], request_type, path, request_body)
16
+ end
17
+
18
+ def after_response(request_type, path, response_body)
19
+ return unless callback_defined?(:after_response)
20
+ @class.__send__(callbacks[:after_response], request_type, path, response_body)
21
+ end
22
+
23
+ def ssl_verify_none
24
+ return unless callback_defined?(:ssl_verify_none)
25
+ @class.__send__(callbacks[:ssl_verify_none])
26
+ end
27
+
28
+ def mock_response_create_job
29
+ return unless callback_defined?(:mock_response_create_job)
30
+ @class.__send__(callbacks[:mock_response_create_job])
31
+ end
32
+
33
+ def mock_response_close_job
34
+ return unless callback_defined?(:mock_response_close_job)
35
+ @class.__send__(callbacks[:mock_response_close_job])
36
+ end
37
+
38
+ def mock_response_add_query
39
+ return unless callback_defined?(:mock_response_add_query)
40
+ @class.__send__(callbacks[:mock_response_add_query])
41
+ end
42
+
43
+ def mock_response_add_batch
44
+ return unless callback_defined?(:mock_response_add_batch)
45
+ @class.__send__(callbacks[:mock_response_add_batch])
46
+ end
47
+
48
+ def mock_response_get_job_details
49
+ return unless callback_defined?(:mock_response_get_job_details)
50
+ @class.__send__(callbacks[:mock_response_get_job_details])
51
+ end
52
+
53
+ def mock_response_get_batch_details
54
+ return unless callback_defined?(:mock_response_get_batch_details)
55
+ @class.__send__(callbacks[:mock_response_get_batch_details])
56
+ end
57
+
58
+ def mock_response_get_result
59
+ return unless callback_defined?(:mock_response_get_result)
60
+ @class.__send__(callbacks[:mock_response_get_result])
61
+ end
62
+
63
+ def mock_response_get_query_result
64
+ return unless callback_defined?(:mock_response_get_query_result)
65
+ @class.__send__(callbacks[:mock_response_get_query_result])
66
+ end
67
+
68
+ private
69
+
70
+ def callback_defined?(name)
71
+ !!callbacks[name]
72
+ end
73
+
74
+ def callbacks
75
+ return {} unless @class.respond_to?(:seiun_callbacks)
76
+ return {} unless @class.seiun_callbacks.is_a?(Hash)
77
+ @class.seiun_callbacks
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,86 @@
1
+ module Seiun
2
+ class Client
3
+ SEC_TO_WAIT_ASYNC = 180
4
+ private_constant :SEC_TO_WAIT_ASYNC
5
+
6
+ def initialize(databasedotcom: nil, batch_size: 10_000)
7
+ @batch_size = batch_size
8
+ @connection = Seiun::Connection.new(databasedotcom)
9
+ end
10
+
11
+ def insert(table_name, records, callback_class: nil, async: true)
12
+ operate(:insert, table_name, records: records, callback: callback_class, async: async)
13
+ end
14
+
15
+ def insert_queue(table_name, callback_class: nil, async: true)
16
+ @insert_queue ||= {}
17
+ @insert_queue[table_name] ||= Seiun::Queue.new(batch_size: @batch_size) do |records|
18
+ insert(table_name, records, callback_class: callback_class, async: async)
19
+ end
20
+ end
21
+
22
+ def update(table_name, records, callback_class: nil, async: true)
23
+ operate(:update, table_name, records: records, callback: callback_class, async: async)
24
+ end
25
+
26
+ def update_queue(table_name, callback_class: nil, async: true)
27
+ @update_queue ||= {}
28
+ @update_queue[table_name] ||= Seiun::Queue.new(batch_size: @batch_size) do |records|
29
+ update(table_name, records, callback_class: callback_class, async: async)
30
+ end
31
+ end
32
+
33
+ def upsert(table_name, records, ext_field_name, callback_class: nil, async: true)
34
+ operate(:upsert, table_name, records: records,
35
+ ext_field_name: ext_field_name, callback: callback_class, async: async)
36
+ end
37
+
38
+ def upsert_queue(table_name, ext_field_name, callback_class: nil, async: true)
39
+ @upsert_queue ||= {}
40
+ @upsert_queue[table_name] ||= Seiun::Queue.new(batch_size: @batch_size) do |records|
41
+ upsert(table_name, records, ext_field_name, callback_class: callback_class, async: async)
42
+ end
43
+ end
44
+
45
+ def delete(table_name, records, callback_class: nil, async: true)
46
+ operate(:delete, table_name, records: records, callback: callback_class, async: async)
47
+ end
48
+
49
+ def delete_queue(table_name, callback_class: nil, async: true)
50
+ @delete_queue ||= {}
51
+ @delete_queue[table_name] ||= Seiun::Queue.new(batch_size: @batch_size) do |records|
52
+ delete(table_name, records, callback_class: callback_class, async: async)
53
+ end
54
+ end
55
+
56
+ def query(table_name, soql, callback_class: nil)
57
+ operate(:query, table_name, soql: soql, callback: callback_class)
58
+ end
59
+
60
+ private
61
+
62
+ def operate(operation, object, records: [], soql: "", ext_field_name: nil, callback: nil, async: true)
63
+ callback = Seiun::Callback::Wrapper.new(callback) if callback
64
+ records = records.map{|r| Seiun::Callback::RecordWrapper.new(r) }
65
+ job = Seiun::Job.new(@connection, operation, object, ext_field_name: ext_field_name, callback: callback)
66
+ job.post_creation
67
+ operation == :query ? job.add_query(soql) : records.each_slice(@batch_size).each{|chunk| job.add_batch(chunk) }
68
+ job.post_closing
69
+ wait_asyc(job) if operation == :query || async == false
70
+ operation == :query ? job.get_query_result : job
71
+ end
72
+
73
+ def wait_asyc(job)
74
+ Timeout.timeout(sec_to_wait_async) do
75
+ until job.all_batch_finished?
76
+ job.get_batch_details
77
+ sleep 1
78
+ end
79
+ end
80
+ end
81
+
82
+ def sec_to_wait_async
83
+ SEC_TO_WAIT_ASYNC
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,111 @@
1
+ module Seiun
2
+ class Connection
3
+ def initialize(databasedotcom)
4
+ @databasedotcom = databasedotcom
5
+ end
6
+
7
+ def create_job(data, callback: nil)
8
+ connect(:create_job, data: data, callback: callback)
9
+ end
10
+
11
+ def close_job(data, job_id, callback: nil)
12
+ connect(:close_job, data: data, job_id: job_id, callback: callback)
13
+ end
14
+
15
+ def add_query(soql, job_id, callback: nil)
16
+ connect(:add_query, data: soql, job_id: job_id, callback: callback)
17
+ end
18
+
19
+ def add_batch(data, job_id, callback: nil)
20
+ connect(:add_batch, data: data, job_id: job_id, callback: callback)
21
+ end
22
+
23
+ def get_batch_details(job_id, callback: nil)
24
+ connect(:get_batch_details, job_id: job_id, callback: callback)
25
+ end
26
+
27
+ def get_result(job_id, batch_id, callback: nil)
28
+ connect(:get_result, job_id: job_id, batch_id: batch_id, callback: callback)
29
+ end
30
+
31
+ def get_query_result(job_id, batch_id, result_id, callback: nil)
32
+ connect(:get_query_result, job_id: job_id, batch_id: batch_id, result_id: result_id, callback: callback)
33
+ end
34
+
35
+ private
36
+
37
+ def connect(type, data: nil, job_id: nil, batch_id: nil, result_id: nil, callback: nil)
38
+ path = request_path(type, job_id, batch_id, result_id)
39
+ callback.before_request(type, path.dup, data) if callback
40
+ if callback && mock_body = callback.__send__("mock_response_#{type}")
41
+ body = mock_body
42
+ else
43
+ response = nil
44
+ raise_over_retry 3 do
45
+ response = request(type, path, data, job_id, batch_id, result_id, callback: callback)
46
+ response.value
47
+ end
48
+ body = response.body
49
+ end
50
+ callback.after_response(type, path, body) if callback
51
+ body
52
+ end
53
+
54
+ def request(type, path, data, job_id, batch_id, result_id, callback: nil)
55
+ https = Net::HTTP.new(instance_host, 443)
56
+ https.use_ssl = true
57
+ https.verify_mode = OpenSSL::SSL::VERIFY_NONE if callback && callback.ssl_verify_none
58
+ case type
59
+ when :create_job, :close_job, :add_query, :add_batch
60
+ https.post(path, data, headers)
61
+ else
62
+ https.get(path, headers)
63
+ end
64
+ end
65
+
66
+ def request_path(type, job_id, batch_id, result_id)
67
+ case type
68
+ when :create_job
69
+ "/services/async/#{api_version}/job"
70
+ when :close_job
71
+ "/services/async/#{api_version}/job/#{job_id}"
72
+ when :add_query, :add_batch, :get_batch_details
73
+ "/services/async/#{api_version}/job/#{job_id}/batch"
74
+ when :get_result
75
+ "/services/async/#{api_version}/job/#{job_id}/batch/#{batch_id}/result"
76
+ when :get_query_result
77
+ "/services/async/#{api_version}/job/#{job_id}/batch/#{batch_id}/result/#{result_id}"
78
+ end
79
+ end
80
+
81
+ def instance_host
82
+ return @instance_host if @instance_host
83
+ @databasedotcom.instance_url =~ /^https*\:\/\/([^\/]+)/
84
+ @instance_host = $1
85
+ end
86
+
87
+ def headers
88
+ { "X-SFDC-Session" => session_id, 'Content-Type' => 'application/xml; charset=UTF-8' }
89
+ end
90
+
91
+ def session_id
92
+ @session_id ||= @databasedotcom.oauth_token
93
+ end
94
+
95
+ def api_version
96
+ "35.0"
97
+ end
98
+
99
+ def raise_over_retry(times)
100
+ count = 0
101
+ begin
102
+ yield
103
+ rescue => ex
104
+ count += 1
105
+ raise ex, ex.response.body if count >= times
106
+ sleep 1
107
+ retry
108
+ end
109
+ end
110
+ end
111
+ end
data/lib/seiun/job.rb ADDED
@@ -0,0 +1,104 @@
1
+ module Seiun
2
+ class Job
3
+ attr_reader :operation
4
+
5
+ def initialize(connection, operation, object_name, id: nil, ext_field_name: nil, callback: nil)
6
+ @connection = connection
7
+ @operation = operation
8
+ @object_name = object_name
9
+ @id = id if id
10
+ @ext_field_name = ext_field_name if ext_field_name
11
+ @callback = callback
12
+ @batches = []
13
+ end
14
+
15
+ def post_creation
16
+ response_body = @connection.create_job(create_job_xml, callback: @callback)
17
+ parse_job_xml(response_body)
18
+ end
19
+
20
+ def post_closing
21
+ response_body = @connection.close_job(close_job_xml, @id, callback: @callback)
22
+ parse_job_xml(response_body)
23
+ end
24
+
25
+ def add_query(soql)
26
+ response_body = @connection.add_query(soql, @id, callback: @callback)
27
+ parse_batch_xml(response_body)
28
+ end
29
+
30
+ def add_batch(records)
31
+ response_body = @connection.add_batch(add_batch_xml(records), @id, callback: @callback)
32
+ parse_batch_xml(response_body)
33
+ end
34
+
35
+ def get_batch_details
36
+ response_body = @connection.get_batch_details(@id, callback: @callback)
37
+ parse_batch_xml(response_body)
38
+ end
39
+
40
+ def get_query_result
41
+ result = []
42
+ @batches.each do |batch|
43
+ result_response_body = @connection.get_result(@id, batch.id, callback: @callback)
44
+ Seiun::XMLParsers::ResultXML.each(result_response_body) do |result_response|
45
+ response_body = @connection.get_query_result(@id, batch.id, result_response.result_id, callback: @callback)
46
+ Seiun::XMLParsers::RecordXML.each(response_body) do |response|
47
+ result << response.to_hash
48
+ end
49
+ end
50
+ end
51
+ result
52
+ end
53
+
54
+ def all_batch_finished?
55
+ raise "Batches are empty" if @batches.empty?
56
+ @batches.all?{|batch| !["Queued", "InProgress"].include?(batch.sf_state) }
57
+ end
58
+
59
+ private
60
+
61
+ def create_job_xml
62
+ Seiun::XMLGenerators::JobXML.create_job(@operation, @object_name, ext_field_name: @ext_field_name, callback: @callback)
63
+ end
64
+
65
+ def close_job_xml
66
+ Seiun::XMLGenerators::JobXML.close_job(callback: @callback)
67
+ end
68
+
69
+ def add_batch_xml(records)
70
+ Seiun::XMLGenerators::BatchXML.add_batch(records, callback: @callback)
71
+ end
72
+
73
+ def parse_job_xml(response_body)
74
+ Seiun::XMLParsers::JobXML.each(response_body) do |response|
75
+ @id = response.id || @id
76
+ @sf_created_at = response.created_date || @sf_created_at
77
+ @sf_updated_at = response.system_modstamp || @sf_updated_at
78
+ @sf_state = response.state || @sf_state
79
+ end
80
+ end
81
+
82
+ def parse_batch_xml(response_body)
83
+ Seiun::XMLParsers::BatchXML.each(response_body) do |response|
84
+ unless batch = @batches.find{|batch| batch.id == response.id }
85
+ batch = Batch.new(response.id)
86
+ @batches << batch
87
+ end
88
+ batch.job_id = response.job_id || batch.job_id
89
+ batch.sf_state = response.state || batch.sf_state
90
+ batch.sf_created_at = response.created_date || batch.sf_created_at
91
+ batch.sf_updated_at = response.system_modstamp || batch.sf_updated_at
92
+ end
93
+ end
94
+
95
+ class Batch
96
+ attr_reader :id
97
+ attr_accessor :job_id, :sf_state, :sf_created_at, :sf_updated_at, :number_records_processed
98
+
99
+ def initialize(id)
100
+ @id = id
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,36 @@
1
+ module Seiun
2
+ class Queue
3
+ def initialize(batch_size: 10_000, &operation)
4
+ @batch_size = batch_size
5
+ @operation = operation
6
+ initialize_queue
7
+ @jobs = []
8
+ end
9
+
10
+ def <<(record)
11
+ push(record)
12
+ end
13
+
14
+ def push(record)
15
+ @queue << record
16
+ operate if @queue.size == @batch_size
17
+ record
18
+ end
19
+
20
+ def close
21
+ operate
22
+ @jobs
23
+ end
24
+
25
+ private
26
+
27
+ def operate
28
+ @jobs << @operation.call(@queue)
29
+ initialize_queue
30
+ end
31
+
32
+ def initialize_queue
33
+ @queue = []
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,21 @@
1
+ module Seiun
2
+ class Utils
3
+ class << self
4
+ def camelize(str)
5
+ str.to_s.gsub(/_([a-z])/){|match| $1.upcase }
6
+ end
7
+
8
+ def underscore(str)
9
+ str.to_s.gsub(/([a-z0-9])([A-Z])/){|match| "#{$1}_#{$2.downcase}" }
10
+ end
11
+
12
+ def parsable_date?(str)
13
+ str.to_s =~ /^[1-4][0-9]{3}-(?:0[1-9]|1[012])-(?:0[1-9]|[12][0-9]|3[01])$/
14
+ end
15
+
16
+ def parsable_time?(str)
17
+ str.to_s =~ /^[1-4][0-9]{3}-(?:0[1-9]|1[012])-(?:0[1-9]|[12][0-9]|3[01])T(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]/
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ module Seiun
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,24 @@
1
+ module Seiun
2
+ module XMLGenerators
3
+ class Base
4
+ def initialize(callback: nil)
5
+ @callback = callback
6
+ @rexml_doc = REXML::Document.new
7
+ @rexml_doc << REXML::XMLDecl.new('1.0', 'UTF-8')
8
+ end
9
+
10
+ def to_s
11
+ io = StringIO.new
12
+ rexml_doc.write(io)
13
+ io.rewind
14
+ io.read
15
+ end
16
+
17
+ private
18
+
19
+ def rexml_doc
20
+ @rexml_doc
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,55 @@
1
+ module Seiun
2
+ module XMLGenerators
3
+ class BatchXML < Base
4
+ class << self
5
+ def add_batch(records, callback: nil)
6
+ generator = new(callback: callback)
7
+ generator.add_batch(records)
8
+ end
9
+ end
10
+
11
+ def add_batch(records)
12
+ sobjects = rexml_doc.add_element("sObjects",
13
+ "xmlns" => "http://www.force.com/2009/06/asyncapi/dataload",
14
+ "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance")
15
+ records.each do |record|
16
+ add_record(sobjects, record)
17
+ end
18
+ to_s
19
+ end
20
+
21
+ private
22
+
23
+ def to_s
24
+ @callback.after_build_xml(rexml_doc) if @callback
25
+ super
26
+ end
27
+
28
+ def add_record(parent, record)
29
+ sobject = parent.add_element("sObject")
30
+ record.to_hash.each_pair do |key, value|
31
+ if value.is_a?(Hash)
32
+ relation = sobject.add_element(key.to_s)
33
+ add_record(relation, value)
34
+ elsif value.is_a?(NilClass) || value.is_a?(String) && value.empty?
35
+ sobject.add_element(key.to_s, "xsi:nil" => "true")
36
+ else
37
+ sobject.add_element(key.to_s).add_text(convert_value(value))
38
+ end
39
+ end
40
+ end
41
+
42
+ def convert_value(value)
43
+ if value.is_a?(Time) || value.is_a?(DateTime)
44
+ value.iso8601.to_s
45
+ elsif value.is_a?(Date)
46
+ value.strftime("%Y-%m-%d")
47
+ elsif value.is_a?(Array)
48
+ value.join(";")
49
+ else
50
+ value.to_s
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,43 @@
1
+ module Seiun
2
+ module XMLGenerators
3
+ class JobXML < Base
4
+ class << self
5
+ def create_job(operation, object, ext_field_name: nil, callback: nil)
6
+ generator = new(callback: callback)
7
+ generator.create_job(operation, object, ext_field_name: ext_field_name)
8
+ end
9
+
10
+ def close_job(callback: nil)
11
+ generator = new(callback: callback)
12
+ generator.close_job
13
+ end
14
+ end
15
+
16
+ def create_job(operation, object, ext_field_name: nil)
17
+ create_job_info do |jobinfo|
18
+ jobinfo.add_element("operation").add_text(operation.to_s)
19
+ jobinfo.add_element("object").add_text(object.to_s)
20
+ jobinfo.add_element("externalIdFieldName").add_text(ext_field_name.to_s) if ext_field_name
21
+ jobinfo.add_element("contentType").add_text("XML")
22
+ end
23
+ to_s
24
+ end
25
+
26
+ def close_job
27
+ create_job_info do |jobinfo|
28
+ jobinfo.add_element("state").add_text("Closed")
29
+ end
30
+ to_s
31
+ end
32
+
33
+ private
34
+
35
+ def create_job_info
36
+ jobinfo = rexml_doc.add_element("jobInfo",
37
+ "xmlns" => "http://www.force.com/2009/06/asyncapi/dataload",
38
+ "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance")
39
+ yield(jobinfo)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,41 @@
1
+ module Seiun
2
+ module XMLParsers
3
+ class Base
4
+ class << self
5
+ private
6
+
7
+ def parse(xml_str, find_tag, block)
8
+ callback = Proc.new {|attrs| block.call(new(attrs)) }
9
+ listener = XMLParsers::StreamListener.new(find_tag, callback)
10
+ REXML::Parsers::StreamParser.new(xml_str, listener).parse
11
+ end
12
+ end
13
+
14
+ def initialize(attrs)
15
+ @attrs = attrs
16
+ end
17
+
18
+ def to_hash(strict_mode = false)
19
+ return {} unless @attrs.is_a?(Array)
20
+ @attrs.each_with_object({}) do |attribute, result|
21
+ key, values = attribute.to_a.first
22
+ results = [values].flatten.map{|value|
23
+ next if value.is_a?(Hash) && value["xsi:nil"] == "true"
24
+ next value if strict_mode
25
+ if Seiun::Utils.parsable_date?(value) && ( date = Date.parse(value, false) rescue nil )
26
+ next date
27
+ end
28
+ if Seiun::Utils.parsable_time?(value) && ( time = Time.iso8601(value) rescue nil )
29
+ next time
30
+ end
31
+ next true if value == "true"
32
+ next false if value == "false"
33
+ value
34
+ }
35
+ results.uniq! unless strict_mode
36
+ result[key] = ( results.size <= 1 ? results.first : results )
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,17 @@
1
+ module Seiun
2
+ module XMLParsers
3
+ class BatchXML < Base
4
+ class << self
5
+ def each(xml_str, &block)
6
+ parse(xml_str, "batchInfo", block)
7
+ end
8
+ end
9
+
10
+ [ :id, :job_id, :state, :created_date, :system_modstamp ].each do |name|
11
+ define_method name do
12
+ to_hash(true)[Seiun::Utils.camelize(name)]
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ module Seiun
2
+ module XMLParsers
3
+ class JobXML < Base
4
+ class << self
5
+ def each(xml_str, &block)
6
+ parse(xml_str, "jobInfo", block)
7
+ end
8
+ end
9
+
10
+ [ :id, :operation, :object, :created_by_id, :created_date, :system_modstamp,
11
+ :state, :content_type, :external_id_field_name
12
+ ].each do |name|
13
+ define_method name do
14
+ to_hash(true)[Seiun::Utils.camelize(name)]
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,18 @@
1
+ module Seiun
2
+ module XMLParsers
3
+ class RecordXML < Base
4
+ class << self
5
+ def each(xml_str, find_tag: "records", &block)
6
+ parse(xml_str, find_tag, block)
7
+ end
8
+ end
9
+
10
+ def to_hash(strict_mode = false)
11
+ source = super
12
+ source.keys.find_all{|key| key =~ /(^[A-Z]|__c$)/ }.each_with_object({}) do |key, hash|
13
+ hash[key] = source[key]
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,33 @@
1
+ module Seiun
2
+ module XMLParsers
3
+ class ResultXML < Base
4
+ class << self
5
+ def each(xml_str, &block)
6
+ parse(xml_str, "result", block)
7
+ end
8
+ end
9
+
10
+ def result_id
11
+ return unless result.is_a?(String)
12
+ result
13
+ end
14
+
15
+ [ :id, :success ].each do |name|
16
+ define_method name do
17
+ return unless result.is_a?(Hash)
18
+ result[Seiun::Utils.camelize(name)]
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def result
25
+ if @attrs.size == 1 && @attrs.first.is_a?(String)
26
+ @attrs.first
27
+ else
28
+ to_hash
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,42 @@
1
+ module Seiun
2
+ module XMLParsers
3
+ class StreamListener
4
+ include REXML::StreamListener
5
+
6
+ def initialize(find_tag, callback)
7
+ @find_tag = find_tag
8
+ @callback = callback
9
+ @stack = []
10
+ end
11
+
12
+ def tag_start(name, attrs)
13
+ if @stack.empty? && name == @find_tag
14
+ element = []
15
+ element << attrs unless attrs.empty?
16
+ @current = element
17
+ @stack = [ element ]
18
+ elsif @current
19
+ element = []
20
+ element << attrs unless attrs.empty?
21
+ @stack.last << { name => element }
22
+ @stack.push(element)
23
+ end
24
+ end
25
+
26
+ def text(text)
27
+ text = text.strip
28
+ return if text.empty?
29
+ @stack.last << text if @current
30
+ end
31
+
32
+ def tag_end(name)
33
+ if @stack.size == 1 && name == @find_tag
34
+ @callback.call(@current)
35
+ @current, @stack = nil, []
36
+ elsif @current
37
+ pop_tag = @stack.pop
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
data/seiun.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'seiun/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "seiun"
8
+ spec.version = Seiun::VERSION
9
+ spec.authors = ["Naoki Watanabe"]
10
+ spec.email = ["naoki.watanabe@ikina.org"]
11
+
12
+ spec.summary = %q{Salesforce Adapter for Ruby environment.}
13
+ spec.description = %q{You can communicate Salesforce via Bulk API from ruby environment.}
14
+ spec.homepage = "https://github.com/naoki-watanabe/seiun"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency 'databasedotcom', '>= 1.0.8'
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.11"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec", "~> 3.0"
27
+ end
metadata ADDED
@@ -0,0 +1,132 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: seiun
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Naoki Watanabe
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-12-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: databasedotcom
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.0.8
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 1.0.8
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.11'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.11'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ description: You can communicate Salesforce via Bulk API from ruby environment.
70
+ email:
71
+ - naoki.watanabe@ikina.org
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".rspec"
78
+ - ".ruby-version"
79
+ - ".travis.yml"
80
+ - CODE_OF_CONDUCT.md
81
+ - Gemfile
82
+ - LICENSE.txt
83
+ - README.md
84
+ - Rakefile
85
+ - auth_credentials.example.yml
86
+ - bin/console
87
+ - bin/setup
88
+ - lib/seiun.rb
89
+ - lib/seiun/callback/extends.rb
90
+ - lib/seiun/callback/record_wrapper.rb
91
+ - lib/seiun/callback/wrapper.rb
92
+ - lib/seiun/client.rb
93
+ - lib/seiun/connection.rb
94
+ - lib/seiun/job.rb
95
+ - lib/seiun/queue.rb
96
+ - lib/seiun/utils.rb
97
+ - lib/seiun/version.rb
98
+ - lib/seiun/xml_generators/base.rb
99
+ - lib/seiun/xml_generators/batch_xml.rb
100
+ - lib/seiun/xml_generators/job_xml.rb
101
+ - lib/seiun/xml_parsers/base.rb
102
+ - lib/seiun/xml_parsers/batch_xml.rb
103
+ - lib/seiun/xml_parsers/job_xml.rb
104
+ - lib/seiun/xml_parsers/record_xml.rb
105
+ - lib/seiun/xml_parsers/result_xml.rb
106
+ - lib/seiun/xml_parsers/stream_listener.rb
107
+ - seiun.gemspec
108
+ homepage: https://github.com/naoki-watanabe/seiun
109
+ licenses:
110
+ - MIT
111
+ metadata: {}
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubyforge_project:
128
+ rubygems_version: 2.5.1
129
+ signing_key:
130
+ specification_version: 4
131
+ summary: Salesforce Adapter for Ruby environment.
132
+ test_files: []