lowtype 1.4.0 → 1.5.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: dd21f076ae997541e19b4bd3d405d434a7a3395165afa5084dbe7e2701d591a7
4
- data.tar.gz: ab3cc40ff0baef867751c9a0c5660cb6340951167c677ab7c4412b389e49f394
3
+ metadata.gz: 8fa1f2cac7f1067dd6d9c7c8f9a2c9d206cc22638c0223259a39909b65b8169f
4
+ data.tar.gz: bd3eaa654437ebabeecad44fccd7aa01cff8fb92d489318181c7d0fc0f757ebe
5
5
  SHA512:
6
- metadata.gz: 906c8a132cbddaf7b907b7933f4a8fe9686c47dad2ad5811c49be40637c8ce4cb63efd88bcb602255610addf80b57421fdde95b5919e5708e5931994a3575c8f
7
- data.tar.gz: 2e7f57855d2e290a2bc353470bec98af3bbf24550006de66ae265de3ecd99623a75874e47ce57bf9667ecf8efaad843a4d173c219222c35067c52002e50e5a05
6
+ metadata.gz: ece1f0a96afaebdb246a4448875873fddd1eb8b2785a83f20c41f8960d46f6d37bab589743bec4fc945c237c8fef6cca5ce1a973133e81dcb08fddfdb39273a4
7
+ data.tar.gz: 5e5d946234acf6666fc3671d7027368fce3f0bcfcdf665083d45b15a6ea8bd56111c9c61f5ad3bbfeb09995ebce1563160c0a3c3eebb46d0902638077637e3be
data/lib/cli/cli.rb ADDED
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'trees'
4
+
5
+ require_relative 'rbs/exporter'
6
+ require_relative 'type_stripper'
7
+
8
+ module LowType
9
+ module CLI
10
+ extend Trees
11
+
12
+ line('rbs :src') do |src|
13
+ s { 'Exports files from the source to type signatures in the sig directory' }
14
+ e { 'lowtype rbs app' }
15
+ x { RBS::Exporter.export(path: src) }
16
+
17
+ line('--boot', '-b') do |src, boot|
18
+ s { 'Runs a file before export and could be used to load constants outside your source directory' }
19
+ e { 'lowtype rbs app --boot config/boot.rb' }
20
+ x { RBS::Exporter.export(path: src, boot:) }
21
+ end
22
+ end
23
+
24
+ line('strip!') do
25
+ s { 'Strips files from the current directory of their types' }
26
+ e { 'lowtype strip' }
27
+ x { TypeStripper.export(src: '.', dest: '.') }
28
+ end
29
+
30
+ line('strip :src :dest') do |src, dest|
31
+ s { 'Exports files from the source directory to a destination directory with types stripped' }
32
+ e { 'lowtype strip src/app dest/app' }
33
+ x { TypeStripper.export(src:, dest:) }
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lowkey'
4
+
5
+ require_relative 'method_sig'
6
+
7
+ module LowType
8
+ module CLI
9
+ module RBS
10
+ module ClassSig
11
+ class << self
12
+ def build(class_proxy:, file_proxy:)
13
+ body = method_lines(class_proxy)
14
+ wrap_namespace(namespace: class_proxy.namespace, name: class_proxy.name, body:, file_proxy:).join("\n")
15
+ end
16
+
17
+ private
18
+
19
+ def method_lines(class_proxy)
20
+ instance_lines = class_proxy.instance_methods.each_value.map do |method_proxy|
21
+ MethodSig.build(method_proxy:)
22
+ end
23
+
24
+ class_lines = class_proxy.class_methods.each_value.map do |method_proxy|
25
+ MethodSig.build(method_proxy:, class_method: true)
26
+ end
27
+
28
+ instance_lines + class_lines
29
+ end
30
+
31
+ def wrap_namespace(namespace:, name:, body:, file_proxy:)
32
+ lines = wrap("class #{name}", body)
33
+
34
+ segments(namespace).reverse_each do |segment|
35
+ lines = wrap("#{namespace_type(segment:, file_proxy:)} #{segment}", lines)
36
+ end
37
+
38
+ lines
39
+ end
40
+
41
+ def wrap(header, body_lines)
42
+ [header, *body_lines.map { |line| " #{line}" }, 'end']
43
+ end
44
+
45
+ # Lowkey's "namespace" includes the class's own name.
46
+ def segments(namespace)
47
+ namespace.split('::')[0..-2]
48
+ end
49
+
50
+ def namespace_type(segment:, file_proxy:)
51
+ file_proxy.definitions[segment].is_a?(::Lowkey::ClassProxy) ? 'class' : 'module'
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'lowkey'
5
+ require 'dirload'
6
+
7
+ require_relative 'class_sig'
8
+
9
+ module LowType
10
+ module CLI
11
+ module RBS
12
+ class MissingPathError < StandardError; end
13
+
14
+ # Exports .rbs files for every class in `path` (a file or directory) that includes LowType.
15
+ # Uses dirload() to autoload missing constants from files within the same directory.
16
+ module Exporter
17
+ class << self
18
+ def export(path:, boot: nil)
19
+ dirload(path).loaded_paths.each do |file_path, _adapter|
20
+ next unless file_path.end_with?('.rb') || file_path.end_with?('.rbx')
21
+
22
+ export_file(file_path:)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def export_file(file_path:)
29
+ return unless file_proxy = ::Lowkey[file_path]
30
+
31
+ class_proxies = typed_class_proxies(file_proxy)
32
+ return if class_proxies.empty?
33
+
34
+ content = class_proxies.map do |class_proxy|
35
+ ClassSig.build(class_proxy:, file_proxy:)
36
+ end.join("\n\n")
37
+
38
+ save_file(file_path:, content:)
39
+ end
40
+
41
+ def typed_class_proxies(file_proxy)
42
+ file_proxy.definitions.values.filter do |proxy|
43
+ includes_lowtype = proxy.lines[proxy.start_line..proxy.end_line].find do |line|
44
+ line.include?('include LowType')
45
+ end
46
+
47
+ proxy.is_a?(::Lowkey::ClassProxy) && includes_lowtype
48
+ end
49
+ end
50
+
51
+ def save_file(file_path:, content:)
52
+ sig_path = sig_path(file_path:)
53
+ FileUtils.mkdir_p(File.dirname(sig_path))
54
+ File.write(sig_path, "#{content}\n")
55
+
56
+ puts "Exported #{sig_path}"
57
+ end
58
+
59
+ def sig_path(file_path:)
60
+ relative = file_path.delete_prefix("#{Dir.pwd}/").delete_prefix('lib/')
61
+ File.join('sig', relative.sub(/\.rb\z/, '.rbs'))
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'type_mapper'
4
+
5
+ module LowType
6
+ module CLI
7
+ module RBS
8
+ # Maps a TypeExpression to an RBS type.
9
+ module ExpressionMapper
10
+ class << self
11
+ def match(expression)
12
+ return 'untyped' if expression.nil?
13
+
14
+ types = expression.types.map { |type| TypeMapper.map(type) }.uniq
15
+ types = ['untyped'] if types.empty?
16
+
17
+ union = types.join(' | ')
18
+ nilable?(expression) ? nilable(union:, multiple: types.length > 1) : union
19
+ end
20
+
21
+ private
22
+
23
+ def nilable?(expression)
24
+ expression.default_value.nil?
25
+ end
26
+
27
+ def nilable(union:, multiple:)
28
+ multiple ? "(#{union})?" : "#{union}?"
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'expression_mapper'
4
+
5
+ module LowType
6
+ module CLI
7
+ module RBS
8
+ # Builds an RBS method signature line from a Lowkey::MethodProxy.
9
+ # TODO: Support positional args, keyword args and blocks.
10
+ module MethodSig
11
+ POSITIONAL_TYPES = %i[pos_req pos_opt].freeze
12
+ KEYWORD_TYPES = %i[key_req key_opt].freeze
13
+
14
+ class << self
15
+ def build(method_proxy:, class_method: false)
16
+ scope = class_method ? 'self.' : ''
17
+ "def #{scope}#{method_proxy.name}: (#{params(method_proxy)}) -> #{return_type(method_proxy)}"
18
+ end
19
+
20
+ private
21
+
22
+ def params(method_proxy)
23
+ positional = method_proxy.params.select { |param| POSITIONAL_TYPES.include?(param.type) }
24
+ keyword = method_proxy.params.select { |param| KEYWORD_TYPES.include?(param.type) }
25
+
26
+ (positional.map { |param| positional_fragment(param) } + keyword.map { |param| keyword_fragment(param) }).join(', ')
27
+ end
28
+
29
+ def positional_fragment(param_proxy)
30
+ type = ExpressionMapper.match(param_proxy.expression)
31
+ optional?(param_proxy) ? "?#{type} #{param_proxy.name}" : "#{type} #{param_proxy.name}"
32
+ end
33
+
34
+ def keyword_fragment(param_proxy)
35
+ type = ExpressionMapper.match(param_proxy.expression)
36
+ optional?(param_proxy) ? "?#{param_proxy.name}: #{type}" : "#{param_proxy.name}: #{type}"
37
+ end
38
+
39
+ def optional?(param_proxy)
40
+ return !param_proxy.expression.required? if param_proxy.expression
41
+
42
+ %i[pos_opt key_opt].include?(param_proxy.type)
43
+ end
44
+
45
+ def return_type(method_proxy)
46
+ ExpressionMapper.match(method_proxy.return_proxy&.expression)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../types/complex_types'
4
+
5
+ module LowType
6
+ module CLI
7
+ module RBS
8
+ module TypeMapper
9
+ COMPLEX_TYPE_MAP = {
10
+ Low::Types::Boolean => 'bool',
11
+ Low::Types::HTML => 'String',
12
+ Low::Types::JSON => 'String',
13
+ Low::Types::XML => 'String',
14
+ Low::Types::Status => 'Integer',
15
+ Low::Types::Headers => 'Hash[String, untyped]',
16
+ Low::Types::Tuple => 'Array[untyped]'
17
+ }.freeze
18
+
19
+ class << self
20
+ def map(type)
21
+ return 'nil' if type == NilClass
22
+ return 'bool' if [TrueClass, FalseClass].include?(type)
23
+ return COMPLEX_TYPE_MAP[type] if COMPLEX_TYPE_MAP.key?(type)
24
+
25
+ case type
26
+ when ::Low::TypeExpression then map_expression(type)
27
+ when Array then map_array(type)
28
+ when Hash then map_hash(type)
29
+ when Class then map_class(type)
30
+ else 'untyped'
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ # A nested type expressio like "String | Integer" inside "Array[String | Integer]".
37
+ def map_expression(expression)
38
+ require_relative 'expression_mapper'
39
+ ExpressionMapper.match(expression)
40
+ end
41
+
42
+ # When [T] then every element is type T. When [X, Y, Z] then each element is that type at that position.
43
+ def map_array(type)
44
+ return 'Array[untyped]' if type.empty?
45
+ return "[#{type.map { |subtype| map(subtype) }.join(', ')}]" if type.length > 1
46
+
47
+ "Array[#{map(type.first)}]"
48
+ end
49
+
50
+ def map_hash(type)
51
+ return 'Hash[untyped, untyped]' if type.empty?
52
+
53
+ "Hash[#{map(type.keys.first)}, #{map(type.values.first)}]"
54
+ end
55
+
56
+ def map_class(type)
57
+ return 'untyped' if type.name.nil?
58
+
59
+ "::#{type.name}"
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LowType
4
+ module TypeStripper
5
+ module_function
6
+
7
+ def export(src:, dest:); end
8
+ end
9
+ end
@@ -7,9 +7,11 @@ require_relative '../proxies/param_proxy'
7
7
  require_relative '../queries/type_query'
