doi_extractor 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +54 -0
  3. data/Capfile +34 -0
  4. data/Gemfile +6 -0
  5. data/README.md +50 -0
  6. data/Rakefile +43 -0
  7. data/bin/doi_extractor +18 -0
  8. data/config/deploy.rb +34 -0
  9. data/config/deploy/production.rb +65 -0
  10. data/doi_extractor.gemspec +29 -0
  11. data/lib/doi_extractor.rb +25 -0
  12. data/lib/doi_extractor/cancel_command.rb +24 -0
  13. data/lib/doi_extractor/command.rb +81 -0
  14. data/lib/doi_extractor/command_line_parser.rb +154 -0
  15. data/lib/doi_extractor/create_command.rb +32 -0
  16. data/lib/doi_extractor/download_command.rb +109 -0
  17. data/lib/doi_extractor/download_location.rb +86 -0
  18. data/lib/doi_extractor/errors.rb +34 -0
  19. data/lib/doi_extractor/ipums_client.rb +159 -0
  20. data/lib/doi_extractor/ipums_uri_builder.rb +51 -0
  21. data/lib/doi_extractor/old_ruby_patch.rb +25 -0
  22. data/lib/doi_extractor/options.rb +132 -0
  23. data/lib/doi_extractor/secrets.rb +18 -0
  24. data/lib/doi_extractor/status_command.rb +62 -0
  25. data/lib/doi_extractor/version.rb +3 -0
  26. data/spec/fixtures/api_creds.yml +2 -0
  27. data/spec/reports/SPEC-DoiExtractor-CancelCommand-when-successful.xml +7 -0
  28. data/spec/reports/SPEC-DoiExtractor-CancelCommand.xml +3 -0
  29. data/spec/reports/SPEC-DoiExtractor-CommandLineParser-with-a-valid-create-command-with-email.xml +14 -0
  30. data/spec/reports/SPEC-DoiExtractor-CommandLineParser-with-a-valid-create-command.xml +9 -0
  31. data/spec/reports/SPEC-DoiExtractor-CommandLineParser-with-a-valid-download-command.xml +9 -0
  32. data/spec/reports/SPEC-DoiExtractor-CommandLineParser-with-a-valid-status-command.xml +9 -0
  33. data/spec/reports/SPEC-DoiExtractor-CommandLineParser-with-an-invalid-command.xml +9 -0
  34. data/spec/reports/SPEC-DoiExtractor-CommandLineParser-with-an-unknown-option.xml +5 -0
  35. data/spec/reports/SPEC-DoiExtractor-CommandLineParser.xml +3 -0
  36. data/spec/reports/SPEC-DoiExtractor-CreateCommand-when-an-existing-extract-is-processing.xml +7 -0
  37. data/spec/reports/SPEC-DoiExtractor-CreateCommand-when-successful.xml +7 -0
  38. data/spec/reports/SPEC-DoiExtractor-CreateCommand.xml +3 -0
  39. data/spec/reports/SPEC-DoiExtractor-DownloadCommand-user-cancels-download.xml +7 -0
  40. data/spec/reports/SPEC-DoiExtractor-DownloadCommand-when-an-extract-is-available-when-force-is-not-set.xml +5 -0
  41. data/spec/reports/SPEC-DoiExtractor-DownloadCommand-when-an-extract-is-available.xml +13 -0
  42. data/spec/reports/SPEC-DoiExtractor-DownloadCommand.xml +3 -0
  43. data/spec/reports/SPEC-DoiExtractor-DownloadLocation.xml +11 -0
  44. data/spec/reports/SPEC-DoiExtractor-IpumsClient.xml +7 -0
  45. data/spec/reports/SPEC-DoiExtractor-IpumsUriBuilder-internal-environment.xml +5 -0
  46. data/spec/reports/SPEC-DoiExtractor-IpumsUriBuilder-live-environment.xml +5 -0
  47. data/spec/reports/SPEC-DoiExtractor-IpumsUriBuilder-local-environment.xml +5 -0
  48. data/spec/reports/SPEC-DoiExtractor-IpumsUriBuilder.xml +3 -0
  49. data/spec/reports/SPEC-DoiExtractor-Options-for-command-with-create-command-with-invalid-doi-version.xml +5 -0
  50. data/spec/reports/SPEC-DoiExtractor-Options-for-command-with-create-command.xml +7 -0
  51. data/spec/reports/SPEC-DoiExtractor-Options-for-command-with-download-command.xml +7 -0
  52. data/spec/reports/SPEC-DoiExtractor-Options-for-command-with-status-command.xml +7 -0
  53. data/spec/reports/SPEC-DoiExtractor-Options-for-command.xml +3 -0
  54. data/spec/reports/SPEC-DoiExtractor-Options-when-setting-path-values.xml +9 -0
  55. data/spec/reports/SPEC-DoiExtractor-Options.xml +5 -0
  56. data/spec/reports/SPEC-DoiExtractor-Secrets.xml +7 -0
  57. data/spec/reports/SPEC-DoiExtractor-StatusCommand.xml +5 -0
  58. data/spec/spec_helper.rb +20 -0
  59. data/spec/support/test_input.rb +36 -0
  60. data/spec/unit/cancel_command_spec.rb +28 -0
  61. data/spec/unit/command_line_parser_spec.rb +68 -0
  62. data/spec/unit/create_command_spec.rb +44 -0
  63. data/spec/unit/download_command_spec.rb +139 -0
  64. data/spec/unit/download_location_spec.rb +71 -0
  65. data/spec/unit/ipums_client_spec.rb +23 -0
  66. data/spec/unit/ipums_uri_builder_spec.rb +26 -0
  67. data/spec/unit/options_spec.rb +86 -0
  68. data/spec/unit/secrets_spec.rb +14 -0
  69. data/spec/unit/status_command_spec.rb +46 -0
  70. metadata +282 -0
