timely-app 1.3.1 → 1.3.3

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -22
  3. data/README.md +20 -1
  4. data/bin/timely-app +6 -118
  5. data/lib/timely-app/cli.rb +166 -0
  6. data/lib/timely-app/client/accounts.rb +1 -1
  7. data/lib/timely-app/client/auth.rb +1 -1
  8. data/lib/timely-app/client/clients.rb +1 -1
  9. data/lib/timely-app/client.rb +63 -26
  10. data/lib/timely-app/link_header.rb +2 -2
  11. data/lib/timely-app/params.rb +5 -4
  12. data/lib/timely-app/record.rb +3 -3
  13. data/lib/timely-app/response.rb +20 -12
  14. data/lib/timely-app.rb +18 -17
  15. data/sig/timely-app/cli.rbs +33 -0
  16. data/sig/timely-app/client/accounts.rbs +10 -0
  17. data/sig/timely-app/client/auth.rbs +8 -0
  18. data/sig/timely-app/client/clients.rbs +12 -0
  19. data/sig/timely-app/client/events.rbs +36 -0
  20. data/sig/timely-app/client/forecasts.rbs +12 -0
  21. data/sig/timely-app/client/labels.rbs +16 -0
  22. data/sig/timely-app/client/permissions.rbs +8 -0
  23. data/sig/timely-app/client/projects.rbs +14 -0
  24. data/sig/timely-app/client/reports.rbs +8 -0
  25. data/sig/timely-app/client/roles.rbs +6 -0
  26. data/sig/timely-app/client/teams.rbs +14 -0
  27. data/sig/timely-app/client/user_capacities.rbs +8 -0
  28. data/sig/timely-app/client/users.rbs +16 -0
  29. data/sig/timely-app/client/webhooks.rbs +14 -0
  30. data/sig/timely-app/client.rbs +24 -0
  31. data/sig/timely-app/errors.rbs +24 -0
  32. data/sig/timely-app/link_header.rbs +8 -0
  33. data/sig/timely-app/params.rbs +14 -0
  34. data/sig/timely-app/record.rbs +15 -0
  35. data/sig/timely-app/response.rbs +10 -0
  36. data/sig/timely-app.rbs +6 -0
  37. data/timely-app.gemspec +23 -18
  38. metadata +132 -14
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 39b5789f35dad839ca62c03bd156cdc9248aa0d289abdadeef5a3e4defe1fe46
4
- data.tar.gz: 74efd8e824d9d7d6f82f7833957aee8da21d705c7136001aabf72736e93d202b
3
+ metadata.gz: 9d8ce8174d129b5c2273101c2c00911260f27c6ace3f2cf674920a5b7b3c20b1
4
+ data.tar.gz: bc5c6f0bb086d3091e820f28ea278786ee4a54562df0bc5b9e151610d7a9b49e
5
5
  SHA512:
6
- metadata.gz: 7d5baa1765e2dd3dd038e11f8f498c157d49275b807f028766d0863529e7a2168369a5ded2f633c28107bb7937aecc3c10e30422e26ce947d44ce473b1feb7fb
7
- data.tar.gz: 4d3c9074a7a56f9412324e3aa513206729fd80af9e539badcb5168b58f0f70dc0518a587ddd4f786f2b129955841061867acc2f10f6af5f9d8e40cad1af99db3
6
+ metadata.gz: 20abf6c65cf3558f079ed82e1b95245a66fac3e7301bdf6f3a8039e7ad20044efce9dddc2dee3c054ba8a18dbdc25437b8de934a37784f9521ce7c30cf34a6be
7
+ data.tar.gz: a84b5732a9a539ee9a2133f370748ee32d2dbff90cf916dc5a2bfa08817e1f90ce53dee7acafef8e887de6a5a40de763e2b250cb7bcc9ac72662fd18d70e3069
data/CHANGELOG.md CHANGED
@@ -1,41 +1,60 @@
1
- # 1.3.0
1
+ # CHANGELOG
2
2
 
