dry-types 0.14.1 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +15 -0
  3. data/.rubocop.yml +43 -0
  4. data/.travis.yml +12 -11
  5. data/CHANGELOG.md +115 -4
  6. data/Gemfile +2 -3
  7. data/benchmarks/hash_schemas.rb +5 -5
  8. data/dry-types.gemspec +1 -1
  9. data/lib/dry/types.rb +67 -45
  10. data/lib/dry/types/any.rb +11 -2
  11. data/lib/dry/types/array.rb +1 -4
  12. data/lib/dry/types/array/member.rb +2 -2
  13. data/lib/dry/types/builder.rb +23 -3
  14. data/lib/dry/types/builder_methods.rb +10 -11
  15. data/lib/dry/types/coercions/params.rb +2 -0
  16. data/lib/dry/types/compat.rb +0 -2
  17. data/lib/dry/types/compiler.rb +25 -33
  18. data/lib/dry/types/constrained.rb +5 -4
  19. data/lib/dry/types/constructor.rb +32 -11
  20. data/lib/dry/types/container.rb +2 -0
  21. data/lib/dry/types/core.rb +22 -12
  22. data/lib/dry/types/default.rb +4 -4
  23. data/lib/dry/types/enum.rb +10 -3
  24. data/lib/dry/types/errors.rb +1 -1
  25. data/lib/dry/types/extensions/maybe.rb +11 -1
  26. data/lib/dry/types/hash.rb +70 -63
  27. data/lib/dry/types/hash/constructor.rb +20 -0
  28. data/lib/dry/types/json.rb +7 -7
  29. data/lib/dry/types/map.rb +6 -1
  30. data/lib/dry/types/module.rb +115 -0
  31. data/lib/dry/types/{definition.rb → nominal.rb} +10 -4
  32. data/lib/dry/types/options.rb +2 -2
  33. data/lib/dry/types/params.rb +11 -11
  34. data/lib/dry/types/printable.rb +12 -0
  35. data/lib/dry/types/printer.rb +309 -0
  36. data/lib/dry/types/result.rb +2 -2
  37. data/lib/dry/types/safe.rb +4 -2
  38. data/lib/dry/types/schema.rb +298 -0
  39. data/lib/dry/types/schema/key.rb +130 -0
  40. data/lib/dry/types/spec/types.rb +12 -4
  41. data/lib/dry/types/sum.rb +14 -15
  42. data/lib/dry/types/version.rb +1 -1
  43. metadata +18 -8
  44. data/lib/dry/types/compat/form_types.rb +0 -27
  45. data/lib/dry/types/compat/int.rb +0 -14
  46. data/lib/dry/types/hash/schema.rb +0 -199
  47. data/lib/dry/types/hash/schema_builder.rb +0 -75
@@ -1,3 +1,5 @@
1
+ require 'dry/container'
2
+
1
3
  module Dry
2
4
  module Types
3
5
  class Container
@@ -2,6 +2,7 @@ require 'dry/types/any'
2
2
 
3
3
  module Dry
4
4
  module Types
5
+ # Primitives with {Kernel} coercion methods
5
6
  COERCIBLE = {
6
7
  string: String,
7
8
  integer: Integer,
@@ -11,6 +12,7 @@ module Dry
11
12
  hash: ::Hash
12
13
  }.freeze
13
14
 
15
+ # Primitives that are non-coercible through {Kernel} methods
14
16
  NON_COERCIBLE = {
15
17
  nil: NilClass,
16
18
  symbol: Symbol,
@@ -23,41 +25,49 @@ module Dry
23
25
  range: Range
24
26
  }.freeze
25
27
 
28
+ # All built-in primitives
26
29
  ALL_PRIMITIVES = COERCIBLE.merge(NON_COERCIBLE).freeze
27
30
 
31
+ # All built-in primitives except {NilClass}
28
32
  NON_NIL = ALL_PRIMITIVES.reject { |name, _| name == :nil }.freeze
29
33
 
30
- # Register built-in types that are non-coercible through kernel methods
34
+ # Register generic types for {ALL_PRIMITIVES}
31
35
  ALL_PRIMITIVES.each do |name, primitive|
32
- register(name.to_s, Definition[primitive].new(primitive))
36
+ type = Nominal[primitive].new(primitive)
37
+ register("nominal.#{name}", type)
33
38
  end
34
39
 
35
- # Register strict built-in types that are non-coercible through kernel methods
40
+ # Register strict types for {ALL_PRIMITIVES}
36
41
  ALL_PRIMITIVES.each do |name, primitive|
