kuby-core 0.9.0 → 0.11.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.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/Gemfile +1 -0
  4. data/README.md +4 -2
  5. data/kuby-core.gemspec +2 -1
  6. data/lib/kuby-core.rb +1 -0
  7. data/lib/kuby.rb +14 -0
  8. data/lib/kuby/basic_logger.rb +1 -0
  9. data/lib/kuby/cli_base.rb +53 -4
  10. data/lib/kuby/commands.rb +29 -4
  11. data/lib/kuby/definition.rb +1 -0
  12. data/lib/kuby/dev_setup.rb +1 -0
  13. data/lib/kuby/docker.rb +3 -0
  14. data/lib/kuby/docker/alpine.rb +16 -12
  15. data/lib/kuby/docker/assets_phase.rb +5 -0
  16. data/lib/kuby/docker/bundler_phase.rb +36 -2
  17. data/lib/kuby/docker/cli.rb +47 -2
  18. data/lib/kuby/docker/copy_phase.rb +11 -3
  19. data/lib/kuby/docker/credentials.rb +1 -0
  20. data/lib/kuby/docker/debian.rb +14 -10
  21. data/lib/kuby/docker/dev_spec.rb +82 -18
  22. data/lib/kuby/docker/distro.rb +43 -0
  23. data/lib/kuby/docker/dockerfile.rb +67 -19
  24. data/lib/kuby/docker/errors.rb +9 -0
  25. data/lib/kuby/docker/inline_layer.rb +7 -0
  26. data/lib/kuby/docker/layer.rb +11 -0
  27. data/lib/kuby/docker/layer_stack.rb +43 -6
  28. data/lib/kuby/docker/local_tags.rb +23 -2
  29. data/lib/kuby/docker/metadata.rb +35 -7
  30. data/lib/kuby/docker/package_list.rb +22 -3
  31. data/lib/kuby/docker/package_phase.rb +25 -5
  32. data/lib/kuby/docker/packages.rb +1 -0
  33. data/lib/kuby/docker/packages/managed_package.rb +20 -2
  34. data/lib/kuby/docker/packages/nodejs.rb +8 -0
  35. data/lib/kuby/docker/packages/package.rb +15 -1
  36. data/lib/kuby/docker/packages/simple_managed_package.rb +10 -1
  37. data/lib/kuby/docker/packages/yarn.rb +14 -0
  38. data/lib/kuby/docker/remote_tags.rb +21 -2
  39. data/lib/kuby/docker/setup_phase.rb +35 -4
  40. data/lib/kuby/docker/spec.rb +89 -13
  41. data/lib/kuby/docker/tags.rb +37 -2
  42. data/lib/kuby/docker/timestamp_tag.rb +14 -2
  43. data/lib/kuby/docker/webserver_phase.rb +41 -6
  44. data/lib/kuby/docker/yarn_phase.rb +5 -0
  45. data/lib/kuby/environment.rb +4 -0
  46. data/lib/kuby/kubernetes.rb +1 -0
  47. data/lib/kuby/kubernetes/deploy_task.rb +1 -0
  48. data/lib/kuby/kubernetes/deployer.rb +8 -4
  49. data/lib/kuby/kubernetes/docker_config.rb +1 -0
  50. data/lib/kuby/kubernetes/docker_desktop_provider.rb +1 -0
  51. data/lib/kuby/kubernetes/errors.rb +1 -0
  52. data/lib/kuby/kubernetes/manifest.rb +1 -0
  53. data/lib/kuby/kubernetes/provider.rb +1 -0
  54. data/lib/kuby/kubernetes/registry_secret.rb +1 -0
  55. data/lib/kuby/kubernetes/spec.rb +1 -0
  56. data/lib/kuby/middleware.rb +1 -0
  57. data/lib/kuby/middleware/health_check.rb +1 -0
  58. data/lib/kuby/plugin.rb +1 -0
  59. data/lib/kuby/plugin_registry.rb +1 -0
  60. data/lib/kuby/plugins.rb +1 -0
  61. data/lib/kuby/plugins/nginx_ingress.rb +1 -0
  62. data/lib/kuby/plugins/rails_app.rb +1 -0
  63. data/lib/kuby/plugins/rails_app/asset_copy_task.rb +1 -0
  64. data/lib/kuby/plugins/rails_app/assets.rb +1 -0
  65. data/lib/kuby/plugins/rails_app/database.rb +10 -9
  66. data/lib/kuby/plugins/rails_app/generators/kuby.rb +45 -32
  67. data/lib/kuby/plugins/rails_app/mysql.rb +1 -3
  68. data/lib/kuby/plugins/rails_app/plugin.rb +13 -4
  69. data/lib/kuby/plugins/rails_app/postgres.rb +1 -0
  70. data/lib/kuby/plugins/rails_app/rewrite_db_config.rb +1 -0
  71. data/lib/kuby/plugins/rails_app/sqlite.rb +1 -0
  72. data/lib/kuby/rails_commands.rb +14 -19
  73. data/lib/kuby/railtie.rb +1 -0
  74. data/lib/kuby/tasks.rb +3 -1
  75. data/lib/kuby/trailing_hash.rb +5 -3
  76. data/lib/kuby/version.rb +3 -1
  77. data/spec/docker/metadata_spec.rb +1 -0
  78. data/spec/docker/spec_spec.rb +37 -0
  79. data/spec/docker/timestamp_tag_spec.rb +1 -0
  80. data/spec/dummy/app/channels/application_cable/channel.rb +1 -0
  81. data/spec/dummy/app/channels/application_cable/connection.rb +1 -0
  82. data/spec/dummy/app/controllers/application_controller.rb +1 -0
  83. data/spec/dummy/app/helpers/application_helper.rb +1 -0
  84. data/spec/dummy/app/jobs/application_job.rb +1 -0
  85. data/spec/dummy/app/mailers/application_mailer.rb +1 -0
  86. data/spec/dummy/app/models/application_record.rb +1 -0
  87. data/spec/dummy/config/application.rb +1 -0
  88. data/spec/dummy/config/boot.rb +1 -0
  89. data/spec/dummy/config/environment.rb +1 -0
  90. data/spec/dummy/config/environments/development.rb +1 -0
  91. data/spec/dummy/config/environments/production.rb +1 -0
  92. data/spec/dummy/config/environments/test.rb +1 -0
  93. data/spec/dummy/config/initializers/application_controller_renderer.rb +1 -0
  94. data/spec/dummy/config/initializers/assets.rb +1 -0
  95. data/spec/dummy/config/initializers/backtrace_silencers.rb +1 -0
  96. data/spec/dummy/config/initializers/content_security_policy.rb +1 -0
  97. data/spec/dummy/config/initializers/cookies_serializer.rb +1 -0
  98. data/spec/dummy/config/initializers/filter_parameter_logging.rb +1 -0
  99. data/spec/dummy/config/initializers/inflections.rb +1 -0
  100. data/spec/dummy/config/initializers/mime_types.rb +1 -0
  101. data/spec/dummy/config/initializers/wrap_parameters.rb +1 -0
  102. data/spec/dummy/config/puma.rb +1 -0
  103. data/spec/dummy/config/routes.rb +1 -0
  104. data/spec/dummy/config/spring.rb +1 -0
  105. data/spec/dummy/db/seeds.rb +1 -0
  106. data/spec/dummy/test/application_system_test_case.rb +1 -0
  107. data/spec/dummy/test/channels/application_cable/connection_test.rb +1 -0
  108. data/spec/dummy/test/test_helper.rb +1 -0
  109. data/spec/spec_helper.rb +10 -0
  110. data/spec/support/docker/fake_cli.rb +1 -0
  111. data/spec/support/docker/remote/fake_client.rb +1 -0
  112. data/spec/trailing_hash_spec.rb +2 -0
  113. metadata +20 -4
