haveapi-go-client 0.26.5 → 0.27.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: 88b50c54ba792fb0420bbd433c4215369b81c41cc125d705a28ea727a01baa15
4
- data.tar.gz: e7de963b436ffbd6b9d8a4f08547c217cdf154e40aa6e0ba422247e77159ebfe
3
+ metadata.gz: bb661e4d9dd5ab5b29d5d3b63af4b37a486848e0623a6e34ac41f53f0a0feebe
4
+ data.tar.gz: 485e7d815e181bf209d541a0eb63c305acd79440cba6144812263b25108369de
5
5
  SHA512:
6
- metadata.gz: 2c7000a73203516df6beb46ee9399d35f3e485a946788b99b50cb4dd28f2d6933109774468f5be86f1d042452978a2b878ab840fc4f7d84bd59be30cfd4a31c0
7
- data.tar.gz: ab94917cb2c4cc52a8d0a7b15d63701e80288fb6884a00d2d931a9bc004148e7615dc75c1388d995ea71f22c46d0ca045d698e64f357fead19a65363b8cc95f2
6
+ metadata.gz: c6ff55c1c5d592a79c5052df6ee2ea9311e0039b7d37068d9048d592ee2fe0b63913c9dca088387abf57a1937b015a75c4bf3c664a20aac9875f62e2d88236d3
7
+ data.tar.gz: 06014c3863378caf51e02c6b6be0d919f740d6659974c185915ec89a8b0a4bd07434e625481599704ead36f3565e718fd0817462d6400462a0dcda01abd1097d
data/Gemfile CHANGED
@@ -7,3 +7,7 @@ group :development do
7
7
  gem 'bundler'
8
8
  gem 'rake'
9
9
  end
10
+
11
+ group :test do
12
+ gem 'rspec'
13
+ end
@@ -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.26.5'
21
+ spec.add_dependency 'haveapi-client', '~> 0.27.0'
22
22
  end
@@ -22,6 +22,14 @@ module HaveAPI::GoClient
22
22
  opts.on('--package PKG', 'Name of the generated Go package') do |v|
23
23
  options[:package] = v
24
24
  end
25
+
26
+ opts.on('--basic-user USER', 'Basic auth username for API description') do |v|
27
+ options[:basic_user] = v
28
+ end
29
+
30
+ opts.on('--basic-password PASSWORD', 'Basic auth password for API description') do |v|
31
+ options[:basic_password] = v
32
+ end
25
33
  end
26
34
 
27
35
  parser.parse!
@@ -27,6 +27,9 @@ module HaveAPI::GoClient
27
27
  @package = opts[:package]
28
28
 
29
29
  conn = HaveAPI::Client::Communicator.new(url)
30
+ if opts[:basic_user] && opts[:basic_password]
31
+ conn.authenticate(:basic, user: opts[:basic_user], password: opts[:basic_password])
32
+ end
30
33
  @api = ApiVersion.new(conn.describe_api(opts[:version]))
31
34
  end
32
35
 
@@ -1,5 +1,5 @@
1
1
  module HaveAPI
2
2
  module GoClient
3
- VERSION = '0.26.5'.freeze
3
+ VERSION = '0.27.0'.freeze
4
4
  end
5
5
  end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe HaveAPI::GoClient::Generator do
