seiun 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []