lowtype 1.3.2 → 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: c777de40504eea68870df0ee7936de827b1e5a771aca97e40d1ef2b41d3e38ed
4
- data.tar.gz: cab3b2a605413173922eee3680a18951f19ec28065e0eb320d5d649b99300c34
3
+ metadata.gz: 8fa1f2cac7f1067dd6d9c7c8f9a2c9d206cc22638c0223259a39909b65b8169f
4
+ data.tar.gz: bd3eaa654437ebabeecad44fccd7aa01cff8fb92d489318181c7d0fc0f757ebe
5
5
  SHA512:
6
- metadata.gz: 7c4391bb32203a889a6a88ebdbe2895fd19bf8f195caa2a6d890475a5312fbb4b3e184bd5ea7a6d0031d95246411b8c56cf8323f62774aeda7d78b958f6bd2f0
7
- data.tar.gz: d958d6ccd1f5d6c8c328aef5d7a12b2a46dc75253f82ed0f26d31da29666aa81f6e49e06515be901f6bd1ee5c4781e048131ef5d72505fd753da9135a52dee05
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
@@ -26,25 +26,28 @@ module Low
26
26
  # │ │ │ │ │
27
27
  class Redefiner
28
28
  class << self
29
- # TODO: Pass in "klass" and use it to class_eval/eval methods in the binding of the class that included LowType.
30
- def redefine(method_proxies:, class_proxy:)
31
- if LowType.config.type_checking
32
- typed_methods(method_proxies:, class_proxy:)
33
- else
34
- untyped_methods(method_proxies:, class_proxy:)
29
+ def redefine(method_proxies:, class_proxy:, klass: nil)
30
+ return typed_methods(method_proxies:, class_proxy:) if LowType.config.type_checking
31
+
32
+ case LowType.config.disable_mode
33
+ when :shim
34
+ shimmed_methods(method_proxies:, class_proxy:)
35
+ when :strip
36
+ strip_types(method_proxies:, class_proxy:, klass:)
35
37
  end
36
38
  end
37
39
 
38
- def untyped_args(args:, kwargs:, method_proxy:) # rubocop:disable Metrics/AbcSize
40
+ def untyped_shimmed_args(args:, kwargs:, method_proxy:) # rubocop:disable Metrics/AbcSize
39
41
  method_proxy.params_with_expressions.each do |param_proxy|
40
- value = param_proxy.position ? args[param_proxy.position] : kwargs[param_proxy.name]
42
+ positional = %i[pos_req pos_opt].include?(param_proxy.type)
43
+ value = positional ? args[param_proxy.position] : kwargs[param_proxy.name]
41
44
 
42
45
  next unless value.nil?
43
46
  raise param_proxy.error_type, param_proxy.error_message(value:) if param_proxy.expression.required?
44
47
 
45
48
  value = param_proxy.expression.default_value # Default value can still be `nil`.
46
49
  value = value.value if value.is_a?(ValueExpression)
47
- param_proxy.position ? args[param_proxy.position] = value : kwargs[param_proxy.name] = value
50
+ positional ? args[param_proxy.position] = value : kwargs[param_proxy.name] = value
48
51
  end
49
52
 
50
53
  [args, kwargs]
@@ -82,15 +85,13 @@ module Low
82
85
  end
83
86
  end
84
87
 
85
- def untyped_methods(method_proxies:, class_proxy:)
88
+ def shimmed_methods(method_proxies:, class_proxy:)
86
89
  Module.new do
87
90
  method_proxies.values.filter(&:expressions?).each do |method_proxy|
88
- # You are now in the binding of the includer class.
91
+ # You are now in the binding of both LowType and the includer class.
89
92
  define_method(method_proxy.name) do |*args, **kwargs|
90
- # NOTE: Type checking is currently disabled. See 'config.type_checking'.
91
- method_proxy = Lowkey[class_proxy.file_path][class_proxy.namespace][__method__]
92
-
93
- args, kwargs = Low::Redefiner.untyped_args(args:, kwargs:, method_proxy:)
93
+ # NOTE: Type checking is currently disabled via shim. See 'config.type_checking'.
94
+ args, kwargs = Low::Redefiner.untyped_shimmed_args(args:, kwargs:, method_proxy:)
94
95
  super(*args, **kwargs)
95
96
  end
96
97
 
@@ -98,6 +99,20 @@ module Low
98
99
  end
99
100
  end