6
+ let(:base_url) { TEST_SERVER.base_url }
7
+ let(:root) { File.expand_path('../../../..', __dir__) }
8
+ let(:cwd) { File.join(root, 'clients', 'go') }
9
+
10
+ it 'generates a client that compiles and can call the API' do
11
+ Dir.mktmpdir('haveapi-go-client-') do |dir|
12
+ cmd = [
13
+ 'bundle', 'exec', 'ruby', 'bin/haveapi-go-client',
14
+ base_url, dir,
15
+ '--module', 'example.com/haveapi-test',
16
+ '--package', 'client',
17
+ '--basic-user', 'user',
18
+ '--basic-password', 'pass'
19
+ ]
20
+
21
+ stdout, stderr, status = Open3.capture3(*cmd, chdir: cwd)
22
+ expect(status).to be_success, "generator failed: #{stdout}\n#{stderr}"
23
+
24
+ File.write(File.join(dir, 'client', 'client_integration_test.go'), <<~GO)
25
+ package client
26
+
27
+ import "testing"
28
+
29
+ func TestProjectList(t *testing.T) {
30
+ c := New("#{base_url}")
31
+ c.SetBasicAuthentication("user", "pass")
32
+
33
+ resp, err := c.Project.List.Prepare().Call()
34
+ if err != nil {
35
+ t.Fatalf("list failed: %v", err)
36
+ }
37
+
38
+ if !resp.Status {
39
+ t.Fatalf("request failed: %s", resp.Message)
40
+ }
41
+
42
+ if len(resp.Output) < 2 {
43
+ t.Fatalf("expected at least 2 projects, got %d", len(resp.Output))
44
+ }
45
+ }
46
+ GO
47
+
48
+ File.write(File.join(dir, 'client', 'client_validation_test.go'), <<~GO)
49
+ package client
50
+
51
+ import (
52
+ "math"
53
+ "testing"
54
+ )
55
+
56
+ func newValidationClient() *Client {
57
+ c := New("#{base_url}")
58
+ c.SetBasicAuthentication("user", "pass")
59
+ return c
60
+ }
61
+
62
+ func TestEchoRejectsNaNFloat(t *testing.T) {
63
+ c := newValidationClient()
64
+ req := c.Test.Echo.Prepare()
65
+ in := req.NewInput()
66
+ in.SetI(1)
67
+ in.SetF(math.NaN())
68
+ in.SetB(true)
69
+ in.SetDt("2020-01-01T00:00:00Z")
70
+ in.SetS("x")
71
+ in.SetT("y")
72
+
73
+ _, err := req.Call()
74
+ if err == nil {
75
+ t.Fatalf("expected validation error, got nil")
76
+ }
77
+
78
+ verr, ok := err.(*ValidationError)
79
+ if !ok {
80
+ t.Fatalf("expected ValidationError, got %T: %v", err, err)
81
+ }
82
+
83
+ if verr.Errors["f"] == nil {
84
+ t.Fatalf("expected float error, got %#v", verr.Errors)
85
+ }
86
+ }
87
+
88
+ func TestEchoRejectsInvalidDatetime(t *testing.T) {
89
+ c := newValidationClient()
90
+ req := c.Test.Echo.Prepare()
91
+ in := req.NewInput()
92
+ in.SetI(1)
93
+ in.SetF(1.0)
94
+ in.SetB(true)
95
+ in.SetDt("not-a-date")
96
+ in.SetS("x")
97
+ in.SetT("y")
98
+
99
+ _, err := req.Call()
100
+ if err == nil {
101
+ t.Fatalf("expected validation error, got nil")
102
+ }
103
+
104
+ verr, ok := err.(*ValidationError)
105
+ if !ok {
106
+ t.Fatalf("expected ValidationError, got %T: %v", err, err)
107
+ }
108
+
109
+ if verr.Errors["dt"] == nil {
110
+ t.Fatalf("expected datetime error, got %#v", verr.Errors)
111
+ }
112
+ }
113
+
114
+ func TestEchoResourceRejectsNegativeID(t *testing.T) {
115
+ c := newValidationClient()
116
+ req := c.Test.EchoResource.Prepare()
117
+ in := req.NewInput()
118
+ in.SetProject(-1)
119
+
120
+ _, err := req.Call()
121
+ if err == nil {
122
+ t.Fatalf("expected validation error, got nil")
123
+ }
124
+
125
+ verr, ok := err.(*ValidationError)
126
+ if !ok {
127
+ t.Fatalf("expected ValidationError, got %T: %v", err, err)
128
+ }
129
+
130
+ if verr.Errors["project"] == nil {
131
+ t.Fatalf("expected resource error, got %#v", verr.Errors)
132
+ }
133
+ }
134
+
135
+ func TestEchoAcceptsValidInput(t *testing.T) {
136
+ c := newValidationClient()
137
+ req := c.Test.Echo.Prepare()
138
+ in := req.NewInput()
139
+ in.SetI(123)
140
+ in.SetF(1.5)
141
+ in.SetB(true)
142
+ in.SetDt("2020-01-01")
143
+ in.SetS("hello")
144
+ in.SetT("world")
145
+
146
+ resp, err := req.Call()
147
+ if err != nil {
148
+ t.Fatalf("echo failed: %v", err)
149
+ }
150
+
151
+ if !resp.Status {
152
+ t.Fatalf("request failed: %s", resp.Message)
153
+ }
154
+ }
155
+ GO
156
+
157
+ go_out, go_err, go_status = Open3.capture3('go', 'test', './...', chdir: dir)
158
+ expect(go_status).to be_success, "go test failed: #{go_out}\n#{go_err}"
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'open3'
5
+ require 'tmpdir'
6
+ require 'haveapi/go_client'
7
+
8
+ require_relative 'support/test_server'
9
+
10
+ TEST_SERVER = ClientTestServer.new
11
+
12
+ RSpec.configure do |config|
13
+ config.order = :random
14
+
15
+ config.before(:suite) do
16
+ TEST_SERVER.start
17
+ end
18
+
19
+ config.after(:suite) do
20
+ TEST_SERVER.stop!
21
+ end
22
+
23
+ config.before do
24
+ TEST_SERVER.reset!
25
+ end
26
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'net/http'
5
+ require 'timeout'
6
+ require 'uri'
7
+
8
+ class ClientTestServer
9
+ READY_PREFIX = 'HAVEAPI_TEST_SERVER_READY'
10
+
11
+ attr_reader :base_url
12
+
13
+ def initialize
14
+ @root = File.expand_path('../../../..', __dir__)
15
+ @server_script = File.join(@root, 'servers', 'ruby', 'test_support', 'client_test_server.rb')
16
+ @gemfile = File.join(@root, 'servers', 'ruby', 'Gemfile')
17
+ @cwd = File.join(@root, 'clients', 'go')
18
+ end
19
+
20
+ def start
21
+ return if @wait_thr
22
+
23
+ env = { 'BUNDLE_GEMFILE' => @gemfile }
24
+ cmd = ['bundle', 'exec', 'ruby', @server_script, '--port', '0']
25
+ @stdin, @stdout, @wait_thr = Open3.popen2e(env, *cmd, chdir: @cwd)
26
+
27
+ read_ready!
28
+ wait_for_health!
29
+ end
30
+
31
+ def reset!
32
+ ensure_started!
33
+
34
+ uri = URI.join(@base_url, '/__reset')
35
+ req = Net::HTTP::Post.new(uri)
36
+ req['Content-Type'] = 'application/json'
37
+ res = Net::HTTP.start(uri.host, uri.port) { |http| http.request(req) }
38
+
39
+ return if res.is_a?(Net::HTTPSuccess)
40
+
41
+ raise "reset failed: #{res.code} #{res.body}"
42
+ end
43
+
44
+ def stop!
45
+ return unless @wait_thr
46
+
47
+ Process.kill('TERM', @wait_thr.pid)
48
+ @wait_thr.value
49
+ rescue Errno::ESRCH
50
+ nil
51
+ ensure
52
+ @stdin&.close
53
+ @stdout&.close
54
+ @wait_thr = nil
55
+ end
56
+
57
+ private
58
+
59
+ def ensure_started!
60
+ start unless @wait_thr
61
+ end
62
+
63
+ def read_ready!
64
+ Timeout.timeout(10) do
65
+ while (line = @stdout.gets)
66
+ next unless line.include?(READY_PREFIX)
67
+
68
+ @base_url = line.split.last&.strip
69
+ break
70
+ end
71
+ end
72
+
73
+ raise 'server did not start' unless @base_url
74
+ rescue Timeout::Error
75
+ raise 'server did not start in time'
76
+ end
77
+
78
+ def wait_for_health!
79
+ Timeout.timeout(5) do
80
+ loop do
81
+ begin
82
+ uri = URI.join(@base_url, '/__health')
83
+ res = Net::HTTP.get_response(uri)
84
+ return if res.is_a?(Net::HTTPSuccess)
85
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, IOError
86
+ # retry
87
+ end
88
+
89
+ sleep 0.05
90
+ end
91
+ end
92
+ rescue Timeout::Error
93
+ raise 'server did not become healthy in time'
94
+ end
95
+ end
@@ -365,8 +365,77 @@ func (inv *<%= action.go_invocation_type %>) IsMetaParameterNil(param string) bo
365
365
  }
