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 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
@@ -0,0 +1,2 @@
1
+ require_relative "union_type/version"
2
+ require_relative "union_type/union_type"
data/lib/union-type.rb ADDED
@@ -0,0 +1,3 @@
1
+ require_relative "union_type/version"
2
+ require_relative "union_type/union_type"
3
+ require_relative "union_type/core_ext"
@@ -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
@@ -0,0 +1,3 @@
1
+ class UnionType
2
+ VERSION = "0.1.0"
3
+ 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: []