3
- * Set minimum ruby version requirement to 2.5.0
3
+ ## 1.3.3 (2026-10-29)
4
4
 
5
- # 1.2.0
5
+ - Preserve HTTP error types when the server returns malformed or unexpected JSON.
6
+ - Keep authorization codes and response contents out of failed sign-in diagnostics.
7
+ - Cancel release preparation when confirmation input is closed.
8
+ - Exit sign-in when the authorization code prompt is closed or blank.
6
9
 
7
- * Fix version definition
10
+ ## 1.3.2 (2026-07-25)
8
11
 
9
- # 1.1.1
12
+ - BREAKING: Require Ruby 3.4 or newer
13
+ - Fix `get_clients` to call `/1.1/:account_id/clients` instead of `/1.1/accounts`
14
+ - Move `TimelyApp::Cli` under `lib/`
10
15
 
11
- * Update gem configuration
16
+ ## 1.3.1 (2023-09-30)
12
17
 
13
- # 1.1.0
18
+ - Refresh gemfile lock
14
19
 
15
- * Update ruby version to 3.2.2
16
- * Update dependencies
20
+ ## 1.3.0 (2023-08-30)
17
21
 
18
- # 1.0.5
22
+ - Set minimum ruby version requirement to 2.5.0
19
23
 
20
- * Fix verbose mode check
24
+ ## 1.2.0 (2023-08-29)
21
25
 
22
- # 1.0.4
26
+ - Fix version definition
23
27
 
24
- * Update CLI to check input properly and handle command arguments
28
+ ## 1.1.1 (2023-08-29)
25
29
 
26
- # 1.0.3
30
+ - Update gem configuration
27
31
 
28
- * Fix gemspec
32
+ ## 1.1.0 (2023-08-15)
29
33
 
30
- # 1.0.2
34
+ - Update ruby version to 3.2.2
35
+ - Update dependencies
31
36
 
32
- * Add basic CLI script `timely-app`
37
+ ## 1.0.5 (2023-03-02)
33
38
 
34
- # 1.0.1
39
+ - Fix verbose mode check
35
40
 
36
- * Fix oauth token request
37
- * Add example web service for CLI authorization
41
+ ## 1.0.4 (2023-02-14)
38
42
 
39
- # 1.0.0
43
+ - Update CLI to check input properly and handle command arguments
40
44
 