366
366
  <% end -%>
367
367
 
368
+ func (inv *<%= action.go_invocation_type %>) validate() error {
369
+ verr := NewValidationError()
370
+ <% if action.has_input? -%>
371
+ if inv.Input != nil {
372
+ <% action.input.parameters.each do |p| -%>
373
+ <% if %w(Float Datetime Resource).include?(p.type) -%>
374
+ if inv.IsParameterSelected("<%= p.go_name %>") {
375
+ if !inv.IsParameterNil("<%= p.go_name %>") {
376
+ <% if p.type == 'Float' -%>
377
+ if !isFiniteFloat64(inv.Input.<%= p.go_name %>) {
378
+ verr.Add("<%= p.name %>", "not a valid float")
379
+ }
380
+ <% elsif p.type == 'Datetime' -%>
381
+ normalized, ok := normalizeAndCheckDatetimeString(inv.Input.<%= p.go_name %>)
382
+ if !ok {
383
+ verr.Add("<%= p.name %>", "not a valid datetime")
384
+ } else {
385
+ inv.Input.<%= p.go_name %> = normalized
386
+ }
387
+ <% elsif p.type == 'Resource' -%>
388
+ if inv.Input.<%= p.go_name %> < 0 {
389
+ verr.Add("<%= p.name %>", "not a valid resource id")
390
+ }
391
+ <% end -%>
392
+ }
393
+ }
394
+ <% end -%>
395
+ <% end -%>
396
+ }
397
+ <% end -%>
398
+ <% if action.metadata.has_global_input? -%>
399
+ if inv.MetaInput != nil {
400
+ <% action.metadata.global.input.parameters.each do |p| -%>
401
+ <% if %w(Float Datetime Resource).include?(p.type) -%>
402
+ if inv.IsMetaParameterSelected("<%= p.go_name %>") {
403
+ if !inv.IsMetaParameterNil("<%= p.go_name %>") {
404
+ <% if p.type == 'Float' -%>
405
+ if !isFiniteFloat64(inv.MetaInput.<%= p.go_name %>) {
406
+ verr.Add("<%= p.name %>", "not a valid float")
407
+ }
408
+ <% elsif p.type == 'Datetime' -%>
409
+ normalized, ok := normalizeAndCheckDatetimeString(inv.MetaInput.<%= p.go_name %>)
410
+ if !ok {
411
+ verr.Add("<%= p.name %>", "not a valid datetime")
412
+ } else {
413
+ inv.MetaInput.<%= p.go_name %> = normalized
414
+ }
415
+ <% elsif p.type == 'Resource' -%>
416
+ if inv.MetaInput.<%= p.go_name %> < 0 {
417
+ verr.Add("<%= p.name %>", "not a valid resource id")
418
+ }
419
+ <% end -%>
420
+ }
421
+ }
422
+ <% end -%>
423
+ <% end -%>
424
+ }
425
+ <% end -%>
426
+
427
+ if verr.Empty() {
428
+ return nil
429
+ }
430
+
431
+ return verr
432
+ }
433
+
368
434
  // Call() invokes the action and returns a response from the API server