@@ -1,28 +1,43 @@
1
+ # typed: strict
2
+
1
3
  require 'digest'
2
4
 
3
5
  module Kuby
4
6
  module Docker
5
7
  class Dockerfile
8
+ extend T::Sig
9
+
6
10
  class Command
11
+ extend T::Sig
12
+
13
+ sig { returns(T::Array[T.any(String, Integer)]) }
7
14
  attr_reader :args
8
15
 
9
- def initialize(*args)
16
+ sig { params(args: T::Array[T.any(String, Integer)]).void }
17
+ def initialize(args)
10
18
  @args = args
11
19
  end
12
20
 
21
+ sig { returns(String) }
13
22
  def to_s
14
23
  args.join(' ')
15
24
  end
16
25
  end
17
26
 
18
27
  class From < Command
19
- attr_reader :image_url, :as
28
+ sig { returns(String) }
29
+ attr_reader :image_url
30
+
31
+ sig { returns(T.nilable(String)) }
32
+ attr_reader :as
20
33
 
34
+ sig { params(image_url: String, as: T.nilable(String)).void }
21
35
  def initialize(image_url, as: nil)
22
36
  @image_url = image_url
23
37
  @as = as
24
38
  end
25
39
 
40
+ sig { returns(String) }
26
41
  def to_s
