union-type 0.1.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 +7 -0
- data/README.md +128 -0
- data/lib/union-type-no-ext.rb +2 -0
- data/lib/union-type.rb +3 -0
- data/lib/union_type/core_ext.rb +26 -0
- data/lib/union_type/union_type.rb +84 -0
- data/lib/union_type/version.rb +3 -0
- metadata +88 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: edfcd1ae4c61f2097f644de21b2403419afc6c121d751823de6018a00975b921
|
|
4
|
+
data.tar.gz: a70a742afc27d29e14b62f420f00cee5bd97b8a97a736a577a566846e422af0c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ce9eaa5d32bf49436f52697773c6ab38aadbda78f27749602d8dda4b13ee87e6f3b5a7fa0c232df7b5a9444257b9304297a0d7401d1b09b04baf1f1cac42d597
|
|
7
|
+
data.tar.gz: e3f0954b10a66a9577ae019017c607def1d9af198d2679211c592d0a26416d158510ea5ee7b5539c891dc41c103a9ec62b21611a74165b0afaaf0bb6b0cc35d5
|
data/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# union-type
|
|
2
|
+
|
|
3
|
+
Union types for Ruby. Combine classes into a single type that matches any of them.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
gem "union-type"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Creating a union type
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
require "union-type"
|
|
17
|
+
|
|
18
|
+
StringOrInt = String | Integer # via Class#|
|
|
19
|
+
StringOrInt = UnionType[String, Integer] # bracket syntax
|
|
20
|
+
# => UnionType(Integer | String)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Types are stored in a `SortedSet` (sorted alphabetically by class name) and deduplicated. Subclasses are dropped when a superclass is already present:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
UnionType[Integer, Float, Numeric]
|
|
27
|
+
# => UnionType(Numeric) — Integer and Float are redundant
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Matching values
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
union = String | Integer
|
|
34
|
+
|
|
35
|
+
union === "hello" # => true
|
|
36
|
+
union === 42 # => true
|
|
37
|
+
union === :sym # => false
|
|
38
|
+
|
|
39
|
+
"hello".is_a?(union) # => true
|
|
40
|
+
42.kind_of?(union) # => true
|
|
41
|
+
"hi".instance_of?(union) # => true (exact class match)
|
|
42
|
+
42.instance_of?(union) # => true
|
|
43
|
+
42.instance_of?(Numeric | String) # => false (42 is not exactly Numeric)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### case/when
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
case value
|
|
50
|
+
when String | Integer then "string or int"
|
|
51
|
+
when Float then "float"
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Combining unions
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
# Union (|)
|
|
59
|
+
(String | Integer) | Float
|
|
60
|
+
# => UnionType(Float | Integer | String)
|
|
61
|
+
|
|
62
|
+
[String, Integer, Float].reduce(:|)
|
|
63
|
+
# => UnionType(Float | Integer | String)
|
|
64
|
+
|
|
65
|
+
# Intersection (&) — returns nil when empty
|
|
66
|
+
(String | Integer) & (Integer | Float)
|
|
67
|
+
# => UnionType(Integer)
|
|
68
|
+
|
|
69
|
+
(String | Integer) & Float
|
|
70
|
+
# => nil
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Subtraction and coverage
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
union = String | Integer | Float
|
|
77
|
+
|
|
78
|
+
union.cover?(Integer) # => true (exact member)
|
|
79
|
+
union.cover?(Numeric) # => false (superclass, not covered)
|
|
80
|
+
|
|
81
|
+
numeric = Numeric | String
|
|
82
|
+
numeric.cover?(Integer) # => true (subclass of Numeric)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Enumerable
|
|
86
|
+
|
|
87
|
+
`UnionType` includes `Enumerable`, yielding classes in sorted order:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
union = String | Integer | Float
|
|
91
|
+
|
|
92
|
+
union.to_a # => [Float, Integer, String]
|
|
93
|
+
union.map(&:name) # => ["Float", "Integer", "String"]
|
|
94
|
+
union.include?(String) # => true
|
|
95
|
+
union.count # => 3
|
|
96
|
+
union.min_by(&:name) # => Float
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Opting out of core extensions
|
|
100
|
+
|
|
101
|
+
If you don't want `Class#|`, `Object#is_a?`, `Object#kind_of?`, or `Object#instance_of?` patched, require the no-ext variant instead:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
require "union-type-no-ext"
|
|
105
|
+
|
|
106
|
+
union = UnionType[String, Integer] # construction still works
|
|
107
|
+
union === "hello" # => true
|
|
108
|
+
"hello".is_a?(union) # => false (Object#is_a? not patched)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## API reference
|
|
112
|
+
|
|
113
|
+
| Method | Description |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `UnionType.new(*classes)` | Create a union; raises `ArgumentError` if empty |
|
|
116
|
+
| `UnionType[*classes]` | Alias for `.new` |
|
|
117
|
+
| `String \| Integer` | Create via `Class#\|` (requires core ext) |
|
|
118
|
+
| `union === value` | True if value matches any member |
|
|
119
|
+
| `union.cover?(klass)` | True if klass is a member or subclass of a member |
|
|
120
|
+
| `union \| other` | Union of two unions (or a class) |
|
|
121
|
+
| `union & other` | Intersection; returns `nil` if empty |
|
|
122
|
+
| `union.types` | The underlying `SortedSet` |
|
|
123
|
+
| `value.is_a?(union)` | Patched `is_a?` (requires core ext) |
|
|
124
|
+
| `value.instance_of?(union)` | Exact class match against members (requires core ext) |
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT
|
data/lib/union-type.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class Class
|
|
2
|
+
def |(other)
|
|
3
|
+
case other
|
|
4
|
+
when Class
|
|
5
|
+
UnionType.new(self, other)
|
|
6
|
+
when UnionType
|
|
7
|
+
UnionType.new(self, *other)
|
|
8
|
+
else
|
|
9
|
+
raise TypeError, "expected Class or UnionType, got #{other.class}"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class Object
|
|
15
|
+
alias_method :__is_a__?, :is_a?
|
|
16
|
+
alias_method :__instance_of__?, :instance_of?
|
|
17
|
+
|
|
18
|
+
def is_a?(type)
|
|
19
|
+
type.__is_a__?(UnionType) ? type === self : __is_a__?(type)
|
|
20
|
+
end
|
|
21
|
+
alias kind_of? is_a?
|
|
22
|
+
|
|
23
|
+
def instance_of?(type)
|
|
24
|
+
type.__is_a__?(UnionType) ? type.include?(self.class) : __instance_of__?(type)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
require "sorted_set"
|
|
2
|
+
|
|
3
|
+
class UnionType
|
|
4
|
+
include Enumerable
|
|
5
|
+
|
|
6
|
+
class Entry
|
|
7
|
+
include Comparable
|
|
8
|
+
|
|
9
|
+
attr_reader :klass
|
|
10
|
+
|
|
11
|
+
def initialize(klass) = (@klass = klass)
|
|
12
|
+
def <=>(other) = klass.name.to_s <=> other.klass.name.to_s
|
|
13
|
+
end
|
|
14
|
+
private_constant :Entry
|
|
15
|
+
|
|
16
|
+
def self.[](*classes) = new(*classes)
|
|
17
|
+
|
|
18
|
+
def initialize(*classes)
|
|
19
|
+
raise ArgumentError, "requires at least one class" if classes.empty?
|
|
20
|
+
|
|
21
|
+
minimal = classes.reject { |c| classes.any? { |other| c != other && c < other } }
|
|
22
|
+
@types = SortedSet.new(minimal.map { Entry.new(_1) })
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def each(&block)
|
|
26
|
+
@types.each { block.call(_1.klass) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Supports case/when and the === operator.
|
|
30
|
+
def ===(value)
|
|
31
|
+
any? { _1 === value }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The underlying SortedSet of Entry objects (sorted by class name).
|
|
35
|
+
def types
|
|
36
|
+
@types
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def cover?(klass)
|
|
40
|
+
any? { klass <= _1 }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def |(other)
|
|
44
|
+
case other
|
|
45
|
+
when Class then UnionType.new(*self, other)
|
|
46
|
+
when UnionType then UnionType.new(*self, *other)
|
|
47
|
+
else raise TypeError, "expected Class or UnionType, got #{other.class}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Returns a new UnionType covering only values matched by both unions,
|
|
52
|
+
# or nil when the intersection is empty.
|
|
53
|
+
def &(other)
|
|
54
|
+
other_types = case other
|
|
55
|
+
when UnionType then other.to_a
|
|
56
|
+
when Class then [other]
|
|
57
|
+
else raise TypeError, "expected Class or UnionType, got #{other.class}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
classes = flat_map { |a|
|
|
61
|
+
other_types.filter_map { |b|
|
|
62
|
+
if a == b || a < b then a
|
|
63
|
+
elsif b < a then b
|
|
64
|
+
end
|
|
65
|
+
}
|
|
66
|
+
}.uniq
|
|
67
|
+
|
|
68
|
+
UnionType.new(*classes) unless classes.empty?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def ==(other)
|
|
72
|
+
other.class == UnionType && to_a == other.to_a
|
|
73
|
+
end
|
|
74
|
+
alias eql? ==
|
|
75
|
+
|
|
76
|
+
def hash
|
|
77
|
+
to_a.hash
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def inspect
|
|
81
|
+
"UnionType(#{map(&:name).join(" | ")})"
|
|
82
|
+
end
|
|
83
|
+
alias to_s inspect
|
|
84
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: union-type
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Bogdan Gusiev
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: sorted_set
|
|
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'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: minitest
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '5.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '5.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rake
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '13.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '13.0'
|
|
54
|
+
description: Union types for Ruby
|
|
55
|
+
email:
|
|
56
|
+
- agresso@gmail.com
|
|
57
|
+
executables: []
|
|
58
|
+
extensions: []
|
|
59
|
+
extra_rdoc_files: []
|
|
60
|
+
files:
|
|
61
|
+
- README.md
|
|
62
|
+
- lib/union-type-no-ext.rb
|
|
63
|
+
- lib/union-type.rb
|
|
64
|
+
- lib/union_type/core_ext.rb
|
|
65
|
+
- lib/union_type/union_type.rb
|
|
66
|
+
- lib/union_type/version.rb
|
|
67
|
+
homepage: https://github.com/bogdangusiev/union-type
|
|
68
|
+
licenses:
|
|
69
|
+
- MIT
|
|
70
|
+
metadata: {}
|
|
71
|
+
rdoc_options: []
|
|
72
|
+
require_paths:
|
|
73
|
+
- lib
|
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - ">="
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: '3.1'
|
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: '0'
|
|
84
|
+
requirements: []
|
|
85
|
+
rubygems_version: 4.0.3
|
|
86
|
+
specification_version: 4
|
|
87
|
+
summary: Union types for Ruby
|
|
88
|
+
test_files: []
|