haveapi-go-client 0.28.4 → 0.29.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f8e8ca71c9d33fc75d52ba7763c7d474a51a2d17db5467d40fd769a285217cf
4
- data.tar.gz: 2ff6be50150de27d116dd272f0987d40d2eaa3f57ff09e8f71ce3a611ca0969e
3
+ metadata.gz: fa106a3e9f534f8b38eb7dad77a01c8a93e0eec07cf2ff1a847e1310e4b66445
4
+ data.tar.gz: e1606903a5b5991eda7ce2ae2ac7df59e623f7b2fadad2f45ec2e6cb84568f6b
5
5
  SHA512:
6
- metadata.gz: '0192a300f26c9562803e3ab6d1a9286b623be01e4a59e72686122c21a9f81cc52d2857a46caee4f3e1056ff705b9222af246bbd2ae90f663a7a527e432856688'
7
- data.tar.gz: f0320e8d633d6a3bd023b333213c86fc5cf0a81501967311b4c04333951aa194f17391473d90a8c9093eca950e28859d964128a2da1c0e680e1a6dfa657567f0
6
+ metadata.gz: 8850e3b1488d265ba0b20fc79a94cf4d9d9bf07c628d8538b4c00aae7a5f744536f637d3bd0f2ca4bb11afd56314b84e5971d624260f2230cf62d62b55fb85d2
7
+ data.tar.gz: e55cabd02c8f5751d9d7f81c46cba29e15b2c90ecb5036457bcb362091ca3c09e6a98e723d9a34bba4ef06a5208bd5ed3c5e7c9dd95cc4963a09baeb99cbba67
data/README.md CHANGED
@@ -46,6 +46,7 @@ import (
46
46
 
47
47
  func main() {
48
48
  api := client.New("https://api.vpsfree.cz")
49
+ api.SetLanguage("cs")
49
50
  api.SetBasicAuthentication("admin", "secret")
50
51
 
51
52
  action := api.Cluster.PublicStats.Prepare()
@@ -62,3 +63,17 @@ func main() {
62
63
  fmt.Printf("%+v\n", resp.Output)
63
64
  }
64
65
  ```
66
+
67
+ ## Localization
68
+
69
+ Generated clients include bundled HaveAPI client translations. Set the
70
+ language before making requests:
71
+
72
+ ```go
73
+ api := client.New("https://api.vpsfree.cz")
74
+ api.SetLanguage("cs")
75
+ ```
76
+
77
+ The value is sent in `Accept-Language` by default. Use
78
+ `SetLanguageHeader("X-Language")` when the API is configured with a custom
79
+ language header.
@@ -18,5 +18,5 @@ Gem::Specification.new do |spec|
18
18
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
19
  spec.require_paths = ['lib']
20
20
 
21
- spec.add_dependency 'haveapi-client', '~> 0.28.4'
21
+ spec.add_dependency 'haveapi-client', '~> 0.29.0'
22
22
  end
@@ -1,4 +1,5 @@
1
1
  require 'fileutils'
2
+ require 'yaml'
2
3
  require 'haveapi/client'
3
4
  require 'haveapi/go_client/utils'
4
5
 
@@ -50,12 +51,13 @@ module HaveAPI::GoClient
50
51
  FileUtils.mkpath(dst)
51
52
  end
52
53
 
53
- %w[client authentication request response types].each do |v|
54
+ %w[client authentication request response types i18n].each do |v|
54
55
  ErbTemplate.render_to_if_changed(
55
56
  "#{v}.go",
56
57
  {
57
58
  package:,
58
- api:
59
+ api:,
60
+ i18n_messages:
59
61
  },
60
62
  File.join(dst, "#{v}.go")
61
63
  )
@@ -74,5 +76,12 @@ module HaveAPI::GoClient
74
76
  protected
75
77
 
76
78
  attr_reader :api
79
+
80
+ def i18n_messages
81
+ @i18n_messages ||= YAML.safe_load_file(
82
+ File.expand_path('i18n_messages.yml', __dir__),
83
+ aliases: true
84
+ )
85
+ end
77
86
  end
78
87
  end
@@ -0,0 +1,82 @@
1
+ # This file is generated from i18n/haveapi.yml.
2
+ # Do not edit it manually; manual changes will be overwritten.
3
+ # Update i18n/haveapi.yml and run bundle exec rake i18n:update.
4
+ ---
5
+ en:
6
+ authentication:
7
+ callback_failed: "%{callback} failed: %{error}"
8
+ callback_invalid_return: callback has to return an array or 'stop'
9
+ callback_required: Implement callback %{callback}
10
+ invalid_credentials: invalid credentials
11
+ invalid_oauth2_pkce_verifier: Invalid OAuth2 PKCE verifier
12
+ invalid_oauth2_state: Invalid OAuth2 state
13
+ mfa_required: implement multi-factor authentication
14
+ multistep_callback_required: add callback to handle multi-step authentication
15
+ oauth2_access_token_required: Option access_token must be provided
16
+ oauth2_not_configured: OAuth2 authentication is not configured
17
+ oauth2_revoke_failed: Unable to revoke access token, HTTP %{status}
18
+ token_request_failed: 'Unable to request token: %{message}'
19
+ token_revoke_failed: 'Unable to revoke token: %{message}'
20
+ token_step_failed: 'Failed at authentication step ''%{action}'': %{message}'
21
+ unsupported_auth_action: Unsupported authentication action '%{action}'
22
+ errors:
23
+ access_forbidden: Access forbidden. Bad user name or password? Not authorized?
24
+ action_failed: "%{action} failed: %{message}"
25
+ fatal_api_error: 'Fatal API error: %{error}'
26
+ input_parameters_not_valid: input parameters not valid
27
+ input_parameters_not_valid_for_action: Input parameters not valid for action '%{action}'
28
+ invalid_input_parameters: invalid input parameters
29
+ uncancelable_action: 'Action state #%{id} cannot be cancelled'
30
+ unresolved_arguments: 'Unable to execute action ''%{action}'': unresolved arguments'
31
+ validation:
32
+ cannot_be_null: cannot be null
33
+ invalid_boolean: not a valid boolean
34
+ invalid_datetime: not in ISO 8601 format
35
+ invalid_float: not a valid float
36
+ invalid_input_layout: invalid input layout
37
+ invalid_integer: not a valid integer
38
+ invalid_resource_id: not a valid resource id
39
+ invalid_string: not a valid string
40
+ required_parameter_missing: required parameter missing
41
+ validation_failed: validation failed
42
+ validation_failed_with_errors: 'validation failed: %{errors}'
43
+ cs:
44
+ authentication:
45
+ callback_failed: "%{callback} selhal: %{error}"
46
+ callback_invalid_return: callback musí vrátit pole nebo 'stop'
47
+ callback_required: Implementujte callback %{callback}
48
+ invalid_credentials: neplatné přihlašovací údaje
49
+ invalid_oauth2_pkce_verifier: Neplatný OAuth2 PKCE verifier
50
+ invalid_oauth2_state: Neplatný OAuth2 state
51
+ mfa_required: implementujte vícefaktorové ověření
52
+ multistep_callback_required: přidejte callback pro zpracování vícefázového ověření
53
+ oauth2_access_token_required: Volba access_token musí být zadána
54
+ oauth2_not_configured: OAuth2 ověření není nastaveno
55
+ oauth2_revoke_failed: Přístupový token nelze zrušit, HTTP %{status}
56
+ token_request_failed: 'Token nelze vyžádat: %{message}'
57
+ token_revoke_failed: 'Token nelze zrušit: %{message}'
58
+ token_step_failed: 'Krok ověření ''%{action}'' selhal: %{message}'
59
+ unsupported_auth_action: Nepodporovaná ověřovací akce '%{action}'
60
+ errors:
61
+ access_forbidden: Přístup odepřen. Chybné uživatelské jméno nebo heslo? Nejste
62
+ oprávněni?
63
+ action_failed: "%{action} selhala: %{message}"
64
+ fatal_api_error: 'Fatální chyba API: %{error}'
65
+ input_parameters_not_valid: vstupní parametry nejsou platné
66
+ input_parameters_not_valid_for_action: Vstupní parametry pro akci '%{action}'
67
+ nejsou platné
68
+ invalid_input_parameters: neplatné vstupní parametry
69
+ uncancelable_action: 'Stav akce #%{id} nelze zrušit'
70
+ unresolved_arguments: 'Akci ''%{action}'' nelze spustit: chybí argumenty'
71
+ validation:
72
+ cannot_be_null: nesmí být null
73
+ invalid_boolean: neplatná pravdivostní hodnota
74
+ invalid_datetime: není ve formátu ISO 8601
75
+ invalid_float: neplatné desetinné číslo
76
+ invalid_input_layout: neplatná struktura vstupu
77
+ invalid_integer: neplatné celé číslo
78
+ invalid_resource_id: neplatné ID prostředku
79
+ invalid_string: neplatný řetězec
80
+ required_parameter_missing: povinný parametr chybí
81
+ validation_failed: validace selhala
82
+ validation_failed_with_errors: 'validace selhala: %{errors}'
@@ -1,5 +1,5 @@
1
1
  module HaveAPI
2
2
  module GoClient
3
- VERSION = '0.28.4'.freeze
3
+ VERSION = '0.29.0'.freeze
4
4
  end
5
5
  end
@@ -170,6 +170,8 @@ RSpec.describe HaveAPI::GoClient::Generator do
170
170
 
171
171
  import (
172
172
  "math"
173
+ "net/http"
174
+ "strings"
173
175
  "testing"
174
176
  )
175
177
 
@@ -179,6 +181,21 @@ RSpec.describe HaveAPI::GoClient::Generator do
179
181
  return c
180
182
  }
181
183
 
184
+ func TestValidationErrorCompatibility(t *testing.T) {
185
+ verr := NewValidationError()
186
+ verr.Add("field", "broken")
187
+ if verr.Empty() {
188
+ t.Fatalf("expected validation error to be non-empty")
189
+ }
190
+
191
+ legacyLiteral := ValidationError{map[string][]string{
192
+ "field": []string{"broken"},
193
+ }}
194
+ if legacyLiteral.Empty() {
195
+ t.Fatalf("expected legacy literal validation error to be non-empty")
196
+ }
197
+ }
198
+
182
199
  func TestEchoRejectsNaNFloat(t *testing.T) {
183
200
  c := newValidationClient()
184
201
  req := c.Test.Echo.Prepare()
@@ -205,6 +222,52 @@ RSpec.describe HaveAPI::GoClient::Generator do
205
222
  }
206
223
  }
207
224
 
225
+ func TestLanguageHeaderAndLocalizedValidation(t *testing.T) {
226
+ c := newValidationClient()
227
+ if err := c.SetLanguage("cs-CZ"); err != nil {
228
+ t.Fatalf("set language failed: %v", err)
229
+ }
230
+ if err := c.SetLanguageHeader("X-Language"); err != nil {
231
+ t.Fatalf("set language header failed: %v", err)
232
+ }
233
+
234
+ httpReq, err := http.NewRequest("GET", "#{base_url}/v1/test", nil)
235
+ if err != nil {
236
+ t.Fatalf("new request failed: %v", err)
237
+ }
238
+ c.addLanguageHeader(httpReq)
239
+ if got := httpReq.Header.Get("X-Language"); got != "cs-CZ" {
240
+ t.Fatalf("expected language header cs-CZ, got %q", got)
241
+ }
242
+
243
+ req := c.Test.Echo.Prepare()
244
+ in := req.NewInput()
245
+ in.SetI(1)
246
+ in.SetF(math.NaN())
247
+ in.SetB(true)
248
+ in.SetDt("2020-01-01T00:00:00Z")
249
+ in.SetS("x")
250
+ in.SetT("y")
251
+
252
+ _, err = req.Call()
253
+ if err == nil {
254
+ t.Fatalf("expected validation error, got nil")
255
+ }
256
+
257
+ verr, ok := err.(*ValidationError)
258
+ if !ok {
259
+ t.Fatalf("expected ValidationError, got %T: %v", err, err)
260
+ }
261
+
262
+ if got := strings.Join(verr.Errors["f"], " "); !strings.Contains(got, "neplatné desetinné číslo") {
263
+ t.Fatalf("expected Czech float error, got %q", got)
264
+ }
265
+
266
+ if !strings.Contains(err.Error(), "validace selhala") {
267
+ t.Fatalf("expected Czech validation summary, got %q", err.Error())
268
+ }
269
+ }
270
+
208
271
  func TestEchoRejectsInvalidDatetime(t *testing.T) {
209
272
  c := newValidationClient()
210
273
  req := c.Test.Echo.Prepare()
@@ -422,6 +485,12 @@ RSpec.describe HaveAPI::GoClient::Generator do
422
485
  c := New("http://unused.example")
423
486
  c.SetHTTPClient(&http.Client{Transport: transport})
424
487
  c.SetExistingOAuth2Auth(token)
488
+ if err := c.SetLanguage("cs-CZ"); err != nil {
489
+ t.Fatalf("set language failed: %v", err)
490
+ }
491
+ if err := c.SetLanguageHeader("X-Language"); err != nil {
492
+ t.Fatalf("set language header failed: %v", err)
493
+ }
425
494
 
426
495
  if err := c.RevokeAccessToken(); err != nil {
427
496
  t.Fatalf("revoke failed: %v", err)
@@ -443,6 +512,10 @@ RSpec.describe HaveAPI::GoClient::Generator do
443
512
  t.Fatalf("expected OAuth2 header %q, got %q", token, got)
444
513
  }
445
514
 
515
+ if got := transport.req.Header.Get("X-Language"); got != "cs-CZ" {
516
+ t.Fatalf("expected language header cs-CZ, got %q", got)
517
+ }
518
+
446
519
  if got := transport.req.Header.Get("Content-Type"); got != "application/x-www-form-urlencoded" {
447
520
  t.Fatalf("expected form content type, got %q", got)
448
521
  }
@@ -367,7 +367,7 @@ func (inv *<%= action.go_invocation_type %>) IsMetaParameterNil(param string) bo
367
367
  <% end -%>
368
368
 
369
369
  func (inv *<%= action.go_invocation_type %>) validate() error {
370
- verr := NewValidationError()
370
+ verr := NewValidationError(inv.Action.Client)
371
371
  <% if action.has_input? -%>
372
372
  if inv.Input != nil {
373
373
  <% action.input.parameters.each do |p| -%>
@@ -376,18 +376,18 @@ func (inv *<%= action.go_invocation_type %>) validate() error {
376
376
  if !inv.IsParameterNil(<%= go_string_literal(p.go_name) %>) {
377
377
  <% if p.type == 'Float' -%>
378
378
  if !isFiniteFloat64(inv.Input.<%= p.go_name %>) {
379
- verr.Add(<%= go_string_literal(p.name) %>, "not a valid float")
379
+ verr.Add(<%= go_string_literal(p.name) %>, t(inv.Action.Client, "validation.invalid_float", nil))
380
380
  }
381
381
  <% elsif p.type == 'Datetime' -%>
382
382
  normalized, ok := normalizeAndCheckDatetimeString(inv.Input.<%= p.go_name %>)
383
383
  if !ok {
384
- verr.Add(<%= go_string_literal(p.name) %>, "not a valid datetime")
384
+ verr.Add(<%= go_string_literal(p.name) %>, t(inv.Action.Client, "validation.invalid_datetime", nil))
385
385
  } else {
386
386
  inv.Input.<%= p.go_name %> = normalized
387
387
  }
388
388
  <% elsif p.type == 'Resource' -%>
389
389
  if inv.Input.<%= p.go_name %> < 0 {
390
- verr.Add(<%= go_string_literal(p.name) %>, "not a valid resource id")
390
+ verr.Add(<%= go_string_literal(p.name) %>, t(inv.Action.Client, "validation.invalid_resource_id", nil))
391
391
  }
392
392
  <% end -%>
393
393
  }
@@ -404,18 +404,18 @@ func (inv *<%= action.go_invocation_type %>) validate() error {
404
404
  if !inv.IsMetaParameterNil(<%= go_string_literal(p.go_name) %>) {
405
405
  <% if p.type == 'Float' -%>
406
406
  if !isFiniteFloat64(inv.MetaInput.<%= p.go_name %>) {
407
- verr.Add(<%= go_string_literal(p.name) %>, "not a valid float")
407
+ verr.Add(<%= go_string_literal(p.name) %>, t(inv.Action.Client, "validation.invalid_float", nil))
408
408
  }
409
409
  <% elsif p.type == 'Datetime' -%>
410
410
  normalized, ok := normalizeAndCheckDatetimeString(inv.MetaInput.<%= p.go_name %>)
411
411
  if !ok {
412
- verr.Add(<%= go_string_literal(p.name) %>, "not a valid datetime")
412
+ verr.Add(<%= go_string_literal(p.name) %>, t(inv.Action.Client, "validation.invalid_datetime", nil))
413
413
  } else {
414
414
  inv.MetaInput.<%= p.go_name %> = normalized
415
415
  }
416
416
  <% elsif p.type == 'Resource' -%>
417
417
  if inv.MetaInput.<%= p.go_name %> < 0 {
418
- verr.Add(<%= go_string_literal(p.name) %>, "not a valid resource id")
418
+ verr.Add(<%= go_string_literal(p.name) %>, t(inv.Action.Client, "validation.invalid_resource_id", nil))
419
419
  }
420
420
  <% end -%>
421
421
  }
@@ -28,7 +28,7 @@ func (client *Client) SetExistingOAuth2Auth(accessToken string) {
28
28
  func (client *Client) RevokeAccessToken() error {
29
29
  auth, ok := client.Authentication.(*OAuth2Auth)
30
30
  if !ok {
31
- return fmt.Errorf("OAuth2 authentication is not configured")
31
+ return fmt.Errorf("%s", t(client, "authentication.oauth2_not_configured", nil))
32
32
  }
33
33
 
34
34
  form := url.Values{}
@@ -46,6 +46,7 @@ func (client *Client) RevokeAccessToken() error {
46
46
  }
47
47
 
48
48
  req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
49
+ client.addLanguageHeader(req)
49
50
  auth.Authenticate(req)
50
51
 
51
52
  resp, err := client.do(req)
@@ -57,7 +58,9 @@ func (client *Client) RevokeAccessToken() error {
57
58
  defer resp.Body.Close()
58
59
 
59
60
  if resp.StatusCode != 200 {
60
- return fmt.Errorf("Unable to revoke access token, HTTP %v", resp.StatusCode)
61
+ return fmt.Errorf("%s", t(client, "authentication.oauth2_revoke_failed", map[string]string{
62
+ "status": fmt.Sprint(resp.StatusCode),
63
+ }))
61
64
  }
62
65
 
63
66
  client.Authentication = nil
@@ -65,7 +65,9 @@ func (client *Client) SetNewTokenAuth(options *TokenAuthOptions) error {
65
65
  if err != nil {
66
66
  return err
67
67
  } else if !resp.Status {
68
- return fmt.Errorf("Unable to request token: %v", resp.Message)
68
+ return fmt.Errorf("%s", t(client, "authentication.token_request_failed", map[string]string{
69
+ "message": resp.Message,
70
+ }))
69
71
  }
70
72
 
71
73
  if resp.Output.Complete {
@@ -101,11 +103,16 @@ func (auth *TokenAuth) nextAuthenticationStep(options *TokenAuthOptions, action
101
103
  input.SetToken(token)
102
104
 
103
105
  if options.<%= "#{a.go_name}Callback" %> == nil {
104
- return fmt.Errorf(<%= go_string_literal("Implement callback #{a.go_name}Callback") %>)
106
+ return fmt.Errorf("%s", t(auth.Resource.Client, "authentication.callback_required", map[string]string{
107
+ "callback": <%= go_string_literal("#{a.go_name}Callback") %>,
108
+ }))
105
109
  }
106
110
 
107
111
  if err := options.<%= "#{a.go_name}Callback" %>(input); err != nil {
108
- return fmt.Errorf(<%= go_string_literal("#{a.go_name}Callback failed: %v") %>, err)
112
+ return fmt.Errorf("%s", t(auth.Resource.Client, "authentication.callback_failed", map[string]string{
113
+ "callback": <%= go_string_literal("#{a.go_name}Callback") %>,
114
+ "error": err.Error(),
115
+ }))
109
116
  }
110
117
 
111
118
  resp, err := request.Call()
@@ -113,7 +120,10 @@ func (auth *TokenAuth) nextAuthenticationStep(options *TokenAuthOptions, action
113
120
  if err != nil {
114
121
  return err
115
122
  } else if !resp.Status {
116
- return fmt.Errorf("Failed at authentication step '%s': %v", action, resp.Message)
123
+ return fmt.Errorf("%s", t(auth.Resource.Client, "authentication.token_step_failed", map[string]string{
124
+ "action": action,
125
+ "message": resp.Message,
126
+ }))
117
127
  }
118
128
 
119
129
  if resp.Output.Complete {
@@ -130,7 +140,9 @@ func (auth *TokenAuth) nextAuthenticationStep(options *TokenAuthOptions, action
130
140
  }
131
141
  <% end -%>
132
142
 
133
- return fmt.Errorf("Unsupported authentication action '%s'", action)
143
+ return fmt.Errorf("%s", t(auth.Resource.Client, "authentication.unsupported_auth_action", map[string]string{
144
+ "action": action,
145
+ }))
134
146
  }
135
147
 
136
148
  // SetExistingTokenAuth will use a previously acquired token
@@ -156,7 +168,9 @@ func (client *Client) RevokeAuthToken() error {
156
168
  if err != nil {
157
169
  return err
158
170
  } else if !resp.Status {
159
- return fmt.Errorf("Unable to revoke token: %v", resp.Message)
171
+ return fmt.Errorf("%s", t(client, "authentication.token_revoke_failed", map[string]string{
172
+ "message": resp.Message,
173
+ }))
160
174
  }
161
175
 
162
176
  client.Authentication = nil
@@ -13,6 +13,12 @@ type Client struct {
13
13
  // Options for authentication method
14
14
  Authentication Authenticator
15
15
 
16
+ // Language sent to the API server, e.g. "cs" or "cs-CZ".
17
+ Language string
18
+
19
+ // HTTP header used to send Language. Defaults to Accept-Language.
20
+ LanguageHeader string
21
+
16
22
  httpClient *http.Client
17
23
  oauth2TrustedOrigins map[string]struct{}
18
24
 
@@ -0,0 +1,198 @@
1
+ package <%= package %>
2
+
3
+ import (
4
+ "net/http"
5
+ "sort"
6
+ "strconv"
7
+ "strings"
8
+ )
9
+
10
+ const defaultLanguageHeader = "Accept-Language"
11
+
12
+ var clientI18nMessages = map[string]map[string]map[string]string{
13
+ <% i18n_messages.keys.sort.each do |locale| -%>
14
+ <%= go_string_literal(locale) %>: {
15
+ <% i18n_messages[locale].keys.sort.each do |group| -%>
16
+ <%= go_string_literal(group) %>: {
17
+ <% i18n_messages[locale][group].keys.sort.each do |key| -%>
18
+ <%= go_string_literal(key) %>: <%= go_string_literal(i18n_messages[locale][group][key]) %>,
19
+ <% end -%>
20
+ },
21
+ <% end -%>
22
+ },
23
+ <% end -%>
24
+ }
25
+
26
+ // SetLanguage configures the value sent in the language request header.
27
+ func (client *Client) SetLanguage(language string) error {
28
+ if strings.ContainsAny(language, "\x00\r\n") {
29
+ err := NewValidationError(client)
30
+ err.Errors["language"] = []string{
31
+ t(client, "validation.invalid_string", nil),
32
+ }
33
+ return err
34
+ }
35
+
36
+ client.Language = language
37
+ return nil
38
+ }
39
+
40
+ // SetLanguageHeader configures the HTTP header used by SetLanguage.
41
+ func (client *Client) SetLanguageHeader(header string) error {
42
+ if !validHeaderName(header) {
43
+ err := NewValidationError(client)
44
+ err.Errors["language_header"] = []string{
45
+ t(client, "validation.invalid_string", nil),
46
+ }
47
+ return err
48
+ }
49
+
50
+ client.LanguageHeader = header
51
+ return nil
52
+ }
53
+
54
+ func (client *Client) addLanguageHeader(req *http.Request) {
55
+ if client == nil || req == nil || client.Language == "" {
56
+ return
57
+ }
58
+
59
+ req.Header.Set(client.languageHeaderName(), client.Language)
60
+ }
61
+
62
+ func (client *Client) languageHeaderName() string {
63
+ if client == nil || client.LanguageHeader == "" {
64
+ return defaultLanguageHeader
65
+ }
66
+
67
+ return client.LanguageHeader
68
+ }
69
+
70
+ func t(client *Client, key string, values map[string]string) string {
71
+ language := ""
72
+ if client != nil {
73
+ language = client.Language
74
+ }
75
+
76
+ return tLanguage(language, key, values)
77
+ }
78
+
79
+ func tLanguage(language string, key string, values map[string]string) string {
80
+ message := lookupMessage(localeFor(language), key)
81
+ if message == "" {
82
+ message = lookupMessage("en", key)
83
+ }
84
+ if message == "" {
85
+ message = key
86
+ }
87
+
88
+ for name, value := range values {
89
+ message = strings.ReplaceAll(message, "%{"+name+"}", value)
90
+ }
91
+
92
+ return message
93
+ }
94
+
95
+ func lookupMessage(locale string, key string) string {
96
+ parts := strings.Split(strings.TrimPrefix(key, "haveapi_client."), ".")
97
+ if len(parts) != 2 {
98
+ return ""
99
+ }
100
+
101
+ groups, ok := clientI18nMessages[locale]
102
+ if !ok {
103
+ return ""
104
+ }
105
+
106
+ messages, ok := groups[parts[0]]
107
+ if !ok {
108
+ return ""
109
+ }
110
+
111
+ return messages[parts[1]]
112
+ }
113
+
114
+ func localeFor(language string) string {
115
+ if language == "" {
116
+ return "en"
117
+ }
118
+
119
+ for _, tag := range parseAcceptLanguage(language) {
120
+ locale := normalizeLocale(tag)
121
+ if _, ok := clientI18nMessages[locale]; ok {
122
+ return locale
123
+ }
124
+ }
125
+
126
+ return "en"
127
+ }
128
+
129
+ func parseAcceptLanguage(header string) []string {
130
+ type tag struct {
131
+ name string
132
+ q float64
133
+ }
134
+
135
+ tags := []tag{}
136
+ for _, part := range strings.Split(header, ",") {
137
+ tokens := strings.Split(part, ";")
138
+ name := strings.TrimSpace(tokens[0])
139
+ q := 1.0
140
+
141
+ for _, token := range tokens[1:] {
142
+ pair := strings.SplitN(token, "=", 2)
143
+ if len(pair) == 2 && strings.TrimSpace(pair[0]) == "q" {
144
+ if parsed, err := strconv.ParseFloat(strings.TrimSpace(pair[1]), 64); err == nil {
145
+ q = parsed
146
+ }
147
+ }
148
+ }
149
+
150
+ if name != "" && q > 0 {
151
+ tags = append(tags, tag{name: name, q: q})
152
+ }
153
+ }
154
+
155
+ sort.SliceStable(tags, func(i, j int) bool {
156
+ return tags[i].q > tags[j].q
157
+ })
158
+
159
+ ret := make([]string, 0, len(tags))
160
+ for _, tag := range tags {
161
+ ret = append(ret, tag.name)
162
+ }
163
+
164
+ return ret
165
+ }
166
+
167
+ func normalizeLocale(tag string) string {
168
+ normalized := strings.ToLower(strings.TrimSpace(strings.ReplaceAll(tag, "_", "-")))
169
+ if i := strings.Index(normalized, "."); i >= 0 {
170
+ normalized = normalized[:i]
171
+ }
172
+ if i := strings.Index(normalized, "-"); i >= 0 {
173
+ normalized = normalized[:i]
174
+ }
175
+
176
+ return normalized
177
+ }
178
+
179
+ func validHeaderName(header string) bool {
180
+ if header == "" {
181
+ return false
182
+ }
183
+
184
+ for _, r := range header {
185
+ if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
186
+ continue
187
+ }
188
+
189
+ switch r {
190
+ case '!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '^', '_', '`', '|', '~':
191
+ continue
192
+ default:
193
+ return false
194
+ }
195
+ }
196
+
197
+ return true
198
+ }
@@ -182,6 +182,8 @@ func (client *Client) DoQueryStringRequest(path string, queryParams map[string]s
182
182
  return err
183
183
  }