41
- * Initial version
45
+ ## 1.0.3 (2023-02-14)
46
+
47
+ - Fix gemspec
48
+
49
+ ## 1.0.2 (2023-02-14)
50
+
51
+ - Add basic CLI script `timely-app`
52
+
53
+ ## 1.0.1 (2023-02-14)
54
+
55
+ - Fix oauth token request
56
+ - Add example web service for CLI authorization
57
+
58
+ ## 1.0.0 (2023-02-14)
59
+
60
+ - Initial version
data/README.md CHANGED
@@ -1,19 +1,25 @@
1
1
  # timely-app
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/timely-app.svg)](https://badge.fury.io/rb/timely-app) [![Test Status](https://github.com/amkisko/timely-app/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/timely-app/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/timely_app.rb/graph/badge.svg?token=QL3A7ATCRK)](https://codecov.io/gh/amkisko/timely_app.rb)
3
+ [![Gem Version](https://badge.fury.io/rb/timely-app.svg)](https://badge.fury.io/rb/timely-app) [![Test Status](https://github.com/amkisko/timely-app/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/timely-app/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/timely_app.rb/graph/badge.svg?token=QL3A7ATCRK)](https://app.codecov.io/github/amkisko/timely_app.rb)
4
4
 
5
5
  Ruby client for the [Timely API](https://dev.timelyapp.com).
6
6
 
7
7
  Sponsored by [Kisko Labs](https://www.kiskolabs.com).
8
8
 
9
+ <a href="https://www.kiskolabs.com">
10
+ <img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
11
+ </a>
12
+
9
13
  ## Install
10
14
 
11
15
  Using Bundler:
16
+
12
17
  ```sh
13
18
  bundle add timely-app
14
19
  ```
15
20
 
16
21
  Using RubyGems:
22
+
17
23
  ```sh
18
24
  gem install timely-app
19
25
  ```
@@ -99,6 +105,19 @@ gem build timely-app.gemspec
99
105
  gem push timely-app-*.gem
100
106
  ```
101
107
 
108
+ ## Links
109
+
110
+ - [GitHub](https://github.com/amkisko/timely_app.rb)
111
+ - [GitLab](https://gitlab.com/amkisko/timely_app.rb)
112
+ - [RubyGems](https://rubygems.org/gems/timely-app)
113
+ - [Versions Atom](https://rubygems.org/gems/timely-app/versions.atom) (feed id `5816973660388`)
114
+ - [libraries.io](https://libraries.io/rubygems/timely-app)
115
+ - [Deps.dev](https://deps.dev/rubygems/timely-app)
116
+ - [SonarCloud](https://sonarcloud.io/project/overview?id=amkisko_timely_app.rb)
117
+ - [Snyk](https://snyk.io/test/github/amkisko/timely_app.rb)
118
+ - [Codecov](https://app.codecov.io/github/amkisko/timely_app.rb)
119
+ - [OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/amkisko/timely_app.rb)
120
+
102
121
  ## License
103
122
 
104
123
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/bin/timely-app CHANGED
@@ -4,121 +4,6 @@ require "yaml"
4
4
  require "optparse"
5
5
  require "timely-app"
6
6
 
7
- module TimelyApp
8
- class CLI
9
- attr_reader :options, :client
10
-
11
- def initialize(options = {})
12
- @options = options
13
- @client = TimelyApp::Client.new(
14
- access_token: options[:access_token] || fetch_access_token,
15
- account_id: options[:account_id] || fetch_account_id,
16
- verbose: options[:verbose]
17
- )
18
- end
19
-
20
- def get_config(key)
21
- read_config_file&.fetch(key, nil)
22
- end
23
-
24
- def set_config(key, value)
25
- save_config_file(key => value)
26
- end
27
-
28
- def command_exists?(cmd)
29
- return false if cmd.nil? || cmd.empty?
30
-
31
- client.respond_to?(cmd)
32
- end
33
-
34
- def call(cmd, *args)
35
- client.send(cmd, *args)
36
- end
37
-
38
- def auth(client_id, client_secret)
39
- if !client_id || !client_secret
40
- puts "Usage: timely-app auth CLIENT_ID CLIENT_SECRET"
41
- exit 1
42
- end
43
-
44
- auth_client = TimelyApp::Client.new(verbose: options[:verbose])
45
- auth_url = auth_client.get_oauth_authorize_url(
46
- client_id: client_id,
47
- redirect_uri: "urn:ietf:wg:oauth:2.0:oob"
48
- )
49
- puts "Visit this URL in your browser:"
50
- puts auth_url
51
- puts "\nEnter authorization code here:"
52
- code = gets.chomp
53
- begin
54
- token = auth_client.post_oauth_token(
55
- client_id: client_id,
56
- client_secret: client_secret,
57
- code: code,
58
- redirect_uri: "urn:ietf:wg:oauth:2.0:oob",
59
- grant_type: "authorization_code"
60
- )
61
- if options[:save]
62
- save_config_file(
63
- access_token: token.access_token,
64
- refresh_token: token.refresh_token,
65
- created_at: token.created_at
66
- )
67
- else
68
- puts "\nAccess token:\n#{token.access_token}\n"
69
- end
70
- if options[:verbose]
71
- puts "Token details: #{token.to_h}"
72
- end
73
- rescue TimelyApp::Error => e
74
- puts "Authentication failed"
75
- if options[:verbose]
76
- puts "Code: #{code}"
77
- puts "Response code: #{e.response.code}"
78
- puts "Response: #{e.response.body}"
79
- end
80
- exit 1
81
- end
82
- end
83
-
84
- private
85
-
86
- def config_file_path
87
- Dir.home + "/.timelyrc"
88
- end
89
-
90
- def read_config_file
91
- if File.exist?(config_file_path)
92
- YAML.load_file(config_file_path)
93
- end
94
- end
95
-
96
- def fetch_account_id
97
- read_config_file&.fetch("account_id", nil)
98
- end
99
-
100
- def fetch_access_token
101
- read_config_file&.fetch("access_token", nil)
102
- end
103
-
104
- def check_access_token
105
- if !fetch_access_token
106
- puts "No access token found. Run `timely-app auth` to get one."
107
- exit 1
108
- end
109
- end
110
-
111
- def save_config_file(**options)
112
- config = read_config_file || {}
113
- config.merge!(options)
114
- File.open(config_file_path, "w") do |f|
115
- f.write(config.to_yaml)
116
- end
117
- puts "Saved to #{config_file_path}"
118
- end
119
- end
120
- end
121
-
122
7
  options = {}
123
8
  OptionParser.new do |opts|
124
9
  opts.banner = "Usage: timely-app [command] [options]"
@@ -141,8 +26,8 @@ cli = TimelyApp::CLI.new(options)
141
26
  cmd = ARGV.shift
142
27
  case cmd
143
28
  when "auth"
144
- client_id = ARGV.shift || ENV.fetch('TIMELY_CLIENT_ID')
145
- client_secret = ARGV.shift || ENV.fetch('TIMELY_CLIENT_SECRET')
29
+ client_id = ARGV.shift || ENV.fetch("TIMELY_CLIENT_ID")
30
+ client_secret = ARGV.shift || ENV.fetch("TIMELY_CLIENT_SECRET")
146
31
  ARGV.clear
147
32
  if client_id && client_secret
148
33
  cli.auth(client_id, client_secret)
@@ -161,7 +46,10 @@ when "config"
161
46
  end
162
47
  else
163
48
  if cli.command_exists?(cmd)
164
- args = ARGV.reduce({}){ |h, arg| k, v = arg.split("="); h[k] = v; h }
49
+ args = ARGV.each_with_object({}) { |arg, h|
50
+ k, v = arg.split("=")
51
+ h[k] = v
52
+ }
165
53
  ARGV.clear
166
54
  response = cli.call(cmd, *args)
167
55
  case response
@@ -0,0 +1,166 @@
1
+ require "securerandom"
2
+ require "yaml"
3
+
4
+ module TimelyApp
5
+ class CLI
6
+ attr_reader :options, :client
7
+
8
+ def initialize(options = {})
9
+ @options = options
10
+ @client = TimelyApp::Client.new(
11
+ access_token: options[:access_token] || fetch_access_token,
12
+ account_id: options[:account_id] || fetch_account_id,
13
+ verbose: options[:verbose]
14
+ )
15
+ end
16
+
17
+ def get_config(key)
18
+ read_config_file&.fetch(key, nil)
19
+ end
20
+
21
+ def set_config(key, value)
22
+ save_config_file(key => value)
23
+ end
24
+
25
+ def command_exists?(cmd)
26
+ return false if cmd.nil? || cmd.empty?
27
+
28
+ client.respond_to?(cmd)
29
+ end
30
+
31
+ def call(cmd, *args)
32
+ client.send(cmd, *args)
33
+ end
34
+
35
+ def auth(client_id, client_secret)
36
+ validate_auth_credentials!(client_id, client_secret)
37
+
38
+ auth_client = TimelyApp::Client.new(verbose: options[:verbose])
39
+ code = authorize_via_browser(auth_client, client_id)
40
+ token = fetch_oauth_token(auth_client, client_id, client_secret, code)
41
+ persist_or_print_token(token)
42
+ rescue TimelyApp::Error => error
43
+ handle_auth_failure(error)
44
+ end
45
+
46
+ private
47
+
48
+ def validate_auth_credentials!(client_id, client_secret)
49
+ return if client_id && client_secret
50
+
51
+ puts "Usage: timely-app auth CLIENT_ID CLIENT_SECRET"
52
+ exit 1
53
+ end
54
+
55
+ def authorize_via_browser(auth_client, client_id)
56
+ auth_url = auth_client.get_oauth_authorize_url(
57
+ client_id: client_id,
58
+ redirect_uri: oauth_redirect_uri
59
+ )
60
+ puts "Visit this URL in your browser:"
61
+ puts auth_url
62
+ puts "\nEnter authorization code here:"
63
+ code = gets&.chomp
64
+ if code.nil? || code.empty?
65
+ puts "Authorization code is required"
66
+ exit 1
67
+ end
68
+ code
69
+ end
70
+
71
+ def oauth_redirect_uri
72
+ "urn:ietf:wg:oauth:2.0:oob"
73
+ end
74
+
75
+ def fetch_oauth_token(auth_client, client_id, client_secret, code)
76
+ auth_client.post_oauth_token(
77
+ client_id: client_id,
78
+ client_secret: client_secret,
79
+ code: code,
80
+ redirect_uri: oauth_redirect_uri,
81
+ grant_type: "authorization_code"
82
+ )
83
+ end
84
+
85
+ def persist_or_print_token(token)
86
+ if options[:save]
87
+ save_config_file(
88
+ access_token: token.access_token,
89
+ refresh_token: token.refresh_token,
90
+ created_at: token.created_at
91
+ )
92
+ else
93
+ puts "\nAccess token:\n#{token.access_token}\n"
94
+ end
95
+ puts "Authentication succeeded" if options[:verbose]
96
+ end
97
+
98
+ def handle_auth_failure(error)
99
+ puts "Authentication failed"
100
+ if options[:verbose]
101
+ puts "Response code: #{error.response.code}"
102
+ end
103
+ exit 1
104
+ end
105
+
106
+ def config_file_path
107
+ Dir.home + "/.timelyrc"
108
+ end
109
+
110
+ def read_config_file
111
+ return unless File.exist?(config_file_path)
112
+
113
+ config = YAML.safe_load_file(
114
+ config_file_path,
115
+ permitted_classes: [Symbol, Time],
116
+ aliases: false
117
+ )
118
+ return {} unless config.is_a?(Hash)
119
+
120
+ config.to_h { |key, value| [key.to_s, value] }
121
+ end
122
+
123
+ def fetch_account_id
124
+ read_config_file&.fetch("account_id", nil)
125
+ end
126
+
127
+ def fetch_access_token
128
+ read_config_file&.fetch("access_token", nil)
129
+ end
130
+
131
+ def check_access_token
132
+ unless fetch_access_token
133
+ puts "No access token found. Run `timely-app auth` to get one."
134
+ exit 1
135
+ end
136
+ end
137
+
138
+ def save_config_file(**options)
139
+ config = read_config_file || {}
140
+ config.merge!(options.to_h { |key, value| [key.to_s, value] })
141
+ write_config_file(config.to_yaml)
142
+ puts "Saved to #{config_file_path}"
143
+ end
144
+
145
+ def write_config_file(content)
146
+ temporary_path = "#{config_file_path}.#{Process.pid}.#{SecureRandom.hex(8)}.tmp"
147
+
148
+ File.open(temporary_path, File::WRONLY | File::CREAT | File::EXCL, 0o600) do |file|
149
+ file.write(content)
150
+ file.flush
151
+ file.fsync
152
+ end
153
+ File.rename(temporary_path, config_file_path)
154
+ File.chmod(0o600, config_file_path)
155
+ sync_config_directory
156
+ ensure
157
+ File.delete(temporary_path) if temporary_path && File.exist?(temporary_path)
158
+ end
159
+
160
+ def sync_config_directory
161
+ File.open(File.dirname(config_file_path), File::RDONLY, &:fsync)
162
+ rescue Errno::EINVAL
163
+ nil
164
+ end
165
+ end
166
+ end
@@ -4,7 +4,7 @@
4
4
  module TimelyApp
5
5
  class Client
6
6
  def get_accounts
7
- get('/1.1/accounts')
7
+ get("/1.1/accounts")
8
8
  end
9
9
 
10
10
  def get_account_activities(account_id, **params)
@@ -6,7 +6,7 @@ module TimelyApp
6
6
  def get_oauth_authorize_url(client_id:, redirect_uri:)
7
7
  host_uri_join(
8
8
  "/1.1/oauth/authorize",
9
- response_type: 'code',
9
+ response_type: "code",
10
10
  redirect_uri: redirect_uri,
11
11
  client_id: client_id
12
12
  )
@@ -8,7 +8,7 @@ module TimelyApp
8
8
  end
9
9
 
10
10
  def get_clients(**params)
11
- get('/1.1/accounts', params)
11
+ get("/1.1/#{account_id}/clients", **params)
12
12
  end
13
13
 
14
14
  def get_client(id)
@@ -1,29 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'timely-app/errors'
4
- require 'timely-app/link_header'
5
- require 'timely-app/params'
6
- require 'timely-app/record'
7
- require 'timely-app/response'
8
- require 'net/http'
9
- require 'json'
3
+ require "timely-app/errors"
4
+ require "timely-app/link_header"
5
+ require "timely-app/params"
6
+ require "timely-app/record"
7
+ require "timely-app/response"
8
+ require "net/http"
9
+ require "json"
10
10
 
11
11
  module TimelyApp
12
12
  class Client
13
+ DEFAULT_OPEN_TIMEOUT = 5
14
+ DEFAULT_READ_TIMEOUT = 30
15
+ FILTERED_VALUE = "[FILTERED]"
16
+ SENSITIVE_FIELD = /authorization|code|credential|password|secret|token/i
17
+
13
18
  attr_accessor :account_id
14
19
 
15
20
  def initialize(options = {})
16
- @auth_header = 'Authorization'
21
+ @auth_header = "Authorization"
17
22
  @auth_value = "Bearer #{options[:access_token]}"
18
23
  @user_agent = options.fetch(:user_agent) { "timely-app/#{VERSION} ruby/#{RUBY_VERSION}" }
19
24
 
20
- @host = 'api.timelyapp.com'
21
-
22
- @http = Net::HTTP.new(@host, Net::HTTP.https_default_port)
23
- @http.use_ssl = true
25
+ @host = "api.timelyapp.com"
26
+ @http = build_http(options)
24
27
 
25
28
  @account_id = options[:account_id]
26
- @verbose = options[:verbose] || !ENV['VERBOSE'].nil? || false
29
+ @verbose = options[:verbose] || !ENV["VERBOSE"].nil? || false
27
30
  end
28
31
 
29
32
  def get(path, params = nil)
@@ -32,12 +35,20 @@ module TimelyApp
32
35
 
33
36
  private
34
37
 
38
+ def build_http(options)
39
+ http = Net::HTTP.new(@host, Net::HTTP.https_default_port)
40
+ http.use_ssl = true
41
+ http.open_timeout = options.fetch(:open_timeout, DEFAULT_OPEN_TIMEOUT)
42
+ http.read_timeout = options.fetch(:read_timeout, DEFAULT_READ_TIMEOUT)
43
+ http
44
+ end
45
+
35
46
  def verbose?
36
47
  @verbose == true
37
48
  end
38
49
 
39
50
  def host_uri_join(path, params)
40
- URI::join("https://#{@host}", Params.join(path, params)).to_s
51
+ URI.join("https://#{@host}", Params.join(path, params)).to_s
41
52
  end
42
53
 
43
54
  def post(path, attributes)
@@ -53,24 +64,50 @@ module TimelyApp
53
64
  end
54
65
 
55
66
  def request(http_request, body_object = nil)
56
- http_request['User-Agent'] = @user_agent
67
+ apply_request_headers(http_request, body_object)
68
+ response = @http.request(http_request)
69
+ log_verbose_exchange(http_request, response)
70
+ raise Response.error(response) unless response.is_a?(Net::HTTPSuccess)
71
+
72
+ Response.parse(response)
73
+ end
74
+
75
+ def apply_request_headers(http_request, body_object)
76
+ http_request["User-Agent"] = @user_agent
57
77
  http_request[@auth_header] = @auth_value
58
78
 
59
- if body_object
60
- http_request['Content-Type'] = 'application/json'
61
- http_request.body = JSON.generate(body_object)
62
- end
79
+ return unless body_object
63
80
 
64
- response = @http.request(http_request)
81
+ http_request["Content-Type"] = "application/json"
82
+ http_request.body = JSON.generate(body_object)
83
+ end
65
84
 
66
- if verbose?
67
- puts ">> request: #{http_request.method} #{http_request.path} #{http_request.body}"
68
- puts "<< response: #{http_request.method} #{http_request.path} #{response.code} #{response.body}"
69
- end
85
+ def log_verbose_exchange(http_request, response)
86
+ return unless verbose?
70
87
 
71
- raise Response.error(response) unless response.is_a?(Net::HTTPSuccess)
88
+ puts ">> request: #{http_request.method} #{http_request.path} #{filtered_body(http_request.body)}"
89
+ puts "<< response: #{http_request.method} #{http_request.path} #{response.code} #{filtered_body(response.body)}"
90
+ end
72
91
 
73
- Response.parse(response)
92
+ def filtered_body(body)
93
+ return if body.nil? || body.empty?
94
+
95
+ JSON.generate(filter_sensitive_values(JSON.parse(body)))
96
+ rescue JSON::ParserError
97
+ FILTERED_VALUE
98
+ end
99
+
100
+ def filter_sensitive_values(value)
101
+ case value
102
+ when Hash
103
+ value.to_h { |key, nested_value|
104
+ [key, key.match?(SENSITIVE_FIELD) ? FILTERED_VALUE : filter_sensitive_values(nested_value)]
105
+ }
106
+ when Array
107
+ value.map { |nested_value| filter_sensitive_values(nested_value) }
108
+ else
109
+ value
110
+ end
74
111
  end
75
112
  end
76
113
  end
@@ -1,5 +1,5 @@
1
- require 'timely-app/record'
2
- require 'uri'
1
+ require "timely-app/record"
2
+ require "uri"
3
3
 
4
4
  module TimelyApp
5
5
  module LinkHeader
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
- require 'cgi'
2
+
3
+ require "cgi"
3
4
 
4
5
  module TimelyApp
5
6
  module Params
@@ -8,17 +9,17 @@ module TimelyApp
8
9
  def join(path, params = nil)
9
10
  return path if params.nil? || params.empty?
10
11
 
11
- path + '?' + encode(params)
12
+ path + "?" + encode(params)
12
13
  end
13
14
 
14
15
  def encode(params)
15
- params.map { |k, v| "#{escape(k)}=#{array_escape(v)}" }.join('&')
16
+ params.map { |k, v| "#{escape(k)}=#{array_escape(v)}" }.join("&")
16
17
  end
17
18
 
18
19
  private
19
20
 
20
21
  def array_escape(object)
21
- Array(object).map { |value| escape(value) }.join(',')
22
+ Array(object).map { |value| escape(value) }.join(",")
22
23
  end
23
24
 
24
25
  def escape(component)
@@ -14,13 +14,13 @@ module TimelyApp
14
14
 
15
15
  def method_missing(name, *args, &block)
16
16
  if @attributes.has_key?(name) && args.empty? && block.nil?
17
- return @attributes[name]
17
+ @attributes[name]
18
18
  else
19
- super name, *args, &block
19
+ super
20
20
  end
21
21
  end
22
22
 
23
- def respond_to_missing?(name, include_private = false)
23
+ def respond_to_missing?(name, _include_private = false)
24
24
  @attributes.has_key?(name)
25
25
  end
26
26