8
8
 
9
9
  module Low
10
- root_path = File.expand_path(__dir__)
11
- adapter_paths = Dir.chdir(root_path) { Dir.glob('adapters/*') }.map { |path| File.join(root_path, path) }
12
- module_paths = %w[expressions/expressions instance_types redefiner].map { |path| File.join(root_path, "#{path}.rb") }
10
+ lib_path = File.expand_path('..', __dir__)
11
+ adapter_paths = Dir.glob('adapters/*', base: lib_path).map { |path| File.join(lib_path, path) }
12
+ module_paths = %w[expressions/expression_helpers definitions/type_accessors definitions/redefiner].map do |path|
13
+ File.join(lib_path, "#{path}.rb")
14
+ end
13
15
 
14
16
  HIDDEN_PATHS = [File.expand_path(__FILE__), *adapter_paths, *module_paths].freeze
15
17
 
data/lib/version.rb CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Low
4
4
  module Type
5
- VERSION = '1.4.0'
5
+ VERSION = '1.5.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lowtype
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi
8
- bindir: exe
8
+ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: dirload
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: expressions
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -37,6 +51,20 @@ dependencies:
37
51
  - - "~>"
38
52
  - !ruby/object:Gem::Version
39
53
  version: '0.4'
54
+ - !ruby/object:Gem::Dependency
55
+ name: trees
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
40
68
  description: "LowType introduces the concept of \"type expressions\" in method arguments.
41
69
  \nWhen an argument's default value resolves to a type instead of a value then it's
42
70
  treated as a type expression. \nNow you can have types in Ruby in the simplest syntax
@@ -49,6 +77,13 @@ extra_rdoc_files: []
49
77
  files:
50
78
  - lib/adapters/adapter_loader.rb
51
79
  - lib/adapters/sinatra_adapter.rb
80
+ - lib/cli/cli.rb
81
+ - lib/cli/rbs/class_sig.rb
82
+ - lib/cli/rbs/exporter.rb
83
+ - lib/cli/rbs/expression_mapper.rb
84
+ - lib/cli/rbs/method_sig.rb
85
+ - lib/cli/rbs/type_mapper.rb
86
+ - lib/cli/type_stripper.rb
52
87
  - lib/definitions/evaluator.rb
53
88
  - lib/definitions/redefiner.rb
54
89
  - lib/definitions/type_accessors.rb