27
42
  str = "FROM #{image_url}"
28
43
  str << " AS #{as}" if as
@@ -31,26 +46,38 @@ module Kuby
31
46
  end
32
47
 
33
48
  class Workdir < Command
49
+ sig { returns(String) }
34
50
  def to_s; "WORKDIR #{super}"; end
35
51
  end
36
52
 
37
53
  class Env < Command
54
+ sig { returns(String) }
38
55
  def to_s; "ENV #{super}"; end
39
56
  end
40
57
 
41
58
  class Run < Command
59
+ sig { returns(String) }
42
60
  def to_s; "RUN #{super}"; end
43
61
  end
44
62
 
45
63
  class Copy < Command
46
- attr_reader :source, :dest, :from
64
+ sig { returns(String) }
65
+ attr_reader :source
47
66
 
67
+ sig { returns(String) }
68
+ attr_reader :dest
69
+
70
+ sig { returns(T.nilable(String)) }
71
+ attr_reader :from
72
+
73
+ sig { params(source: String, dest: String, from: T.nilable(String)).void }
48
74
  def initialize(source, dest, from: nil)
49
75
  @source = source
50
76
  @dest = dest
51
77
  @from = from
52
78
  end
53
79
 
80
+ sig { returns(String) }
54
81
  def to_s
55
82
  cmd = ['COPY']
56
83
  cmd << "--from=#{from}" if from
@@ -60,72 +87,92 @@ module Kuby
60
87
  end
61
88
 
62
89
  class Expose < Command
90
+ sig { returns(String) }
63
91
  def to_s; "EXPOSE #{super}"; end
64
92
  end
65
93
 
66
94
  class Cmd < Command
95
+ sig { returns(String) }
67
96
  def to_s; "CMD #{super}"; end
68
97
  end
69
98
 
70
99
  class Arg < Command
100
+ sig { returns(String) }
71
101
  def to_s; "ARG #{super}"; end
72
102
  end
73
103
 
74
- attr_reader :commands, :cursor
104
+ sig { returns(T::Array[Command]) }
105
+ attr_reader :commands
106
+
107
+ sig { returns(Integer) }
108
+ attr_reader :cursor
75
109
 
110
+ sig { void }
76
111
  def initialize
77
- @commands = []
78
- @cursor = 0
112
+ @commands = T.let([], T::Array[Command])
113
+ @cursor = T.let(0, Integer)
79
114
  end
80
115
 
81
- def from(*args, **kwargs)
82
- add From.new(*args, **kwargs)
116
+ sig { params(image_url: String, as: T.nilable(String)).void }
117
+ def from(image_url, as: nil)
118
+ add From.new(image_url, as: as)
83
119
  end
84
120
 
121
+ sig { params(args: String).void }
85
122
  def workdir(*args)
86
- add Workdir.new(*args)
123
+ add Workdir.new(args)
87
124
  end
88
125
 
126
+ sig { params(args: String).void }
89
127
  def env(*args)
90
- add Env.new(*args)
128
+ add Env.new(args)
91
129
  end
92
130
 
131
+ sig { params(args: String).void }
93
132
  def arg(*args)
94
- add Arg.new(*args)
133
+ add Arg.new(args)
95
134
  end
96
135
 
136
+ sig { params(args: String).void }
97
137
  def run(*args)
98
- add Run.new(*args)
138
+ add Run.new(args)
99
139
  end
100
140
 
101
- def copy(*args, **kwargs)
102
- add Copy.new(*args, **kwargs)
141
+ sig { params(source: String, dest: String, from: T.nilable(String)).void }
142
+ def copy(source, dest, from: nil)
143
+ add Copy.new(source, dest, from: from)
103
144
  end
