union-type 0.1.0 → 0.1.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.
- checksums.yaml +4 -4
- data/LICENSE +21 -0
- data/lib/union_type/core_ext.rb +30 -0
- data/lib/union_type/union_type.rb +91 -5
- data/lib/union_type/version.rb +3 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: df57236a684d1ebd848590bf182f721c445dc23e3baeaa0b64e94fdccf8eac5b
|
|
4
|
+
data.tar.gz: 98a0c1d10cc84f2288cb9f19c573b1d180fd548d0c7a6fbfda3a317b9db188ad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bcbda94716e0b6556ca8c7933ebfc3e0ed89eb237045485f6d1124e312b3bb889afa2f709f09c43ef028e1ad06378ea92c24642c796326076eb0fc50c42c03bf
|
|
7
|
+
data.tar.gz: cff63507e647111edef948440d1f0a959f37070c6d136e2d0931cfc9ab6b05196b65a306b1fd92c69964bdef758fedfd82f381be821aeec1721872d5e77817ca
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bogdan Gusiev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/lib/union_type/core_ext.rb
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
class Class
|
|
2
|
+
# Returns a {UnionType} combining +self+ and +other+.
|
|
3
|
+
# Enables the +String | Integer+ literal syntax.
|
|
4
|
+
#
|
|
5
|
+
# @param other [Class, UnionType]
|
|
6
|
+
# @return [UnionType]
|
|
7
|
+
# @raise [TypeError] if +other+ is not a {Class} or {UnionType}
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# String | Integer # => UnionType(Integer | String)
|
|
11
|
+
# String | Integer | Float # => UnionType(Float | Integer | String)
|
|
2
12
|
def |(other)
|
|
3
13
|
case other
|
|
4
14
|
when Class
|
|
@@ -15,11 +25,31 @@ class Object
|
|
|
15
25
|
alias_method :__is_a__?, :is_a?
|
|
16
26
|
alias_method :__instance_of__?, :instance_of?
|
|
17
27
|
|
|
28
|
+
# Returns +true+ if this object is an instance of any class in the union.
|
|
29
|
+
# Falls back to the standard +is_a?+ for plain {Class} or {Module} arguments.
|
|
30
|
+
#
|
|
31
|
+
# @param type [Class, Module, UnionType]
|
|
32
|
+
# @return [Boolean]
|
|
33
|
+
#
|
|
34
|
+
# @example
|
|
35
|
+
# "hello".is_a?(String | Integer) # => true
|
|
36
|
+
# 42.is_a?(String | Integer) # => true
|
|
37
|
+
# :sym.is_a?(String | Integer) # => false
|
|
18
38
|
def is_a?(type)
|
|
19
39
|
type.__is_a__?(UnionType) ? type === self : __is_a__?(type)
|
|
20
40
|
end
|
|
21
41
|
alias kind_of? is_a?
|
|
22
42
|
|
|
43
|
+
# Returns +true+ if this object's exact class is one of the union's members.
|
|
44
|
+
# Unlike {#is_a?}, subclass membership does not satisfy this check.
|
|
45
|
+
# Falls back to the standard +instance_of?+ for plain {Class} arguments.
|
|
46
|
+
#
|
|
47
|
+
# @param type [Class, UnionType]
|
|
48
|
+
# @return [Boolean]
|
|
49
|
+
#
|
|
50
|
+
# @example
|
|
51
|
+
# 42.instance_of?(Integer | String) # => true
|
|
52
|
+
# 42.instance_of?(Numeric | String) # => false (42 is not exactly Numeric)
|
|
23
53
|
def instance_of?(type)
|
|
24
54
|
type.__is_a__?(UnionType) ? type.include?(self.class) : __instance_of__?(type)
|
|
25
55
|
end
|
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
require "sorted_set"
|
|
2
2
|
|
|
3
|
+
# A union type that matches values belonging to any of its member classes.
|
|
4
|
+
#
|
|
5
|
+
# Member classes are stored in a {SortedSet} sorted alphabetically by name.
|
|
6
|
+
# Redundant subclasses are dropped automatically: if a superclass is already
|
|
7
|
+
# present, its subclasses add no information and are removed.
|
|
8
|
+
#
|
|
9
|
+
# @example Basic usage
|
|
10
|
+
# union = String | Integer # => UnionType(Integer | String)
|
|
11
|
+
# union === "hello" # => true
|
|
12
|
+
# union === 42 # => true
|
|
13
|
+
# union === :sym # => false
|
|
14
|
+
#
|
|
15
|
+
# @example case/when
|
|
16
|
+
# case value
|
|
17
|
+
# when String | Integer then "string or int"
|
|
18
|
+
# when Float then "float"
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# @example Superclass deduplication
|
|
22
|
+
# UnionType[Integer, Float, Numeric] # => UnionType(Numeric)
|
|
3
23
|
class UnionType
|
|
4
24
|
include Enumerable
|
|
5
25
|
|
|
26
|
+
# @private
|
|
6
27
|
class Entry
|
|
7
28
|
include Comparable
|
|
8
29
|
|
|
@@ -13,33 +34,81 @@ class UnionType
|
|
|
13
34
|
end
|
|
14
35
|
private_constant :Entry
|
|
15
36
|
|
|
37
|
+
# Creates a frozen {UnionType} from the given classes.
|
|
38
|
+
# Equivalent to {.new} but reads more naturally as a literal.
|
|
39
|
+
#
|
|
40
|
+
# @param classes [Array<Class>] one or more classes
|
|
41
|
+
# @return [UnionType]
|
|
42
|
+
#
|
|
43
|
+
# @example
|
|
44
|
+
# UnionType[String, Integer] # => UnionType(Integer | String)
|
|
16
45
|
def self.[](*classes) = new(*classes)
|
|
17
46
|
|
|
47
|
+
# @param classes [Array<Class>] one or more classes to include in the union.
|
|
48
|
+
# Subclasses of other members are dropped automatically.
|
|
49
|
+
# @raise [ArgumentError] if no classes are given
|
|
18
50
|
def initialize(*classes)
|
|
19
51
|
raise ArgumentError, "requires at least one class" if classes.empty?
|
|
20
52
|
|
|
21
53
|
minimal = classes.reject { |c| classes.any? { |other| c != other && c < other } }
|
|
22
|
-
@types = SortedSet.new(minimal.map { Entry.new(_1) })
|
|
54
|
+
@types = SortedSet.new(minimal.map { Entry.new(_1) }).freeze
|
|
55
|
+
freeze
|
|
23
56
|
end
|
|
24
57
|
|
|
58
|
+
# Yields each member class in sorted (alphabetical) order.
|
|
59
|
+
# Required by {Enumerable}; enables {#to_a}, {#map}, {#include?}, etc.
|
|
60
|
+
#
|
|
61
|
+
# @yieldparam klass [Class]
|
|
62
|
+
# @return [self]
|
|
25
63
|
def each(&block)
|
|
26
64
|
@types.each { block.call(_1.klass) }
|
|
27
65
|
end
|
|
28
66
|
|
|
29
|
-
#
|
|
67
|
+
# Returns +true+ if +value+ is an instance of any member class.
|
|
68
|
+
# This makes {UnionType} work in +case+/+when+ expressions.
|
|
69
|
+
#
|
|
70
|
+
# @param value [Object]
|
|
71
|
+
# @return [Boolean]
|
|
72
|
+
#
|
|
73
|
+
# @example
|
|
74
|
+
# (String | Integer) === "hi" # => true
|
|
75
|
+
# (String | Integer) === :sym # => false
|
|
30
76
|
def ===(value)
|
|
31
77
|
any? { _1 === value }
|
|
32
78
|
end
|
|
33
79
|
|
|
34
|
-
# The underlying SortedSet of
|
|
80
|
+
# The underlying {SortedSet} of internal entry objects.
|
|
81
|
+
# Entries are sorted by class name; use {#to_a} to get plain {Class} objects.
|
|
82
|
+
#
|
|
83
|
+
# @return [SortedSet]
|
|
35
84
|
def types
|
|
36
85
|
@types
|
|
37
86
|
end
|
|
38
87
|
|
|
88
|
+
# Returns +true+ if this union covers +klass+ — i.e. +klass+ is a member or
|
|
89
|
+
# a subclass of a member.
|
|
90
|
+
#
|
|
91
|
+
# @param klass [Class]
|
|
92
|
+
# @return [Boolean]
|
|
93
|
+
#
|
|
94
|
+
# @example
|
|
95
|
+
# (Numeric | String).cover?(Integer) # => true (Integer < Numeric)
|
|
96
|
+
# (Numeric | String).cover?(Numeric) # => true (exact member)
|
|
97
|
+
# (Integer | String).cover?(Numeric) # => false (superclass, not covered)
|
|
39
98
|
def cover?(klass)
|
|
40
99
|
any? { klass <= _1 }
|
|
41
100
|
end
|
|
42
101
|
|
|
102
|
+
# Returns a new {UnionType} that is the union of +self+ and +other+.
|
|
103
|
+
# Redundant subclasses are removed as in {.new}.
|
|
104
|
+
#
|
|
105
|
+
# @param other [Class, UnionType]
|
|
106
|
+
# @return [UnionType]
|
|
107
|
+
# @raise [TypeError] if +other+ is not a {Class} or {UnionType}
|
|
108
|
+
#
|
|
109
|
+
# @example
|
|
110
|
+
# (String | Integer) | Float # => UnionType(Float | Integer | String)
|
|
111
|
+
# [String, Integer, Float].reduce(:|) # => UnionType(Float | Integer | String)
|
|
43
112
|
def |(other)
|
|
44
113
|
case other
|
|
45
114
|
when Class then UnionType.new(*self, other)
|
|
@@ -48,8 +117,18 @@ class UnionType
|
|
|
48
117
|
end
|
|
49
118
|
end
|
|
50
119
|
|
|
51
|
-
# Returns a new UnionType
|
|
52
|
-
#
|
|
120
|
+
# Returns a new {UnionType} containing only classes matched by *both* unions,
|
|
121
|
+
# keeping the more specific class when one is a subclass of the other.
|
|
122
|
+
# Returns +nil+ when the intersection is empty.
|
|
123
|
+
#
|
|
124
|
+
# @param other [Class, UnionType]
|
|
125
|
+
# @return [UnionType, nil]
|
|
126
|
+
# @raise [TypeError] if +other+ is not a {Class} or {UnionType}
|
|
127
|
+
#
|
|
128
|
+
# @example
|
|
129
|
+
# (String | Integer) & (Integer | Float) # => UnionType(Integer)
|
|
130
|
+
# (String | Integer) & Numeric # => UnionType(Integer)
|
|
131
|
+
# (String | Integer) & Float # => nil
|
|
53
132
|
def &(other)
|
|
54
133
|
other_types = case other
|
|
55
134
|
when UnionType then other.to_a
|
|
@@ -68,15 +147,22 @@ class UnionType
|
|
|
68
147
|
UnionType.new(*classes) unless classes.empty?
|
|
69
148
|
end
|
|
70
149
|
|
|
150
|
+
# Value equality: two unions are equal when they contain the same classes.
|
|
151
|
+
# Argument order does not matter.
|
|
152
|
+
#
|
|
153
|
+
# @param other [Object]
|
|
154
|
+
# @return [Boolean]
|
|
71
155
|
def ==(other)
|
|
72
156
|
other.class == UnionType && to_a == other.to_a
|
|
73
157
|
end
|
|
74
158
|
alias eql? ==
|
|
75
159
|
|
|
160
|
+
# @return [Integer] hash consistent with {#eql?}, for use as a Hash key
|
|
76
161
|
def hash
|
|
77
162
|
to_a.hash
|
|
78
163
|
end
|
|
79
164
|
|
|
165
|
+
# @return [String] human-readable representation, e.g. +UnionType(Integer | String)+
|
|
80
166
|
def inspect
|
|
81
167
|
"UnionType(#{map(&:name).join(" | ")})"
|
|
82
168
|
end
|
data/lib/union_type/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: union-type
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bogdan Gusiev
|
|
@@ -58,13 +58,14 @@ executables: []
|
|
|
58
58
|
extensions: []
|
|
59
59
|
extra_rdoc_files: []
|
|
60
60
|
files:
|
|
61
|
+
- LICENSE
|
|
61
62
|
- README.md
|
|
62
63
|
- lib/union-type-no-ext.rb
|
|
63
64
|
- lib/union-type.rb
|
|
64
65
|
- lib/union_type/core_ext.rb
|
|
65
66
|
- lib/union_type/union_type.rb
|
|
66
67
|
- lib/union_type/version.rb
|
|
67
|
-
homepage: https://github.com/
|
|
68
|
+
homepage: https://github.com/bogdan/ruby-union-type
|
|
68
69
|
licenses:
|
|
69
70
|
- MIT
|
|
70
71
|
metadata: {}
|