literal 0.2.0 → 1.0.0.rc1
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/README.md +1 -156
- data/lib/literal/data_structure.rb +24 -15
- data/lib/literal/enum.rb +20 -15
- data/lib/literal/errors/type_error.rb +76 -3
- data/lib/literal/properties/data_schema.rb +3 -4
- data/lib/literal/properties/schema.rb +57 -7
- data/lib/literal/properties.rb +18 -4
- data/lib/literal/property.rb +47 -20
- data/lib/literal/rails/enum_serializer.rb +20 -0
- data/lib/literal/rails/enum_type.rb +41 -0
- data/lib/literal/rails/patches/active_record.rb +31 -0
- data/lib/literal/rails.rb +9 -0
- data/lib/literal/railtie.rb +13 -0
- data/lib/literal/types/array_type.rb +13 -1
- data/lib/literal/types/boolean_type.rb +0 -6
- data/lib/literal/types/constraint_type.rb +32 -3
- data/lib/literal/types/enumerable_type.rb +1 -1
- data/lib/literal/types/float_type.rb +1 -1
- data/lib/literal/types/hash_type.rb +7 -1
- data/lib/literal/types/integer_type.rb +1 -1
- data/lib/literal/types/json_data_type.rb +3 -1
- data/lib/literal/types/map_type.rb +15 -1
- data/lib/literal/types/set_type.rb +7 -1
- data/lib/literal/types/string_type.rb +1 -1
- data/lib/literal/types/symbol_type.rb +1 -1
- data/lib/literal/types/tuple_type.rb +11 -1
- data/lib/literal/types/union_type.rb +8 -2
- data/lib/literal/types.rb +202 -32
- data/lib/literal/version.rb +1 -1
- data/lib/literal.rb +8 -3
- metadata +11 -7
- data/lib/literal.test.rb +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 75c7cc8853143bd8f4ac74b7fc4a5b5d2812bd39c0b4f696a87c2ffc0fb31046
|
4
|
+
data.tar.gz: 7781d6362e9ae8f75d0156809f95b0c1f8a37df855356173c9b6f09428419bc0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6acc2552c11ff3c5f12877b5597f9a6a138bf4901f5fce3801004e286b522c6263fa2454621ac4f5511f553af073d5113d96cd1af1045bc9e44c014c15ab7420
|
7
|
+
data.tar.gz: 6f9b205b3343426385185ebd0ea0e04164f6a06f9d42d6cb34c542c8d12a98ab327e274b336fb13d389e7380f5bd79831eb2b508f2d019cd6143658db40f3195
|
data/README.md
CHANGED
@@ -1,158 +1,3 @@
|
|
1
1
|
# A Literal Ruby Gem [WIP]
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
Literal uses Ruby-native types. Any method that responds to `===(value)` is considered a type. Note, this is how Ruby’s case statements and pattern matching work. It’s also how `Array#any?` and `Array#all?` work. Essentially all Ruby objects are types and just work the way you’d expect them to. A few examples:
|
6
|
-
|
7
|
-
- On a `Range`, `===(value)` checks if the value is within the range.
|
8
|
-
- On a `Regexp`, `===(value)` checks if the value matches the pattern.
|
9
|
-
- On a `Class`, `===(value)` checks if the value is an instance of the class.
|
10
|
-
- On a `Proc`, `===(value)` calls the proc with the value.
|
11
|
-
- On a `String`, `===(value)` checks if the value is equal to the string.
|
12
|
-
- On the class `String`, `===(value)` checks if the value is a string.
|
13
|
-
|
14
|
-
Literal extends this idea with the concept of generics or _parameterised_ types. A generic is a method that returns an object that respond to `===(value)`.
|
15
|
-
|
16
|
-
If we want to check that a given value is an `Array`, we could do this:
|
17
|
-
|
18
|
-
```ruby
|
19
|
-
Array === [1, 2, 3]
|
20
|
-
```
|
21
|
-
|
22
|
-
But what if we want to check that it’s an Array of Integers? Literal provides a library of special types that can be composed for this purpose. In this case, we can use the type `_Array(Integer)`.
|
23
|
-
|
24
|
-
```ruby
|
25
|
-
_Array(Integer) === [1, 2, 3]
|
26
|
-
```
|
27
|
-
|
28
|
-
These special types are defined on the `Literal::Types` module. To access them in a class, you can `extend` this module. To access them on an instance, you can `include` the module. If you want to use them globally, you can `extend` the module at the root.
|
29
|
-
|
30
|
-
```ruby
|
31
|
-
extend Literal::Types
|
32
|
-
```
|
33
|
-
|
34
|
-
This is recommended for applications, but not for libraries, as we don’t want to pollute the global namespace from library code.
|
35
|
-
|
36
|
-
`Literal::Properties`, `Literal::Object`, `Literal::Struct` and `Literal::Data` already extend `Literal::Types`, so you don’t need to extend `Literal::Types` yourself if you’re only using literal types for literal properties.
|
37
|
-
|
38
|
-
## Properties
|
39
|
-
|
40
|
-
`Literal::Properties` is a mixin that allows you to define the structure of an object. Properties are defined using the `prop` method.
|
41
|
-
|
42
|
-
The first argument is the name of the property as a `Symbol`. The second argument is the _type_ of the property. Remember, the type can be any object that responds to `===(value)`.
|
43
|
-
|
44
|
-
The third argument is optional. You can set this to `:*`, `:**`, `:&`, or `:positional` to change the kind of initializer parameter.
|
45
|
-
|
46
|
-
```ruby
|
47
|
-
class Person
|
48
|
-
extend Literal::Properties
|
49
|
-
|
50
|
-
prop :name, String
|
51
|
-
prop :age, Integer
|
52
|
-
end
|
53
|
-
```
|
54
|
-
|
55
|
-
You can also use keyword arguments to define _readers_ and _writers_. These can be set to `false`, `:public`, `:protected`, or `:private` and default to `false`.
|
56
|
-
|
57
|
-
```ruby
|
58
|
-
class Person
|
59
|
-
extend Literal::Properties
|
60
|
-
|
61
|
-
prop :name, String, reader: :public
|
62
|
-
prop :age, Integer, writer: :protected
|
63
|
-
end
|
64
|
-
```
|
65
|
-
|
66
|
-
Properties are required by deafult. To make them optional, set the type to a that responds to `===(nil)` with `true`. `Literal::Types` provides a special types for this purpose. Let’s make the age optional by setting its type to a `_Nilable(Integer)`:
|
67
|
-
|
68
|
-
```ruby
|
69
|
-
class Person
|
70
|
-
extend Literal::Properties
|
71
|
-
|
72
|
-
prop :name, String
|
73
|
-
prop :age, _Nilable(Integer)
|
74
|
-
end
|
75
|
-
```
|
76
|
-
|
77
|
-
Alternatively, you can give the property a default value. This default value must match the type of the property.
|
78
|
-
|
79
|
-
```ruby
|
80
|
-
class Person
|
81
|
-
extend Literal::Properties
|
82
|
-
|
83
|
-
prop :name, String, default: "John Doe"
|
84
|
-
prop :age, _Nilable(Integer)
|
85
|
-
end
|
86
|
-
```
|
87
|
-
|
88
|
-
Note, the above example will fail unless you have frozen string literals enabled. (Which, honestly, you should.) Default values must be frozen. If you can’t use a frozen value, you can pass a proc instead.
|
89
|
-
|
90
|
-
```ruby
|
91
|
-
class Person
|
92
|
-
extend Literal::Properties
|
93
|
-
|
94
|
-
prop :name, String, default: -> { "John Doe" }
|
95
|
-
prop :age, _Nilable(Integer)
|
96
|
-
end
|
97
|
-
```
|
98
|
-
|
99
|
-
The proc will be called to generate the default value.
|
100
|
-
|
101
|
-
You can also pass a block to the `prop` method. This block will be called with the value of the property when it’s set, which is useful for coercion.
|
102
|
-
|
103
|
-
```ruby
|
104
|
-
class Person
|
105
|
-
extend Literal::Properties
|
106
|
-
|
107
|
-
prop :name, String
|
108
|
-
prop :age, Integer do |value|
|
109
|
-
value.to_i
|
110
|
-
end
|
111
|
-
end
|
112
|
-
```
|
113
|
-
|
114
|
-
Coercion takes place prior to type-checking, so you can safely coerce a value to a different type in the block.
|
115
|
-
|
116
|
-
You can use properties that conflict with ruby keywords. Literal will handle everything for you automatically.
|
117
|
-
|
118
|
-
```ruby
|
119
|
-
class Person
|
120
|
-
extend Literal::Properties
|
121
|
-
|
122
|
-
prop :class, String, :positional
|
123
|
-
prop :end, Integer
|
124
|
-
end
|
125
|
-
```
|
126
|
-
|
127
|
-
If you’d prefer to subclass than extend a module, you can use the `Literal::Object` class instead. `Literal::Object` literally extends `Literal::Properties`.
|
128
|
-
|
129
|
-
```ruby
|
130
|
-
class Person < Literal::Object
|
131
|
-
prop :name, String
|
132
|
-
prop :age, Integer
|
133
|
-
end
|
134
|
-
```
|
135
|
-
|
136
|
-
## Structs
|
137
|
-
|
138
|
-
`Literal::Struct` is like `Literal::Object`, but it also provides a few extras.
|
139
|
-
|
140
|
-
Structs implement `==` so you can compare one struct to another. They also implement `hash`. Structs also have public _readers_ and _writers_ by default.
|
141
|
-
|
142
|
-
```ruby
|
143
|
-
class Person < Literal::Struct
|
144
|
-
prop :name, String
|
145
|
-
prop :age, Integer
|
146
|
-
end
|
147
|
-
```
|
148
|
-
|
149
|
-
## Data
|
150
|
-
|
151
|
-
`Literal::Data` is like `Literal::Struct`, but you can’t define _writers_. Additionally, objects are _frozen_ after initialization. Additionally any non-frozen properties are duplicated and frozen.
|
152
|
-
|
153
|
-
```ruby
|
154
|
-
class Person < Literal::Data
|
155
|
-
prop :name, String
|
156
|
-
prop :age, Integer
|
157
|
-
end
|
158
|
-
```
|
3
|
+
See the website for [documentation](https://literal.fun/docs).
|
@@ -10,25 +10,18 @@ class Literal::DataStructure
|
|
10
10
|
object
|
11
11
|
end
|
12
12
|
|
13
|
-
def ==(other)
|
14
|
-
if Literal::DataStructure === other
|
15
|
-
to_h == other.to_h
|
16
|
-
else
|
17
|
-
false
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
def hash
|
22
|
-
[self.class, to_h].hash
|
23
|
-
end
|
24
|
-
|
25
13
|
def [](key)
|
26
|
-
instance_variable_get("@#{key}")
|
14
|
+
instance_variable_get(:"@#{key}")
|
27
15
|
end
|
28
16
|
|
29
17
|
def []=(key, value)
|
30
|
-
|
31
|
-
|
18
|
+
# TODO: Sync error array w/ generated setter
|
19
|
+
@literal_properties[key].check(value) { |c| raise NotImplementedError }
|
20
|
+
instance_variable_set(:"@#{key}", value)
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_h
|
24
|
+
{}
|
32
25
|
end
|
33
26
|
|
34
27
|
def deconstruct
|
@@ -57,4 +50,20 @@ class Literal::DataStructure
|
|
57
50
|
def marshal_dump
|
58
51
|
[1, to_h, frozen?]
|
59
52
|
end
|
53
|
+
|
54
|
+
def hash
|
55
|
+
self.class.hash
|
56
|
+
end
|
57
|
+
|
58
|
+
def ==(other)
|
59
|
+
other.is_a?(self.class) && other.class.literal_properties.empty?
|
60
|
+
end
|
61
|
+
alias eql? ==
|
62
|
+
|
63
|
+
def self.__generate_literal_methods__(new_property, buffer = +"")
|
64
|
+
super
|
65
|
+
literal_properties.generate_hash(buffer)
|
66
|
+
literal_properties.generate_eq(buffer)
|
67
|
+
buffer
|
68
|
+
end
|
60
69
|
end
|
data/lib/literal/enum.rb
CHANGED
@@ -21,6 +21,15 @@ class Literal::Enum
|
|
21
21
|
@indexes = {}
|
22
22
|
@index = {}
|
23
23
|
end
|
24
|
+
|
25
|
+
if RUBY_ENGINE != "truffleruby"
|
26
|
+
TracePoint.trace(:end) do |tp|
|
27
|
+
if tp.self == subclass
|
28
|
+
tp.self.__after_defined__
|
29
|
+
tp.disable
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
24
33
|
end
|
25
34
|
|
26
35
|
def index(name, type, unique: true, &block)
|
@@ -34,9 +43,9 @@ class Literal::Enum
|
|
34
43
|
|
35
44
|
key, value = kwargs.first
|
36
45
|
|
37
|
-
|
38
|
-
|
39
|
-
|
46
|
+
types = @indexes.fetch(key)
|
47
|
+
type = types.first
|
48
|
+
Literal.check(actual: value, expected: type) { |c| raise NotImplementedError }
|
40
49
|
|
41
50
|
@index.fetch(key)[value]
|
42
51
|
end
|
@@ -76,9 +85,9 @@ class Literal::Enum
|
|
76
85
|
end
|
77
86
|
end
|
78
87
|
|
79
|
-
def new(
|
88
|
+
def new(*args, **kwargs, &block)
|
80
89
|
raise ArgumentError if frozen?
|
81
|
-
new_object = super(
|
90
|
+
new_object = super(*args, **kwargs, &nil)
|
82
91
|
|
83
92
|
if block
|
84
93
|
new_object.instance_exec(&block)
|
@@ -90,6 +99,10 @@ class Literal::Enum
|
|
90
99
|
def __after_defined__
|
91
100
|
raise ArgumentError if frozen?
|
92
101
|
|
102
|
+
if RUBY_VERSION < "3.2"
|
103
|
+
constants(false).each { |name| const_added(name) }
|
104
|
+
end
|
105
|
+
|
93
106
|
@indexes.each do |name, (type, unique, block)|
|
94
107
|
index = @members.group_by(&block).freeze
|
95
108
|
|
@@ -111,8 +124,8 @@ class Literal::Enum
|
|
111
124
|
freeze
|
112
125
|
end
|
113
126
|
|
114
|
-
def each
|
115
|
-
@members.each(
|
127
|
+
def each
|
128
|
+
@members.each { |member| yield(member) }
|
116
129
|
end
|
117
130
|
|
118
131
|
def each_value(&)
|
@@ -166,11 +179,3 @@ class Literal::Enum
|
|
166
179
|
Marshal.dump(@value)
|
167
180
|
end
|
168
181
|
end
|
169
|
-
|
170
|
-
TracePoint.trace(:end) do |tp|
|
171
|
-
it = tp.self
|
172
|
-
|
173
|
-
if Class === it && it < Literal::Enum
|
174
|
-
it.__after_defined__
|
175
|
-
end
|
176
|
-
end
|
@@ -1,10 +1,83 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class Literal::TypeError < TypeError
|
4
|
+
INDENT = " "
|
5
|
+
|
4
6
|
include Literal::Error
|
5
7
|
|
6
|
-
|
7
|
-
|
8
|
-
|
8
|
+
class Context
|
9
|
+
attr_reader :receiver, :method, :label, :expected, :actual, :children
|
10
|
+
|
11
|
+
def initialize(
|
12
|
+
receiver: nil, # _Nilable(Object)
|
13
|
+
method: nil, # _Nilable(String)
|
14
|
+
label: nil, # _Nilable(String)
|
15
|
+
expected: nil, # _Nilable(_Any)
|
16
|
+
actual: nil, # _Nilable(_Any)
|
17
|
+
parent: nil # _Nilable(Context)
|
18
|
+
)
|
19
|
+
@receiver = receiver
|
20
|
+
@method = method
|
21
|
+
@label = label
|
22
|
+
@expected = expected
|
23
|
+
@actual = actual
|
24
|
+
@children = []
|
25
|
+
end
|
26
|
+
|
27
|
+
def descend(level = 0, &blk)
|
28
|
+
yield self, level
|
29
|
+
@children.each { |child| child.descend(level + 1, &blk) }
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def fill_receiver(receiver:, method:, label: nil)
|
34
|
+
@receiver = receiver
|
35
|
+
@method = method
|
36
|
+
@label = label
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_child(expected: nil, **kwargs)
|
40
|
+
child = self.class.new(expected:, **kwargs)
|
41
|
+
expected.record_literal_type_errors(child) if expected.respond_to?(:record_literal_type_errors)
|
42
|
+
@children << child
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize(context:)
|
47
|
+
@context = context
|
48
|
+
|
49
|
+
super()
|
50
|
+
end
|
51
|
+
|
52
|
+
def message
|
53
|
+
message = +"Type mismatch\n\n"
|
54
|
+
|
55
|
+
@context.descend do |c, level|
|
56
|
+
idt = INDENT * level
|
57
|
+
if c.receiver || c.method
|
58
|
+
message << idt
|
59
|
+
message << c.receiver.class.inspect if c.receiver
|
60
|
+
message << c.method if c.method
|
61
|
+
message << " (from #{backtrace[1]})" if level.zero?
|
62
|
+
message << "\n"
|
63
|
+
end
|
64
|
+
if c.label
|
65
|
+
idt << INDENT
|
66
|
+
message << idt << c.label << "\n"
|
67
|
+
end
|
68
|
+
if c.expected && c.children.empty?
|
69
|
+
message << idt << " Expected: #{c.expected.inspect}\n"
|
70
|
+
message << idt << " Actual (#{c.actual.class}): #{c.actual.inspect}\n"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
message
|
74
|
+
end
|
75
|
+
|
76
|
+
def deconstruct_keys(keys)
|
77
|
+
to_h.slice(*keys)
|
78
|
+
end
|
79
|
+
|
80
|
+
def to_h
|
81
|
+
@context.to_h
|
9
82
|
end
|
10
83
|
end
|
@@ -1,9 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class Literal::Properties::DataSchema < Literal::Properties::Schema
|
4
|
-
def
|
5
|
-
|
6
|
-
|
7
|
-
buffer << "after_initialize if respond_to?(:after_initialize)\nfreeze\n"
|
4
|
+
def generate_after_initializer(buffer = +"")
|
5
|
+
super
|
6
|
+
buffer << " freeze\n"
|
8
7
|
end
|
9
8
|
end
|
@@ -19,7 +19,9 @@ class Literal::Properties::Schema
|
|
19
19
|
def <<(value)
|
20
20
|
@mutex.synchronize do
|
21
21
|
@properties_index[value.name] = value
|
22
|
-
|
22
|
+
# ruby's sort is unstable, this trick makes it stable
|
23
|
+
n = 0
|
24
|
+
@sorted_properties = @properties_index.values.sort_by! { |it| n += 1; [it, n] }
|
23
25
|
end
|
24
26
|
|
25
27
|
self
|
@@ -40,24 +42,71 @@ class Literal::Properties::Schema
|
|
40
42
|
@sorted_properties.size
|
41
43
|
end
|
42
44
|
|
45
|
+
def empty?
|
46
|
+
@sorted_properties.empty?
|
47
|
+
end
|
48
|
+
|
43
49
|
def generate_initializer(buffer = +"")
|
50
|
+
buffer << "alias initialize initialize\n"
|
44
51
|
buffer << "def initialize(#{generate_initializer_params})\n"
|
45
52
|
generate_initializer_body(buffer)
|
53
|
+
buffer << "" \
|
54
|
+
"rescue Literal::TypeError => error\n" \
|
55
|
+
" error.set_backtrace(caller(2))\n" \
|
56
|
+
" raise\n" \
|
57
|
+
"else\n"
|
58
|
+
generate_after_initializer(buffer)
|
46
59
|
buffer << "end\n"
|
47
60
|
end
|
48
61
|
|
62
|
+
def generate_after_initializer(buffer = +"")
|
63
|
+
buffer << " after_initialize if respond_to?(:after_initialize)\n"
|
64
|
+
end
|
65
|
+
|
49
66
|
def generate_to_h(buffer = +"")
|
50
|
-
buffer << "
|
67
|
+
buffer << "alias to_h to_h\n"
|
68
|
+
buffer << "def to_h\n" << " {\n"
|
51
69
|
|
52
70
|
sorted_properties = @sorted_properties
|
53
71
|
i, n = 0, sorted_properties.size
|
54
72
|
while i < n
|
55
73
|
property = sorted_properties[i]
|
56
|
-
buffer << property.name.name << ": @" << property.name.name << ",\n"
|
74
|
+
buffer << " " << property.name.name << ": @" << property.name.name << ",\n"
|
57
75
|
i += 1
|
58
76
|
end
|
59
77
|
|
60
|
-
buffer << "}\n" << "end\n"
|
78
|
+
buffer << " }\n" << "end\n"
|
79
|
+
end
|
80
|
+
|
81
|
+
def generate_hash(buffer = +"")
|
82
|
+
buffer << "def hash\n [self.class,\n"
|
83
|
+
|
84
|
+
sorted_properties = @sorted_properties
|
85
|
+
i, n = 0, sorted_properties.size
|
86
|
+
while i < n
|
87
|
+
property = sorted_properties[i]
|
88
|
+
buffer << " @" << property.name.name << ",\n"
|
89
|
+
i += 1
|
90
|
+
end
|
91
|
+
|
92
|
+
buffer << " ].hash\n" << "end\n"
|
93
|
+
end
|
94
|
+
|
95
|
+
def generate_eq(buffer = +"")
|
96
|
+
buffer << "def ==(other)\n"
|
97
|
+
buffer << " return false unless other.is_a?(self.class) && other.class.literal_properties.size == self.class.literal_properties.size\n"
|
98
|
+
|
99
|
+
sorted_properties = @sorted_properties
|
100
|
+
i, n = 0, sorted_properties.size
|
101
|
+
while i < n
|
102
|
+
property = sorted_properties[i]
|
103
|
+
buffer << " @" << property.name.name << " == other.#{property.escaped_name}"
|
104
|
+
buffer << " &&\n " if i < n - 1
|
105
|
+
i += 1
|
106
|
+
end
|
107
|
+
buffer << " true" if n.zero?
|
108
|
+
buffer << "\nend\n"
|
109
|
+
buffer << "alias eql? ==\n"
|
61
110
|
end
|
62
111
|
|
63
112
|
private
|
@@ -83,7 +132,7 @@ class Literal::Properties::Schema
|
|
83
132
|
else # required
|
84
133
|
buffer << property.escaped_name
|
85
134
|
end
|
86
|
-
|
135
|
+
when :keyword
|
87
136
|
if property.default?
|
88
137
|
buffer << property.name.name << ": Literal::Null"
|
89
138
|
elsif property.type === nil
|
@@ -91,6 +140,8 @@ class Literal::Properties::Schema
|
|
91
140
|
else # required
|
92
141
|
buffer << property.name.name << ":"
|
93
142
|
end
|
143
|
+
else
|
144
|
+
raise "You should never see this error."
|
94
145
|
end
|
95
146
|
|
96
147
|
i += 1
|
@@ -101,9 +152,8 @@ class Literal::Properties::Schema
|
|
101
152
|
end
|
102
153
|
|
103
154
|
def generate_initializer_body(buffer = +"")
|
104
|
-
buffer << "properties = self.class.literal_properties.properties_index\n"
|
155
|
+
buffer << " properties = self.class.literal_properties.properties_index\n"
|
105
156
|
generate_initializer_handle_properties(@sorted_properties, buffer)
|
106
|
-
buffer << "after_initialize if respond_to?(:after_initialize)\n"
|
107
157
|
end
|
108
158
|
|
109
159
|
def generate_initializer_handle_properties(properties, buffer = +"")
|
data/lib/literal/properties.rb
CHANGED
@@ -6,6 +6,11 @@ module Literal::Properties
|
|
6
6
|
|
7
7
|
include Literal::Types
|
8
8
|
|
9
|
+
def self.extended(base)
|
10
|
+
super
|
11
|
+
base.include(base.__send__(:__literal_extension__))
|
12
|
+
end
|
13
|
+
|
9
14
|
def prop(name, type, kind = :keyword, reader: false, writer: false, predicate: false, default: nil, &coercion)
|
10
15
|
if default && !(Proc === default || default.frozen?)
|
11
16
|
raise Literal::ArgumentError.new("The default must be a frozen object or a Proc.")
|
@@ -66,16 +71,25 @@ module Literal::Properties
|
|
66
71
|
end
|
67
72
|
|
68
73
|
def __define_literal_methods__(new_property)
|
69
|
-
|
70
|
-
|
71
|
-
)
|
74
|
+
code = __generate_literal_methods__(new_property)
|
75
|
+
__literal_extension__.module_eval(code)
|
72
76
|
end
|
73
77
|
|
74
78
|
def __literal_extension__
|
75
79
|
if defined?(@__literal_extension__)
|
76
80
|
@__literal_extension__
|
77
81
|
else
|
78
|
-
@__literal_extension__ = Module.new
|
82
|
+
@__literal_extension__ = Module.new do
|
83
|
+
def initialize
|
84
|
+
after_initialize if respond_to?(:after_initialize)
|
85
|
+
end
|
86
|
+
|
87
|
+
def to_h
|
88
|
+
{}
|
89
|
+
end
|
90
|
+
|
91
|
+
set_temporary_name "Literal::Properties(Extension)" if respond_to?(:set_temporary_name)
|
92
|
+
end
|
79
93
|
end
|
80
94
|
end
|
81
95
|
|
data/lib/literal/property.rb
CHANGED
@@ -51,7 +51,24 @@ class Literal::Property
|
|
51
51
|
end
|
52
52
|
|
53
53
|
def default?
|
54
|
-
|
54
|
+
nil != @default
|
55
|
+
end
|
56
|
+
|
57
|
+
def param
|
58
|
+
case @kind
|
59
|
+
when :*
|
60
|
+
"*#{escaped_name}"
|
61
|
+
when :**
|
62
|
+
"**#{escaped_name}"
|
63
|
+
when :&
|
64
|
+
"&#{escaped_name}"
|
65
|
+
when :positional
|
66
|
+
escaped_name
|
67
|
+
when :keyword
|
68
|
+
"#{@name.name}:"
|
69
|
+
else
|
70
|
+
raise "You should never see this error."
|
71
|
+
end
|
55
72
|
end
|
56
73
|
|
57
74
|
def <=>(other)
|
@@ -77,18 +94,28 @@ class Literal::Property
|
|
77
94
|
end
|
78
95
|
end
|
79
96
|
|
80
|
-
def check(value)
|
81
|
-
|
97
|
+
def check(value, &)
|
98
|
+
raise ArgumentError.new("Cannot check type without a block") unless block_given?
|
99
|
+
|
100
|
+
Literal.check(actual: value, expected: @type, &)
|
101
|
+
end
|
102
|
+
|
103
|
+
def check_writer(receiver, value)
|
104
|
+
Literal.check(actual: value, expected: @type) { |c| c.fill_receiver(receiver:, method: "##{@name.name}=(value)") }
|
105
|
+
end
|
106
|
+
|
107
|
+
def check_initializer(receiver, value)
|
108
|
+
Literal.check(actual: value, expected: @type) { |c| c.fill_receiver(receiver:, method: "#initialize", label: param) }
|
82
109
|
end
|
83
110
|
|
84
111
|
def generate_reader_method(buffer = +"")
|
85
112
|
buffer <<
|
86
113
|
(@reader ? @reader.name : "public") <<
|
87
|
-
"
|
114
|
+
"\ndef " <<
|
88
115
|
@name.name <<
|
89
|
-
"\
|
116
|
+
"\n value = @" <<
|
90
117
|
@name.name <<
|
91
|
-
"\
|
118
|
+
"\n value\nend\n"
|
92
119
|
end
|
93
120
|
|
94
121
|
if Literal::TYPE_CHECKS_DISABLED
|
@@ -98,7 +125,7 @@ class Literal::Property
|
|
98
125
|
" def " <<
|
99
126
|
@name.name <<
|
100
127
|
"=(value)\n" <<
|
101
|
-
"@#{@name.name} = value\nend\n"
|
128
|
+
" @#{@name.name} = value\nend\n"
|
102
129
|
end
|
103
130
|
else # type checks are enabled
|
104
131
|
def generate_writer_method(buffer = +"")
|
@@ -107,10 +134,12 @@ class Literal::Property
|
|
107
134
|
" def " <<
|
108
135
|
@name.name <<
|
109
136
|
"=(value)\n" <<
|
110
|
-
"self.class.literal_properties[:" <<
|
137
|
+
" self.class.literal_properties[:" <<
|
111
138
|
@name.name <<
|
112
|
-
"].
|
113
|
-
"
|
139
|
+
"].check_writer(self, value)\n" <<
|
140
|
+
" @" << @name.name << " = value\n" <<
|
141
|
+
"rescue Literal::TypeError => error\n error.set_backtrace(caller(1))\n raise\n" <<
|
142
|
+
"end\n"
|
114
143
|
end
|
115
144
|
end
|
116
145
|
|
@@ -120,15 +149,15 @@ class Literal::Property
|
|
120
149
|
" def " <<
|
121
150
|
@name.name <<
|
122
151
|
"?\n" <<
|
123
|
-
"!!@" <<
|
152
|
+
" !!@" <<
|
124
153
|
@name.name <<
|
125
154
|
"\n" <<
|
126
155
|
"end\n"
|
127
156
|
end
|
128
157
|
|
129
158
|
def generate_initializer_handle_property(buffer = +"")
|
130
|
-
buffer << "# " << @name.name << "\n" <<
|
131
|
-
"property = properties[:" << @name.name << "]\n"
|
159
|
+
buffer << " # " << @name.name << "\n" <<
|
160
|
+
" property = properties[:" << @name.name << "]\n"
|
132
161
|
|
133
162
|
if @kind == :keyword && ruby_keyword?
|
134
163
|
generate_initializer_escape_keyword(buffer)
|
@@ -169,25 +198,23 @@ class Literal::Property
|
|
169
198
|
|
170
199
|
def generate_initializer_assign_default(buffer = +"")
|
171
200
|
buffer <<
|
172
|
-
"if " <<
|
201
|
+
" if " <<
|
173
202
|
((@kind == :&) ? "nil" : "Literal::Null") <<
|
174
203
|
" == " <<
|
175
204
|
escaped_name <<
|
176
|
-
"\n" <<
|
205
|
+
"\n " <<
|
177
206
|
escaped_name <<
|
178
|
-
" = property.default_value\
|
207
|
+
" = property.default_value\n end\n"
|
179
208
|
end
|
180
209
|
|
181
210
|
def generate_initializer_check_type(buffer = +"")
|
182
211
|
buffer <<
|
183
|
-
"
|
184
|
-
"raise Literal::TypeError.expected(" << escaped_name << ", to_be_a: property.type)\n" <<
|
185
|
-
"end\n"
|
212
|
+
" property.check_initializer(self, " << escaped_name << ")\n"
|
186
213
|
end
|
187
214
|
|
188
215
|
def generate_initializer_assign_value(buffer = +"")
|
189
216
|
buffer <<
|
190
|
-
"@" <<
|
217
|
+
" @" <<
|
191
218
|
@name.name <<
|
192
219
|
" = " <<
|
193
220
|
escaped_name <<
|