369
435
  func (inv *<%= action.go_invocation_type %>) Call() (*<%= action.go_response_type %>, error) {
436
+ if err := inv.validate(); err != nil {
437
+ return nil, err
438
+ }
370
439
  <% if action.http_method == 'GET' -%>
371
440
  return inv.callAsQuery()
372
441
  <% else -%>
@@ -1,7 +1,11 @@
1
1
  package <%= package %>
2
2
 
3
3
  import (
4
+ "math"
5
+ "sort"
4
6
  "strconv"
7
+ "strings"
8
+ "time"
5
9
  )
6
10
 
7
11
  type ProgressCallbackReturn int
@@ -20,6 +24,82 @@ type BlockingOperationWatcher interface {
20
24
  WatchOperation(timeout float64, updateIn float64, callback OperationProgressCallback) (*ActionActionStatePollResponse, error)
21
25
  }
22
26
 
27
+ type ValidationError struct {
28
+ Errors map[string][]string
29
+ }
30
+
31
+ func NewValidationError() *ValidationError {
32
+ return &ValidationError{
33
+ Errors: make(map[string][]string),
34
+ }
35
+ }
36
+
37
+ func (e *ValidationError) Add(param string, msg string) {
38
+ if e.Errors == nil {
39
+ e.Errors = make(map[string][]string)
40
+ }
41
+
42
+ e.Errors[param] = append(e.Errors[param], msg)
43
+ }
44
+
45
+ func (e *ValidationError) Empty() bool {
46
+ if e == nil {
47
+ return true
48
+ }
49
+
50
+ return len(e.Errors) == 0
51
+ }
52
+
53
+ func (e *ValidationError) Error() string {
54
+ if e == nil || len(e.Errors) == 0 {
55
+ return "validation failed"
56
+ }
57
+
58
+ keys := make([]string, 0, len(e.Errors))
59
+ for key := range e.Errors {
60
+ keys = append(keys, key)
61
+ }
62
+
63
+ sort.Strings(keys)
64
+
65
+ parts := make([]string, 0, len(keys))
66
+ for _, key := range keys {
67
+ messages := e.Errors[key]
68
+ if len(messages) == 0 {
69
+ parts = append(parts, key)
70
+ } else {
71
+ parts = append(parts, key+": "+strings.Join(messages, ", "))
72
+ }
73
+ }
74
+
75
+ return "validation failed: " + strings.Join(parts, "; ")
76
+ }
77
+
78
+ func isFiniteFloat64(v float64) bool {
79
+ return !math.IsNaN(v) && !math.IsInf(v, 0)
80
+ }
81
+
82
+ func normalizeAndCheckDatetimeString(v string) (string, bool) {
83
+ trimmed := strings.TrimSpace(v)
84
+ if trimmed == "" {
85
+ return trimmed, false
86
+ }
87
+
88
+ if _, err := time.Parse("2006-01-02", trimmed); err == nil {
89
+ return trimmed, true
90
+ }
91
+
92
+ if _, err := time.Parse(time.RFC3339, trimmed); err == nil {
93
+ return trimmed, true
94
+ }
95
+
96
+ if _, err := time.Parse(time.RFC3339Nano, trimmed); err == nil {
97
+ return trimmed, true
98
+ }
99
+
100
+ return trimmed, false
101
+ }
102
+
23
103
  func convertInt64ToString(v int64) string {
24
104
  return strconv.FormatInt(v, 10)
25
105
  }
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.26.5
4
+ version: 0.27.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.26.5
18
+ version: 0.27.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.26.5
25
+ version: 0.27.0
26
26
  description: Go client generator
27
27
  email:
28
28
  - jakub.skokan@vpsfree.cz
@@ -61,6 +61,9 @@ files:
61
61
  - lib/haveapi/go_client/utils.rb
62
62
  - lib/haveapi/go_client/version.rb
63
63
  - shell.nix
64
+ - spec/integration/generator_spec.rb
65
+ - spec/spec_helper.rb
66
+ - spec/support/test_server.rb
64
67
  - template/action.go.erb
65
68
  - template/authentication.go.erb
66
69
  - template/authentication/basic.go.erb