104
145
 
105
- def expose(*args)
106
- add Expose.new(*args)
146
+ sig { params(port: Integer).void }
147
+ def expose(port)
148
+ add Expose.new([port])
107
149
  end
108
150
 
151
+ sig { params(args: String).void }
109
152
  def cmd(*args)
110
- add Cmd.new(*args)
153
+ add Cmd.new(args)
111
154
  end
112
155
 
156
+ sig { returns(String) }
113
157
  def to_s
114
158
  # ensure trailing newline
115
159
  "#{commands.map(&:to_s).join("\n")}\n"
116
160
  end
117
161
 
162
+ sig { returns(String) }
118
163
  def checksum
119
164
  Digest::SHA256.hexdigest(to_s)
120
165
  end
121
166
 
167
+ sig { returns(T::Array[Integer]) }
122
168
  def exposed_ports
123
169
  commands
124
170
  .select { |c| c.is_a?(Expose) }
125
- .map { |c| c.args.first }
171
+ .map { |c| T.cast(c.args.first, Integer) }
126
172
  end
127
173
 
128
- def insert_at(pos)
174
+ sig { params(pos: Integer, block: T.proc.void).void }
175
+ def insert_at(pos, &block)
129
176
  @cursor = pos
130
177
  yield
131
178
  ensure
@@ -134,6 +181,7 @@ module Kuby
134
181
 
135
182
  private
136
183
 
184
+ sig { params(cmd: Command).void }
137
185
  def add(cmd)
138
186
  commands.insert(cursor, cmd)
139
187
  @cursor += 1
@@ -1,16 +1,25 @@
1
+ # typed: strict
2
+
1
3
  module Kuby
2
4
  module Docker
3
5
  class BuildError < StandardError; end
4
6
  class PushError < StandardError; end
7
+ class PullError < StandardError; end
5
8
  class LoginError < StandardError; end
6
9
 
7
10
  class MissingTagError < StandardError
11
+ extend T::Sig
12
+
13
+ sig { returns(String) }
8
14
  attr_reader :tag
9
15
 
16
+ sig { params(tag: String).void }
10
17
  def initialize(tag)
11
18
  @tag = tag
19
+ @message = T.let(@message, T.nilable(String))
12
20
  end
13
21
 
22
+ sig { returns(String) }
14
23
  def message
15
24
  @message ||= "Could not find tag '#{tag}'."
16
25
  end
@@ -1,12 +1,19 @@
1
+ # typed: strict
2
+
1
3
  module Kuby
2
4
  module Docker
3
5
  class InlineLayer < Layer
6
+ extend T::Sig
7
+
8
+ sig { returns(T.proc.params(df: Dockerfile).void) }
4
9
  attr_reader :block
5
10
 
11
+ sig { params(block: T.proc.params(df: Dockerfile).void).void }
6
12
  def initialize(block)
7
13
  @block = block
8
14
  end
9
15
 
16
+ sig { override.params(dockerfile: Dockerfile).void }
10
17
  def apply_to(dockerfile)
11
18
  block.call(dockerfile)
12
19
  end
@@ -1,12 +1,22 @@
1
+ # typed: strict
2
+
1
3
  module Kuby
2
4
  module Docker
3
5
  class Layer
6
+ extend T::Sig
7
+ extend T::Helpers
8
+
9
+ abstract!
10
+
11
+ sig { returns(Environment) }
4
12
  attr_reader :environment
5
13
 
14
+ sig { params(environment: Environment).void }
6
15
  def initialize(environment)
7
16
  @environment = environment
8
17
  end
9
18
 
19
+ sig { params(dockerfile: Dockerfile).void }
10
20
  def apply_to(dockerfile)
11
21
  raise NotImplementedError,
12
22
  "#{__method__} must be defined in derived classes"
@@ -14,6 +24,7 @@ module Kuby
14
24
 
15
25
  private
16
26
 
27
+ sig { returns(Metadata) }
17
28
  def metadata
18
29
  environment.docker.metadata
19
30
  end
@@ -1,20 +1,46 @@
1
+ # typed: strict
2
+
1
3
  module Kuby
2
4
  module Docker
3
5
  class LayerStack