100
101
  end
102
+
103
+ def strip_types(method_proxies:, class_proxy:, klass:)
104
+ method_proxies.values.filter(&:expressions?).each do |method_proxy|
105
+ method_proxy.rewrite_signature
106
+
107
+ klass.class_eval(method_proxy.export, method_proxy.file_path, method_proxy.start_line)
108
+
109
+ if class_proxy.private_start_line && method_proxy.start_line > class_proxy.private_start_line
110
+ klass.send(:private, method_proxy.name)
111
+ end
112
+ end
113
+
114
+ nil
115
+ end
101
116
  end
102
117
  end
103
118
  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
 
@@ -18,7 +20,7 @@ module Low
18
20
  attr_reader :types, :default_value
19
21
 
20
22
  # @param type - A literal type or an instance representation of a typed structure.
21
- def initialize(type: nil, default_value: :LOW_TYPE_UNDEFINED)
23
+ def initialize(type: nil, default_value: :LOWTYPE_UNDEFINED)
22
24
  @types = []
23
25
  @types << type unless type.nil?
24
26
  @default_value = default_value
@@ -27,9 +29,10 @@ module Low
27
29
  end
28
30
 
29
31
  def required?
30
- @default_value == :LOW_TYPE_UNDEFINED
32
+ @default_value == :LOWTYPE_UNDEFINED
31
33
  end
32
34
 
35
+ # TODO: Might need to accept error class here so that type accessors can be disabled and supply ArgumentError.
33
36
  def validate!(value:, proxy:) # rubocop:disable Metrics
34
37
  if value.nil?
35
38
  return true if @default_value.nil?
data/lib/lowtype.rb CHANGED
@@ -52,8 +52,11 @@ module LowType
52
52
 
53
53
  Low::Evaluator.evaluate(method_proxies: class_proxy.keyed_methods, class_binding: class_proxy.class_binding)
54
54
 
55
- klass.prepend Low::Redefiner.redefine(method_proxies: class_proxy.instance_methods, class_proxy:)
56
- klass.singleton_class.prepend Low::Redefiner.redefine(method_proxies: class_proxy.class_methods, class_proxy:)
55
+ result = Low::Redefiner.redefine(method_proxies: class_proxy.instance_methods, class_proxy:, klass:)
56
+ klass.prepend result if result.is_a?(Module)
57
+
58
+ singleton_result = Low::Redefiner.redefine(method_proxies: class_proxy.class_methods, class_proxy:, klass: klass.singleton_class)
59
+ klass.singleton_class.prepend singleton_result if singleton_result.is_a?(Module)
57
60
 
58
61
  Low::Adapter::Loader.load(klass:, class_proxy:)
59
62
 
@@ -65,6 +68,7 @@ module LowType
65
68
 
66
69
  Config = Struct.new(
67
70
  :type_checking,
71
+ :disable_mode,
68
72
  :error_mode,
69
73
  :output_mode,
70
74
  :output_size,
@@ -74,7 +78,7 @@ module LowType
74
78
 
75
79
  class << self
76
80
  def config
77
- @config ||= Config.new(true, :error, :type, 100, true, true)
81
+ @config ||= Config.new(true, :shim, :error, :type, 100, true, true)
78
82
  end
79
83
 
80
84
  def configure
@@ -9,10 +9,14 @@ module ::Lowkey
9
9
  class ParamProxy
10
10
  include ::Low::ErrorHandling
11
11
 
12
+ # Error type should be an ArgumentError when type checking disabled via shim methods.
12
13
  def error_type
13
- ::Low::ArgumentTypeError
14
+ return ::Low::ArgumentTypeError if LowType.config.type_checking
15
+
16
+ ArgumentError
14
17
  end
15
18
 
19
+ # When shimmed the error message will be from LowType, when stripped standard Ruby.
16
20
  def error_message(value:)
17
21
  "Invalid argument type '#{output(value:)}' for parameter '#{@name}'. Valid types: '#{@expression.valid_types}'"
18
22
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Low
4
- class ArgumentTypeError < TypeError; end
4
+ class ArgumentTypeError < ArgumentError; end
5
5
  class LocalTypeError < TypeError; end
6
6
  class ReturnTypeError < TypeError; end
7
7
  class AllowedTypeError < TypeError; end
data/lib/version.rb CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Low
4
4
  module Type
5
- VERSION = '1.3.2'
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.3.2
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