@@ -0,0 +1,154 @@
1
+ module DoiExtractor
2
+ class CommandLineParser
3
+
4
+ attr_reader :top_level_parser, :command_parsers, :command_parser, :options, :error
5
+
6
+ def initialize
7
+ @command_parsers = {
8
+ 'status' => OptionParser.new do |opts|
9
+ opts.banner = "Usage: #{File.basename($0)} status [options]"
10
+ opts.version = VERSION
11
+ default_opts(opts)
12
+ api_opts(opts)
13
+ path_opts(opts)
14
+ extract_group_opts(opts)
15
+ end,
16
+
17
+ 'create' => OptionParser.new do |opts|
18
+ opts.banner = "Usage: #{File.basename($0)} create [options]"
19
+ opts.version = VERSION
20
+ default_opts(opts)
21
+ api_opts(opts)
22
+
23
+ opts.on(:REQUIRED, '-v', '--doi-version DOI_VERSION', 'DOI version') do |v|
24
+ self.options.doi_version = v
25
+ end
26
+
27
+ opts.on('-y', '--year YEAR', 'Specify year (defaults to current year)') do |v|
28
+ self.options.year = v
29
+ end
30
+
31
+ opts.on('-E', '--email EMAIL', 'Specify email address to notify when all extracts are complete') do |v|
32
+ self.options.email = v
33
+ end
34
+ end,
35
+
36
+ 'download' => OptionParser.new do |opts|
37
+ opts.banner = "Usage: #{File.basename($0)} download [options]"
38
+ opts.version = VERSION
39
+ default_opts(opts)
40
+ api_opts(opts)
41
+ path_opts(opts)
42
+ extract_group_opts(opts)
43
+ force_opts(opts, 'Downloads')
44
+ end,
45
+
46
+ 'cancel' => OptionParser.new do |opts|
47
+ opts.banner = "Usage: #{File.basename($0)} cancel [options]"
48
+ opts.version = VERSION
49
+ default_opts(opts)
50
+ api_opts(opts)
51
+ extract_group_opts(opts)
52
+ force_opts(opts, 'Cancels')
53
+ end
54
+ }
55
+
56
+ @top_level_parser = OptionParser.new do |opts|
57
+ opts.version = VERSION
58
+ opts.banner = "Usage: #{File.basename($0)} <COMMAND> [options]\nAvailable Commands: #{@command_parsers.keys.join(', ')}\n\nSee '#{File.basename($0)} <COMMAND> --help' for more information on a specific command"
59
+ end
60
+ end
61
+
62
+ def parse(args)
63
+ begin
64
+ @top_level_parser.order!(args)
65
+
66
+ command = args.shift.to_s.downcase
67
+ @command_parser = @command_parsers[command]
68
+
69
+ if @command_parser
70
+ @options = Options.for_command(command)
71
+ @command_parser.order!(args)
72
+ if @options.valid?
73
+ @valid = true
74
+ else
75
+ @valid = false
76
+ @error = @options.errors.join("\n")
77
+ end
78
+ else
79
+ @valid = false
80
+ @error = 'Invalid Command'
81
+ end
82
+ rescue OptionParser::ParseError => err
83
+ @error = err
84
+ @valid = false
85
+ end
86
+ end
87
+
88
+ def valid?
89
+ @valid
90
+ end
91
+
92
+ def to_s
93
+ [error, @command_parser || @top_level_parser].compact.join("\n\n")
94
+ end
95
+
96
+ private
97
+
98
+ def default_opts(opts)
99
+ opts.on('-e', '--environment ENVIRONMENT', Options::ENVIRONMENTS, "Select environment (defaults to live; values: #{Options::ENVIRONMENTS.join(', ')})") do |v|
100
+ self.options.environment = v
101
+ end
102
+
103
+ opts.on('-p', '--project PROJECT', Options::PROJECTS, "Select project (required; values: #{Options::PROJECTS.join(', ')})") do |v|
104
+ self.options.project = v
105
+ end
106
+
107
+ opts.on('-V', '--[no-]verbose', 'Run verbosely') do |v|
108
+ self.options.verbose = v
109
+ end
110
+ end
111
+
112
+ def api_opts(opts)
113
+ opts.on('-a', '--api-path URI', URI, 'API path (calculated from env and project by default)') do |v|
114
+ self.options.api_uri = v
115
+ end
116
+
117
+ opts.on('--api-user USER', 'API username (Reads config from /pkg by default)') do |v|
118
+ self.options.api_username = v
119
+ end
120
+
121
+ opts.on('--api-password PASSWORD', 'API password (Reads config from /pkg by default)') do |v|
122
+ self.options.api_password = v
123
+ end
124
+ end
125
+
126
+ def path_opts(opts)
127
+ opts.on('-b', '--download-base-path PATH', 'Override download path') do |v|
128
+ self.options.download_base_path = v
129
+ end
130
+ end
131
+
132
+ def extract_group_opts(opts)
133
+ opts.on('-i', '--id ID', Integer, 'DOI Extract ID') do |v|
134
+ self.options.extract_group_id = v
135
+ end
136
+ end
137
+
138
+ def force_opts(opts, verb)
139
+ opts.on('-f', '--force', "#{verb} without prompting") do |v|
140
+ self.options.force = v
141
+ end
142
+ end
143
+
144
+ class << self
145
+ private :new
146
+
147
+ def parse(args)
148
+ opts = new
149
+ opts.parse(args.dup)
150
+ opts
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,32 @@
1
+ module DoiExtractor
2
+ class CreateCommand < Command
3
+
4
+ def _execute
5
+
6
+ say('checking existing DOI extracts', true)
7
+ if any_current_doi_extracts?
8
+ fail('A DOI Extract is already in progress. Nothing created.')
9
+ end
10
+
11
+ say('Creating DOI extract group...')
12
+ ex = ipums_client.create_doi_extract(options.doi_version, options.year, options.email)
13
+
14
+ fail('Server did not return any data; extract status is unknown') unless ex
15
+ fail("Server created DOI extract with ID #{ex.id} and invalid status #{ex.status}") unless ex.status == 'empty'
16
+
17
+ say("DOI Extract with ID #{ex.id} submitted. To check status run the following command:\n doi_extractor status -p #{options.project} -e #{options.environment} -i #{ex.id}\n")
18
+
19
+ if options.email
20
+ say("An email will be sent to #{options.email} after the last extract has finished. At that time you will be able to download and archive the extracts.")
21
+ end
22
+
23
+ end
24
+
25
+ def any_current_doi_extracts?
26
+ in_progress_status = ['empty', 'waiting_to_populate', 'populating', 'unsubmitted', 'waiting_to_submit', 'submitting', 'submitted']
27
+ recent = ipums_client.recent_doi_extracts
28
+ recent.any? { |e| in_progress_status.include?(e.status) }
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,109 @@
1
+ module DoiExtractor
2
+ class DownloadCommand < Command
3
+
4
+ def _execute
5
+ say('retrieving DOI extract', true)
6
+ doi_extract = ipums_client.get_doi_extract(options.extract_group_id)
7
+
8
+ unless doi_extract
9
+ fail("no DOI extract with ID #{id} found")
10
+ end
11
+
12
+ unless doi_extract.all_data_available
13
+ fail('The specified DOI extract group is not ready to download')
14
+ end
15
+
16
+ location = DownloadLocation.new(options.download_base_path, doi_extract)
17
+
18
+ if options.force
19
+ say('skipping user confirmation', true)
20
+ else
21
+ i = ask("download DOI extract files to #{location.path}?")
22
+ if i.to_s.downcase[0...1] != 'y'
23
+ fail('download cancelled')
24
+ end
25
+ end
26
+
27
+ location.ensure!
28
+
29
+ doi_extract.extract_requests.each_with_index do |er, idx|
30
+
31
+ if location.complete_extract_request?(er)
32
+ say("ER #{er.id} already downloaded")
33
+ else
34
+ say_nb("ER #{er.id} downloading (#{idx + 1} of #{doi_extract.extract_requests.count})...")
35
+ location.clean_tmp!(er)
36
+ er.files.each do |f|
37
+ save_path = File.join(location.extract_request_tmp_path(er), f.filename)
38
+ ipums_client.download_extract_file(save_path, er.id, f.ext)
39
+
40
+ # Special handling of ddi files
41
+ if f.ext.downcase == 'xml'
42
+ FileUtils.cp(save_path, location.ddi_path)
43
+ end
44
+
45
+ end
46
+ say(location.package_tmp_to_extract(er), true)
47
+ say("Complete")
48
+ end
49
+ end
50
+
51
+ location.clean_tmp!
52
+
53
+ codebook_er = doi_extract.codebook_extract
54
+ full_ddi = codebook_er ? codebook_er.files.detect { |f| f.ext == 'xml' } : nil
55
+
56
+ if full_ddi.nil?
57
+ fail('Missing full DDI codebook')
58
+ end
59
+
60
+ ipums_client.download_extract_file(location.complete_ddi_path, codebook_er.id, 'xml')
61
+
62
+
63
+ # Write summary file to output location
64
+ File.open(location.summary_file_path, 'w') do |f|
65
+ f.puts "File generated on #{Time.now.strftime('%c')}"
66
+ f.puts
67
+ f.puts "Extract Request Group ID #{doi_extract.id}"
68
+ f.puts "DOI Version v#{doi_extract.doi_version}"
69
+ f.puts
70
+ f.puts "Citation:"
71
+ f.puts doi_extract.citation
72
+ f.puts
73
+ f.puts 'Included Extracts:'
74
+
75
+ write_extract_summary(f, location, doi_extract)
76
+ end
77
+
78
+ location.remove_tmp
79
+
80
+ end
81
+
82
+ def write_extract_summary(file, location, doi_extract)
83
+
84
+ headers = ['ID', 'Sample', 'Archive Filename', 'Data Filename']
85
+ spacer = 4
86
+
87
+ extract_data = doi_extract.extract_requests.map do |er|
88
+ [er.id.to_s, location.extract_request_name(er), location.extract_request_filename(er), er.extract_file_name]
89
+ end
90
+
91
+ max_lengths = (extract_data + [headers]).reduce([0, 0, 0, 0]) do |lengths, data|
92
+ lengths.zip(data).map { |l, d| [l, d.length].max }
93
+ end
94
+
95
+ fmt = "%-#{max_lengths.first + spacer}s"
96
+ max_lengths[1..-1].each do |l|
97
+ fmt << "%#{l + spacer}s"
98
+ end
99
+
100
+ file.puts sprintf(fmt, *headers)
101
+
102
+ extract_data.each do |d|
103
+ file.puts sprintf(fmt, *d)
104
+ end
105
+
106
+ end
107
+
108
+ end
109
+ end
@@ -0,0 +1,86 @@
1
+ module DoiExtractor
2
+ class DownloadLocation
3
+
4
+ attr_reader :path
5
+
6
+ def initialize(base_path, extract_group)
7
+ @extract_group = extract_group
8
+ @base_path = base_path
9
+ if extract_group.nil? || extract_group.year.nil? || extract_group.doi_version.nil?
10
+ raise 'Cannot calculate path: missing doi extract data'
11
+ end
12
+ @path = File.join(base_path, "#{extract_group.year}_v#{extract_group.doi_version}")
13
+ end
14
+
15
+ def exist?
16
+ Dir.exist? path
17
+ end
18
+
19
+ def ensure!
20
+ FileUtils.mkdir_p path
21
+ FileUtils.mkdir_p ddi_path
22
+ end
23
+
24
+ def tmp_path
25
+ File.join(path, 'tmp')
26
+ end
27
+
28
+ def remove_tmp
29
+ if Dir.exist?(tmp_path)
30
+ FileUtils.rm_rf tmp_path
31
+ end
32
+ end
33
+
34
+ def clean_tmp!(er = nil)
35
+ remove_tmp
36
+
37
+ FileUtils.mkdir_p tmp_path
38
+ if er
39
+ FileUtils.mkdir_p extract_request_tmp_path(er)
40
+ end
41
+ end
42
+
43
+ def summary_file_path
44
+ File.join(path, 'summary.txt')
45
+ end
46
+
47
+ def complete_ddi_path
48
+ File.join(ddi_path, 'complete.xml')
49
+ end
50
+
51
+ def ddi_path
52
+ File.join(path, 'ddi')
53
+ end
54
+
55
+ def package_tmp_to_extract(er)
56
+ cmd = %[tar -czvf "#{extract_request_path(er)}" -C "#{tmp_path}" "#{extract_request_name(er)}"]
57
+ output = `#{cmd} 2>&1`
58
+ raise "error running command: #{cmd}\n#{output}" unless $?.success?
59
+ output
60
+ end
61
+
62
+ def extract_request_tmp_path(er)
63
+ File.join(tmp_path, extract_request_name(er))
64
+ end
65
+
66
+
67
+ def complete_extract_request?(er)
68
+ file = extract_request_path(er)
69
+ File.exist?(file)
70
+ end
71
+
72
+ def extract_request_path(er)
73
+ File.join(path, extract_request_filename(er))
74
+ end
75
+
76
+ def extract_request_filename(er)
77
+ "#{extract_request_name(er)}.tar.gz"
78
+ end
79
+
80
+ def extract_request_name(er)
81
+ s = er.samples.first
82
+ [s.name, s.long_description].compact.join(' - ')
83
+ end
84
+
85
+ end
86
+ end
@@ -0,0 +1,34 @@
1
+ module DoiExtractor
2
+
3
+ class CommandFailError < StandardError
4
+
5
+ attr_reader :inner_exception, :exit_code
6
+
7
+ def initialize(message, inner_exception = nil, exit_code = 1)
8
+ super(message)
9
+ @inner_exception = inner_exception
10
+ @exit_code = exit_code
11
+ end
12
+
13
+ def error_report
14
+ report = []
15
+
16
+ report << "Failed: #{self.message}"
17
+
18
+ if inner_exception
19
+ report << "caused by: #{inner_exception.class}: #{inner_exception.message}"
20
+ inner_exception.backtrace.each do |l|
21
+ report << " #{l}"
22
+ end
23
+ end
24
+
25
+ report << ' '
26
+
27
+ report.join("\n")
28
+ end
29
+ end
30
+
31
+ class ApiError < StandardError
32
+ end
33
+
34
+ end
@@ -0,0 +1,159 @@
1
+ module DoiExtractor
2
+ class IpumsClient
3
+
4
+ DEFAULT_READ_TIMEOUT = 120
5
+
6
+ attr_reader :uri, :username, :password
7
+
8
+ def initialize(uri, username, password)
9
+ case uri
10
+ when String
11
+ @uri = URI(uri)
12
+ when URI
13
+ @uri = uri
14
+ else
15
+ raise 'Invalid uri'
16
+ end
17
+
18
+ @username = username
19
+ @password = password
20
+ end
21
+
22
+ def create_doi_extract(doi_version, year, email)
23
+ res = post('api/extracts/v1/extract_request_groups', {type: 'doi', year: year, doi_version: doi_version, email: email}, 600)
24
+ parse_body(res)
25
+ end
26
+
27
+ def submit_doi_extract(id)
28
+ res = post("api/extracts/v1/extract_request_groups/#{id}/submit")
29
+ parse_body(res)
30
+ end
31
+
32
+ def recent_doi_extracts
33
+ res = get('api/extracts/v1/extract_request_groups/recent', {type: 'doi'})
34
+ parse_body(res)
35
+ end
36
+
37
+ def get_doi_extract(id)
38
+ res = get("api/extracts/v1/extract_request_groups/#{id}")
39
+ parse_body(res)
40
+ end
41
+
42
+ def cancel_doi_extract(id)
43
+ res = post("api/extracts/v1/extract_request_groups/#{id}/cancel")
44
+ parse_body(res)
45
+ end
46
+
47
+ def submit_extract(id)
48
+ res = post("api/extracts/v1/extract_requests/#{id}/submit")
49
+ parse_body(res)
50
+ end
51
+
52
+ def download_extract_file(file_path, extract_request_id, extension)
53
+ path = "api/extracts/v1/extract_requests/#{extract_request_id}/file/#{extension}"
54
+ File.open(file_path, 'w') do |f|
55
+ perform_request('get', path, nil, f, 600)
56
+ end
57
+ end
58
+
59
+ def get(path, params = nil)
60
+ io = StringIO.new
61
+ perform_request('get', path, params, io)
62
+ io.string
63
+ end
64
+
65
+ def post(path, params = nil, read_timeout = DEFAULT_READ_TIMEOUT)
66
+ io = StringIO.new
67
+ perform_request('post', path, params, io, read_timeout)
68
+ io.string
69
+ end
70
+
71
+ def perform_request(method, path, params, response_io, read_timeout = DEFAULT_READ_TIMEOUT, redirect_limit = 5)
72
+ raise ApiError, 'too many HTTP redirects' if redirect_limit == 0
73
+
74
+ request_uri = self.uri.dup
75
+ request_uri.path = '/' + combine_uri_paths(request_uri.path, path)
76
+
77
+ Net::HTTP.start(uri.host, uri.port, {use_ssl: uri.scheme == 'https'}) do |http|
78
+ # in seconds
79
+ http.read_timeout = read_timeout
80
+ request = build_request(method, request_uri, params)
81
+
82
+ request.basic_auth(username, password)
83
+ request.add_field('Accept', 'application/json')
84
+
85
+ redirect_location = nil
86
+
87
+ http.request(request) do |response|
88
+ case response
89
+ when Net::HTTPSuccess
90
+ response.read_body do |chunk|
91
+ response_io.write chunk
92
+ end
93
+ when Net::HTTPRedirection
94
+ redirect_location = response['location']
95
+ when Net::HTTPUnprocessableEntity
96
+ body = response.body
97
+ body = body && body != '' ? JSON.parse(body) : nil
98
+ msg = body.map { |k, v| "#{k}: #{v}" }.join(', ')
99
+ raise ApiError, "API Server returned an error: #{msg}"
100
+ else
101
+ raise ApiError, "Invalid API Server Response from [#{method.to_s.upcase} #{path}]: #{response.code} - #{response.message}"
102
+ end
103
+ end
104
+
105
+ if redirect_location
106
+ perform_request('get', redirect_location, nil, response_io, read_timeout, redirect_limit - 1)
107
+ end
108
+
109
+ end
110
+ end
111
+
112
+ def combine_uri_paths(*parts)
113
+ parts.compact.map(&:to_s).map(&:strip).map { |p| p.sub(/^\/+/, '').sub(/\/+$/, '') }.join('/')
114
+ end
115
+
116
+ def build_request(method, request_uri, params)
117
+ case method.to_s.downcase
118
+ when 'get'
119
+ case params
120
+ when Hash
121
+ request_uri.query = URI.encode_www_form(params)
122
+ when String
123
+ request_uri.query = params
124
+ when nil
125
+ # Do nothing
126
+ else
127
+ raise ApiError, "Invalid GET params: #{params}"
128
+ end
129
+ Net::HTTP::Get.new(request_uri.request_uri)
130
+ when 'post'
131
+ r = Net::HTTP::Post.new(request_uri.request_uri)
132
+ case params
133
+ when Hash
134
+ r.set_content_type('application/json')
135
+ r.body = JSON.generate(params)
136
+ when String
137
+ r.set_content_type('application/json')
138
+ r.body = params
139
+ when nil
140
+ # Do nothing
141
+ else
142
+ raise ApiError, "Invalid POST params: #{params}"
143
+ end
144
+ r
145
+ else
146
+ raise ApiError, "HTTP method #{method} not implemented"
147
+ end
148
+ end
149
+
150
+ def parse_body(body)
151
+ if body && body != ''
152
+ JSON.parse(body, object_class: OpenStruct)
153
+ else
154
+ nil
155
+ end
156
+ end
157
+
158
+ end
159
+ end