37
- register("strict.#{name}", self[name.to_s].constrained(type: primitive))
42
+ type = self["nominal.#{name}"].constrained(type: primitive)
43
+ register(name.to_s, type)
44
+ register("strict.#{name}", type)
38
45
  end
39
46
 
40
- # Register built-in primitive types with kernel coercion methods
47
+ # Register {COERCIBLE} types
41
48
  COERCIBLE.each do |name, primitive|
42
- register("coercible.#{name}", self[name.to_s].constructor(Kernel.method(primitive.name)))
49
+ register("coercible.#{name}", self["nominal.#{name}"].constructor(Kernel.method(primitive.name)))
43
50
  end
44
51
 
45
- # Register non-coercible optional types
52
+ # Register optional strict {NON_NIL} types
46
53
  NON_NIL.each_key do |name|
47
54
  register("optional.strict.#{name}", self["strict.#{name}"].optional)
48
55
  end
49
56
 
50
- # Register coercible optional types
57
+ # Register optional {COERCIBLE} types
51
58
  COERCIBLE.each_key do |name|
52
59
  register("optional.coercible.#{name}", self["coercible.#{name}"].optional)
53
60
  end
54
61
 
55
- # Register :bool since it's common and not a built-in Ruby type :(
56
- register("bool", self["true"] | self["false"])
57
- register("strict.bool", self["strict.true"] | self["strict.false"])
62
+ # Register `:bool` since it's common and not a built-in Ruby type :(
63
+ register("nominal.bool", self["nominal.true"] | self["nominal.false"])
64
+ bool = self["strict.true"] | self["strict.false"]
65
+ register("strict.bool", bool)
66
+ register("bool", bool)
58
67
 
59
68
  register("any", Any)
60
- register("object", self['any'])
69
+ register("nominal.any", Any)
70
+ register("strict.any", Any)
61
71
  end
62
72
  end
63
73
 
@@ -4,12 +4,13 @@ module Dry
4
4
  module Types
5
5
  class Default
6
6
  include Type
7
- include Dry::Equalizer(:type, :options, :value)
8
7
  include Decorator
9
8
  include Builder
9
+ include Printable
10
+ include Dry::Equalizer(:type, :options, :value, inspect: false)
10
11
 
11
12
  class Callable < Default
12
- include Dry::Equalizer(:type, :options)
13
+ include Dry::Equalizer(:type, :options, inspect: false)
13
14
 
14
15
  # Evaluates given callable
15
16
  # @return [Object]
@@ -67,8 +68,7 @@ module Dry
67
68
  if input.equal?(Undefined)
68
69
  evaluate
69
70
  else
70
- output = type[input]
71
- output.nil? ? evaluate : output
71
+ Undefined.default(type[input]) { evaluate }
72
72
  end
73
73
  end
74
74
  alias_method :[], :call
@@ -4,7 +4,7 @@ module Dry
4
4
  module Types
5
5
  class Enum
6
6
  include Type
7
- include Dry::Equalizer(:type, :options, :mapping)
7
+ include Dry::Equalizer(:type, :options, :mapping, inspect: false)
8
8
  include Decorator
9
9
 
10
10
  # @return [Array]
@@ -53,18 +53,25 @@ module Dry
53
53
 
54
54
  # @api public
55
55
  #
56
- # @see Definition#to_ast
56
+ # @see Nominal#to_ast
57
57
  def to_ast(meta: true)
58
58
  [:enum, [type.to_ast(meta: meta),
59
59
  mapping,
60
60
  meta ? self.meta : EMPTY_HASH]]
61
61
  end
62
62
 
63
+ # @return [String]
64
+ # @api public
65
+ def to_s
66
+ PRINTER.(self)
67
+ end
68
+ alias_method :inspect, :to_s
69
+
63
70
  private
64
71
 
65
72
  # Maps a value
66
73
  #
67
- # @params [Object]
74
+ # @param [Object]
68
75
  # @return [Object]
69
76
  # @api private
70
77
  def map_value(input)
@@ -3,7 +3,7 @@ module Dry
3
3
  extend Dry::Core::ClassAttributes
4
4
 
5
5
  # @!attribute [r] namespace
6
- # @return [Container{String => Definition}]
6
+ # @return [Container{String => Nominal}]
7
7
  defines :namespace
8
8
 
9
9
  namespace self
@@ -5,7 +5,7 @@ module Dry
5
5
  module Types
6
6
  class Maybe
7
7
  include Type
8
- include Dry::Equalizer(:type, :options)
8
+ include Dry::Equalizer(:type, :options, inspect: false)
9
9
  include Decorator
10
10
  include Builder