184
184
 
185
+ client.addLanguageHeader(req)
186
+
185
187
  if client.Authentication != nil {
186
188
  client.Authentication.Authenticate(req)
187
189
  }
@@ -232,6 +234,7 @@ func (client *Client) DoBodyRequest(method string, path string, params interface
232
234
  }
233
235
 
234
236
  req.Header.Set("Content-Type", "application/json")
237
+ client.addLanguageHeader(req)
235
238
 
236
239
  if client.Authentication != nil {
237
240
  client.Authentication.Authenticate(req)
@@ -2,10 +2,13 @@ package <%= package %>
2
2
 
3
3
  import (
4
4
  "math"
5
+ "runtime"
5
6
  "sort"
6
7
  "strconv"
7
8
  "strings"
9
+ "sync"
8
10
  "time"
11
+ "unsafe"
9
12
  )
10
13
 
11
14
  type ProgressCallbackReturn int
@@ -28,10 +31,22 @@ type ValidationError struct {
28
31
  Errors map[string][]string
29
32
  }
30
33
 
31
- func NewValidationError() *ValidationError {
32
- return &ValidationError{
34
+ // Keep the exported ValidationError layout compatible with older generated clients.
35
+ var validationErrorLanguages sync.Map
36
+
37
+ func NewValidationError(client ...*Client) *ValidationError {
38
+ ret := &ValidationError{
33
39
  Errors: make(map[string][]string),
34
40
  }
41
+
42
+ if len(client) > 0 && client[0] != nil && client[0].Language != "" {
43
+ validationErrorLanguages.Store(validationErrorKey(ret), client[0].Language)
44
+ runtime.SetFinalizer(ret, func(e *ValidationError) {
45
+ validationErrorLanguages.Delete(validationErrorKey(e))
46
+ })
47
+ }
48
+
49
+ return ret
35
50
  }
36
51
 
37
52
  func (e *ValidationError) Add(param string, msg string) {
@@ -52,7 +67,11 @@ func (e *ValidationError) Empty() bool {
52
67
 
53
68
  func (e *ValidationError) Error() string {
54
69
  if e == nil || len(e.Errors) == 0 {
55
- return "validation failed"
70
+ if e == nil {
71
+ return t(nil, "validation.validation_failed", nil)
72
+ }
73
+
74
+ return tLanguage(e.language(), "validation.validation_failed", nil)
56
75
  }
57
76
 
58
77
  keys := make([]string, 0, len(e.Errors))
@@ -72,7 +91,27 @@ func (e *ValidationError) Error() string {
72
91
  }
73
92
  }
74
93
 
75
- return "validation failed: " + strings.Join(parts, "; ")
94
+ return tLanguage(e.language(), "validation.validation_failed_with_errors", map[string]string{
95
+ "errors": strings.Join(parts, "; "),
96
+ })
97
+ }
98
+
99
+ func (e *ValidationError) language() string {
100
+ if e == nil {
101
+ return ""
102
+ }
103
+
104
+ v, ok := validationErrorLanguages.Load(validationErrorKey(e))
105
+ if !ok {
106
+ return ""
107
+ }
108
+
109
+ language, _ := v.(string)
110
+ return language
111
+ }
112
+
113
+ func validationErrorKey(e *ValidationError) uintptr {
114
+ return uintptr(unsafe.Pointer(e))
76
115
  }
77
116
 
78
117
  func isFiniteFloat64(v float64) bool {
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: haveapi-go-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.28.4
4
+ version: 0.29.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jakub Skokan
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 0.28.4
18
+ version: 0.29.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: 0.28.4
25
+ version: 0.29.0
26
26
  description: Go client generator
27
27
  email:
28
28
  - jakub.skokan@vpsfree.cz
@@ -49,6 +49,7 @@ files:
49
49
  - lib/haveapi/go_client/cli.rb
50
50
  - lib/haveapi/go_client/erb_template.rb
51
51
  - lib/haveapi/go_client/generator.rb
52
+ - lib/haveapi/go_client/i18n_messages.yml
52
53
  - lib/haveapi/go_client/input_output.rb
53
54
  - lib/haveapi/go_client/metadata.rb
54
55
  - lib/haveapi/go_client/parameter.rb
@@ -70,6 +71,7 @@ files:
70
71
  - template/authentication/token.go.erb
71
72
  - template/client.go.erb
72
73
  - template/go.mod.erb
74
+ - template/i18n.go.erb
73
75
  - template/request.go.erb
74
76
  - template/resource.go.erb
75
77
  - template/response.go.erb