6
+ extend T::Sig
7
+ extend T::Generic
8
+
9
+ Elem = type_member(fixed: Layer)
10
+
4
11
  include Enumerable
5
12
 
6
- attr_reader :stack, :layers
13
+ sig { returns(T::Array[Symbol]) }
14
+ attr_reader :stack
15
+
16
+ sig { returns(T::Hash[Symbol, Layer]) }
17
+ attr_reader :layers
7
18
 
19
+ sig { void }
8
20
  def initialize
9
- @stack = []
10
- @layers = {}
21
+ @stack = T.let([], T::Array[Symbol])
22
+ @layers = T.let({}, T::Hash[Symbol, Layer])
11
23
  end
12
24
 
13
- def each
14
- return to_enum(__method__) unless block_given?
15
- @stack.each { |name| yield layers[name] }
25
+ sig {
26
+ override.params(
27
+ block: T.nilable(T.proc.params(layer: Layer).void)
28
+ )
29
+ .void
30
+ }
31
+ def each(&block)
32
+ return to_enum(T.must(__method__)) unless block_given?
33
+ @stack.each { |name| yield T.must(layers[name]) }
16
34
  end
17
35
 
36
+ sig {
37
+ params(
38
+ name: Symbol,
39
+ layer: T.nilable(Layer),
40
+ block: T.nilable(T.proc.params(df: Dockerfile).void)
41
+ )
42
+ .void
43
+ }
18
44
  def use(name, layer = nil, &block)
19
45
  stack << name
20
46
 
@@ -27,6 +53,15 @@ module Kuby
27
53
  end
28
54
  end
29
55
 
56
+ sig {
57
+ params(
58
+ name: Symbol,
59
+ layer: T.nilable(T.any(Layer, T::Hash[Symbol, T.untyped])),
60
+ options: T::Hash[Symbol, T.untyped],
61
+ block: T.nilable(T.proc.params(df: Dockerfile).void)
62
+ )
63
+ .void
64
+ }
30
65
  def insert(name, layer = nil, options = {}, &block)
31
66
  # this is truly gross but it's the only way I can think of to be able
32
67
  # to call insert these two ways:
@@ -59,11 +94,13 @@ module Kuby
59
94
  end
60
95
  end
61
96
 
97
+ sig { params(name: Symbol).void }
62
98
  def delete(name)
63
99
  stack.delete(name)
64
100
  layers.delete(name)
65
101
  end
66
102
 
103
+ sig { params(name: Symbol).returns(T::Boolean) }
67
104
  def includes?(name)
68
105
  layers.include?(name)
69
106
  end
@@ -1,18 +1,37 @@
1
+ # typed: strict
2
+
1
3
  module Kuby
2
4
  module Docker
3
5
  class LocalTags
4
- attr_reader :cli, :metadata
6
+ extend T::Sig
7
+
8
+ sig { returns CLI }
9
+ attr_reader :cli
5
10
 
11
+ sig { returns(Metadata) }
12
+ attr_reader :metadata
13
+
14
+ sig {
15
+ params(
16
+ cli: CLI,
17
+ metadata: Metadata
18
+ )
19
+ .void
20
+ }
6
21
  def initialize(cli, metadata)
7
22
  @cli = cli
8
23
  @metadata = metadata
24
+
25
+ @latest_timestamp_tag = T.let(@latest_timestamp_tag, T.nilable(TimestampTag))
9
26
  end
10
27
 
28
+ sig { returns(T::Array[String]) }
11
29
  def tags
12
30
  images = cli.images(metadata.image_url)
13
- images.map { |image| image[:tag] }
31
+ images.map { |image| T.must(image[:tag]) }
14
32
  end
15
33
 
34
+ sig { returns(T::Array[String]) }
16
35
  def latest_tags
17
36
  # find "latest" tag
18
37
  images = cli.images(metadata.image_url)
@@ -30,10 +49,12 @@ module Kuby
30
49
  end
31
50
  end
32
51
 
52
+ sig { returns(T::Array[TimestampTag]) }
33
53
  def timestamp_tags
34
54
  tags.map { |t| TimestampTag.try_parse(t) }.compact
35
55
  end
36
56
 
57
+ sig { returns(T.nilable(TimestampTag)) }
37
58
  def latest_timestamp_tag