11
11
  include Dry::Monads::Maybe::Mixin
@@ -60,6 +60,16 @@ module Dry
60
60
  end
61
61
  end
62
62
 
63
+ class Printer
64
+ MAPPING[Maybe] = :visit_maybe
65
+
66
+ def visit_maybe(maybe)
67
+ visit(maybe.type) do |type|
68
+ yield "Maybe<#{ type }>"
69
+ end
70
+ end
71
+ end
72
+
63
73
  # Register non-coercible maybe types
64
74
  NON_NIL.each_key do |name|
65
75
  register("maybe.strict.#{name}", self["strict.#{name}"].maybe)
@@ -1,27 +1,26 @@
1
- require 'dry/types/hash/schema_builder'
1
+ require 'dry/types/hash/constructor'
2
2
 
3
3
  module Dry
4
4
  module Types
5
- class Hash < Definition
6
- SCHEMA_BUILDER = SchemaBuilder.new.freeze
7
-
8
- # @param [{Symbol => Definition}] type_map
9
- # @param [Symbol] constructor
10
- # @return [Schema]
11
- def schema(type_map, constructor = nil)
12
- member_types = transform_types(type_map)
13
-
14
- if constructor.nil?
15
- Schema.new(primitive, member_types: member_types, **options, meta: meta)
5
+ class Hash < Nominal
6
+ NOT_REQUIRED = { required: false }.freeze
7
+
8
+ # @overload schmea(type_map, meta = EMPTY_HASH)
9
+ # @param [{Symbol => Dry::Types::Nominal}] type_map
10
+ # @param [Hash] meta
11
+ # @return [Dry::Types::Schema]
12
+ # @overload schema(keys)
13
+ # @param [Array<Dry::Types::Schema::Key>] key List of schema keys
14
+ # @param [Hash] meta
15
+ # @return [Dry::Types::Schema]
16
+ def schema(keys_or_map, meta = EMPTY_HASH)
17
+ if keys_or_map.is_a?(::Array)
18
+ keys = keys_or_map
16
19
  else
17
- SCHEMA_BUILDER.(
18
- primitive,
19
- **options,
20
- member_types: member_types,
21
- meta: meta,
22
- hash_type: constructor
23
- )
20
+ keys = build_keys(keys_or_map)
24
21
  end
22
+
23
+ Schema.new(primitive, keys: keys, **options, meta: self.meta.merge(meta))
25
24
  end
26
25
 
27
26
  # Build a map type
@@ -38,43 +37,16 @@ module Dry
38
37
  )
39
38
  end
40
39
 
41
- # @param [{Symbol => Definition}] type_map
42
- # @return [Schema]
43
- def weak(type_map)
44
- schema(type_map, :weak)
45
- end
46
-
47
- # @param [{Symbol => Definition}] type_map
48
- # @return [Schema]
49
- def permissive(type_map)
50
- schema(type_map, :permissive)
51
- end
52
-
53
- # @param [{Symbol => Definition}] type_map
40
+ # @param [{Symbol => Nominal}] type_map
54
41
  # @return [Schema]
55
- def strict(type_map)
56
- schema(type_map, :strict)
57
- end
58
-
59
- # @param [{Symbol => Definition}] type_map
60
- # @return [Schema]
61
- def strict_with_defaults(type_map)
62
- schema(type_map, :strict_with_defaults)
63
- end
64
-
65
- # @param [{Symbol => Definition}] type_map
66
- # @return [Schema]
67
- def symbolized(type_map)
68
- schema(type_map, :symbolized)
69
- end
70
-
71
- # Build a schema from an AST
72
- # @api private
73
- # @param [{Symbol => Definition}] member_types
74
- # @return [Schema]
75
- def instantiate(member_types)
76
- SCHEMA_BUILDER.instantiate(primitive, **options, member_types: member_types)
42
+ def weak(*)
43
+ raise "Support for old hash schemas was removed, please refer to the CHANGELOG "\
44
+ "on how to proceed with the new API https://github.com/dry-rb/dry-types/blob/master/CHANGELOG.md"
77
45
  end
46
+ alias_method :permissive, :weak
47
+ alias_method :strict, :weak
48
+ alias_method :strict_with_defaults, :weak
49
+ alias_method :symbolized, :weak
78
50
 
79
51
  # Injects a type transformation function for building schemas
80
52
  # @param [#call,nil] proc
@@ -88,22 +60,45 @@ module Dry
88
60
  end
89
61
 
90
62
  handle = Dry::Types::FnContainer.register(fn)
91
- meta(type_transform_fn: handle)
63
+ with(type_transform_fn: handle)
64
+ end
65
+
66
+ # @api private
67
+ def constructor_type
68
+ ::Dry::Types::Hash::Constructor
69
+ end
70
+
71
+ # Whether the type transforms types of schemas created by {Dry::Types::Hash#schema}
72
+ # @return [Boolean]
73
+ # @api public
74
+ def transform_types?
75
+ !options[:type_transform_fn].nil?
76
+ end
77
+
78
+ # @param meta [Boolean] Whether to dump the meta to the AST
79
+ # @return [Array] An AST representation
80
+ def to_ast(meta: true)
81
+ if RUBY_VERSION >= "2.5"
82
+ opts = options.slice(:type_transform_fn)
83
+ else
84
+ opts = options.select { |k, _| k == :type_transform_fn }
85
+ end
86
+
87
+ [:hash, [opts, meta ? self.meta : EMPTY_HASH]]
92
88
  end
93
89
 
94
90
  private
95
91
 
96
92
  # @api private
97
- def transform_types(type_map)
98
- type_fn = meta.fetch(:type_transform_fn, Schema::NO_TRANSFORM)
93
+ def build_keys(type_map)
94
+ type_fn = options.fetch(:type_transform_fn, Schema::NO_TRANSFORM)
99
95
  type_transform = Dry::Types::FnContainer[type_fn]
100
96
 
101
- type_map.each_with_object({}) { |(name, type), result|
102
- result[name] = type_transform.(
103
- resolve_type(type),
104
- name
105
- )
106
- }
97
+ type_map.map do |map_key, type|
98
+ name, options = key_name(map_key)
99
+ key = Schema::Key.new(resolve_type(type), name, options)
100
+ type_transform.(key)
101
+ end
107
102
  end
108
103
 
109
104
  # @api private
@@ -113,6 +108,18 @@ module Dry
113
108
  else type
114
109
  end
115
110
  end
111
+
112
+ # @api private
113
+ def key_name(key)
114
+ if key.to_s.end_with?('?')
115
+ [key.to_s.chop.to_sym, NOT_REQUIRED]
116
+ else
117
+ [key, EMPTY_HASH]
118
+ end
119
+ end
116
120
  end
117
121
  end
118
122
  end
123
+
124
+ require 'dry/types/schema/key'
125
+ require 'dry/types/schema'
@@ -0,0 +1,20 @@
1
+ require 'dry/types/constructor'
2
+
3
+ module Dry
4
+ module Types
5
+ class Hash < Nominal
6
+ class Constructor < ::Dry::Types::Constructor
7
+ # @api private
8
+ def constructor_type
9
+ ::Dry::Types::Hash::Constructor
10
+ end
11
+
12
+ private
13
+
14
+ def composable?(value)
15
+ super && !value.is_a?(Schema::Key)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -3,31 +3,31 @@ require 'dry/types/coercions/json'
3
3
  module Dry
4
4
  module Types
5
5
  register('json.nil') do
6
- self['nil'].constructor(Coercions::JSON.method(:to_nil))
6
+ self['nominal.nil'].constructor(Coercions::JSON.method(:to_nil))
7
7
  end
8
8
 
9
9
  register('json.date') do
10
- self['date'].constructor(Coercions::JSON.method(:to_date))
10
+ self['nominal.date'].constructor(Coercions::JSON.method(:to_date))
11
11
  end
12
12
 
13
13
  register('json.date_time') do
14
- self['date_time'].constructor(Coercions::JSON.method(:to_date_time))
14
+ self['nominal.date_time'].constructor(Coercions::JSON.method(:to_date_time))
15
15
  end
16
16
 
17
17
  register('json.time') do
18
- self['time'].constructor(Coercions::JSON.method(:to_time))
18
+ self['nominal.time'].constructor(Coercions::JSON.method(:to_time))
19
19
  end
20
20
 
21
21
  register('json.decimal') do
22
- self['decimal'].constructor(Coercions::JSON.method(:to_decimal))
22
+ self['nominal.decimal'].constructor(Coercions::JSON.method(:to_decimal))
23
23
  end
24
24
 
25
25
  register('json.array') do
26
- self['array'].safe
26
+ self['nominal.array'].safe
27
27
  end
28
28
 
29
29
  register('json.hash') do
30
- self['hash'].safe
30
+ self['nominal.hash'].safe
31
31
  end
32
32
  end
33
33
  end
data/lib/dry/types/map.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module Dry
2
2
  module Types
3
- class Map < Definition
3
+ class Map < Nominal
4
4
  def initialize(_primitive, key_type: Types['any'], value_type: Types['any'], meta: EMPTY_HASH)