38
59
  @latest_timestamp_tag ||= timestamp_tags.sort.last
39
60
  end
@@ -1,51 +1,76 @@
1
+ # typed: strict
2
+
1
3
  require 'uri'
2
4
 
3
5
  module Kuby
4
6
  module Docker
5
7
  class Metadata
8
+ extend T::Sig
9
+
6
10
  DEFAULT_DISTRO = :debian
7
- DEFAULT_REGISTRY_HOST = 'https://www.docker.com'.freeze
8
- DEFAULT_REGISTRY_SCHEME = 'https'
9
- LATEST_TAG = 'latest'
11
+ DEFAULT_REGISTRY_HOST = T.let('https://www.docker.com'.freeze, String)
12
+ DEFAULT_REGISTRY_SCHEME = T.let('https', String)
13
+ LATEST_TAG = T.let('latest'.freeze, String)
14
+
15
+ sig { params(image_url: String).void }
16
+ attr_writer :image_url
10
17
 
11
- attr_accessor :image_url
18
+ sig { returns(Environment) }
12
19
  attr_reader :environment
13
20
 
21
+ sig { params(environment: Environment).void }
14
22
  def initialize(environment)
15
23
  @environment = environment
16
- @tags = []
24
+ @tags = T.let([], T::Array[String])
25
+
26
+ @image_url = T.let(@image_url, T.nilable(String))
27
+ @image_host = T.let(@image_host, T.nilable(String))
28
+ @image_hostname = T.let(@image_hostname, T.nilable(String))
29
+ @image_repo = T.let(@image_repo, T.nilable(String))
30
+ @distro = T.let(@distro, T.nilable(Symbol))
31
+ @full_image_uri = T.let(@full_image_uri, T.nilable(URI::Generic))
32
+ @default_image_url = T.let(@default_image_url, T.nilable(String))
33
+ @default_tags = T.let(@default_tags, T.nilable(T::Array[String]))
17
34
  end
18
35
 
36
+ sig { returns(String) }
19
37
  def image_url
20
38
  @image_url || default_image_url
21
39
  end
22
40
 
41
+ sig { returns(String) }
23
42
  def image_host
24
43
  @image_host ||= "#{full_image_uri.scheme}://#{full_image_uri.host}"
25
44
  end
26
45
 
46
+ sig { returns(String) }
27
47
  def image_hostname
28
- @image_hostname ||= URI(image_host).host
48
+ @image_hostname ||= T.must(URI(image_host).host)
29
49
  end
30
50
 
51
+ sig { returns(String) }
31
52
  def image_repo
32
- @image_repo ||= full_image_uri.path.sub(/\A[\/]+/, '')
53
+ @image_repo ||= T.must(full_image_uri.path).sub(/\A[\/]+/, '')
33
54
  end
34
55
 
56
+ sig { returns(T::Array[String]) }
35
57
  def tags
36
58
  @tags.empty? ? default_tags : @tags
37
59
  end
38
60
 
61
+ sig { returns(Symbol) }
39
62
  def distro
40
63
  @distro || DEFAULT_DISTRO
41
64
  end
42
65
 
66
+ sig { params(distro_name: Symbol).void }
43
67
  def distro=(distro_name)
44
68
  @distro = distro_name
45
69
  end
46
70
 
47
71
  private
48
72
 
73
+ sig { returns(URI::Generic) }
49
74
  def full_image_uri
50
75
  @full_image_uri ||= if image_url.include?('://')
51
76
  URI.parse(image_url)
@@ -56,17 +81,20 @@ module Kuby
56
81
  end
57
82
  end
58
83
 
84
+ sig { returns(String) }
59
85
  def default_image_url
60
86
  # assuming dockerhub by not specifying full url
61
87
  @default_image_url ||= environment.app_name.downcase
62
88
  end
63
89
 
90
+ sig { returns(T::Array[String]) }
64
91
  def default_tags
65
92
  @default_tags ||= [
66
93
  TimestampTag.new(Time.now).to_s, LATEST_TAG
67
94
  ]
68
95
  end
69
96
 
97
+ sig { params(url: String).returns(URI::Generic) }
70
98
  def parse_url(url)
71
99
  uri = URI.parse(url)
72
100
  return uri if uri.scheme