5
5
  super(_primitive, key_type: key_type, value_type: value_type, meta: meta)
6
6
  validate_options!
@@ -53,6 +53,11 @@ module Dry
53
53
  meta ? self.meta : EMPTY_HASH]]
54
54
  end
55
55
 
56
+ # @return [Boolean]
57
+ def constrained?
58
+ value_type.constrained?
59
+ end
60
+
56
61
  private
57
62
 
58
63
  def coerce(input)
@@ -0,0 +1,115 @@
1
+ require 'dry/core/deprecations'
2
+ require 'dry/types/builder_methods'
3
+
4
+ module Dry
5
+ module Types
6
+ # Export types registered in a container as module constants.
7
+ # @example
8
+ # module Types
9
+ # include Dry::Types.module(:strict, :coercible, :nominal, default: :strict)
10
+ # end
11
+ # # Types.constants
12
+ # # => [:Class, :Strict, :Symbol, :Integer, :Float, :String, :Array, :Hash,
13
+ # # :Decimal, :Nil, :True, :False, :Bool, :Date, :Nominal, :DateTime, :Range,
14
+ # # :Coercible, :Time]
15
+ class Module < ::Module
16
+ def initialize(registry, *args)
17
+ @registry = registry
18
+ check_parameters(*args)
19
+ constants = type_constants(*args)
20
+ define_constants(constants)
21
+ extend(BuilderMethods)
22
+
23
+ if constants.key?(:Nominal)
24
+ singleton_class.send(:define_method, :included) do |base|
25
+ super(base)
26
+ base.instance_exec(const_get(:Nominal, false)) do |nominal|
27
+ extend Dry::Core::Deprecations[:'dry-types']
28
+ const_set(:Definition, nominal)
29
+ deprecate_constant(:Definition, message: "Nominal")
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ # @api private
36
+ def type_constants(*namespaces, default: Undefined, **aliases)
37
+ if namespaces.empty? && aliases.empty? && Undefined.equal?(default)
38
+ default_ns = :Strict
39
+ elsif Undefined.equal?(default)
40
+ default_ns = Undefined
41
+ else
42
+ default_ns = Inflector.camelize(default).to_sym
43
+ end
44
+
45
+ tree = registry_tree
46
+
47
+ if namespaces.empty? && aliases.empty?
48
+ modules = tree.select { |_, v| v.is_a?(::Hash) }.map(&:first)
49
+ else
50
+ modules = (namespaces + aliases.keys).map { |n| Inflector.camelize(n).to_sym }
51
+ end
52
+
53
+ tree.each_with_object({}) do |(key, value), constants|
54
+ if modules.include?(key)
55
+ name = aliases.fetch(Inflector.underscore(key).to_sym, key)
56
+ constants[name] = value
57
+ end
58
+
59
+ constants.update(value) if key == default_ns
60
+ end
61
+ end
62
+
63
+ # @api private
64
+ def registry_tree
65
+ @registry_tree ||= @registry.keys.each_with_object({}) { |key, tree|
66
+ type = @registry[key]
67
+ *modules, const_name = key.split('.').map { |part|
68
+ Inflector.camelize(part).to_sym
69
+ }
70
+ next if modules.empty?
71
+
72
+ modules.reduce(tree) { |br, name| br[name] ||= {} }[const_name] = type
73
+ }.freeze
74
+ end
75
+
76
+ private
77
+
78
+ # @api private
79
+ def check_parameters(*namespaces, default: Undefined, **aliases)
80
+ referenced = namespaces.dup
81
+ referenced << default unless false.equal?(default) || Undefined.equal?(default)
82
+ referenced.concat(aliases.keys)
83
+
84
+ known = @registry.keys.map { |k|
85
+ ns, *path = k.split('.')
86
+ ns.to_sym unless path.empty?
87
+ }.compact.uniq
88
+
89
+ (referenced.uniq - known).each do |name|
90
+ raise ArgumentError,
91
+ "#{ name.inspect } is not a known type namespace. "\
92
+ "Supported options are #{ known.map(&:inspect).join(', ') }"
93
+ end
94
+ end
95
+
96
+ # @api private
97
+ def define_constants(constants, mod = self)
98
+ constants.each do |name, value|
99
+ case value
100
+ when ::Hash
101
+ if mod.const_defined?(name, false)
102
+ define_constants(value, mod.const_get(name, false))
103
+ else
104
+ m = ::Module.new
105
+ mod.const_set(name, m)
106
+ define_constants(value, m)
107
+ end
108
+ else
109
+ mod.const_set(name, value)
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end