literal 0.1.0 → 0.2.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 +4 -4
- data/README.md +100 -54
- data/lib/literal/data.rb +24 -11
- data/lib/literal/data_property.rb +16 -0
- data/lib/literal/data_structure.rb +60 -0
- data/lib/literal/enum.rb +176 -0
- data/lib/literal/errors/argument_error.rb +5 -0
- data/lib/literal/errors/error.rb +4 -0
- data/lib/literal/errors/type_error.rb +10 -0
- data/lib/literal/null.rb +9 -0
- data/lib/literal/object.rb +5 -0
- data/lib/literal/properties/data_schema.rb +9 -0
- data/lib/literal/properties/schema.rb +118 -0
- data/lib/literal/properties.rb +91 -0
- data/lib/literal/property.rb +196 -0
- data/lib/literal/struct.rb +8 -34
- data/lib/literal/types/any_type.rb +10 -3
- data/lib/literal/types/array_type.rb +10 -9
- data/lib/literal/types/boolean_type.rb +18 -1
- data/lib/literal/types/callable_type.rb +12 -0
- data/lib/literal/types/class_type.rb +10 -9
- data/lib/literal/types/constraint_type.rb +16 -0
- data/lib/literal/types/descendant_type.rb +13 -0
- data/lib/literal/types/enumerable_type.rb +10 -9
- data/lib/literal/types/falsy_type.rb +12 -0
- data/lib/literal/types/float_type.rb +7 -10
- data/lib/literal/types/frozen_type.rb +14 -0
- data/lib/literal/types/hash_type.rb +11 -10
- data/lib/literal/types/integer_type.rb +7 -10
- data/lib/literal/types/interface_type.rb +11 -9
- data/lib/literal/types/intersection_type.rb +20 -0
- data/lib/literal/types/json_data_type.rb +21 -0
- data/lib/literal/types/lambda_type.rb +12 -0
- data/lib/literal/types/map_type.rb +16 -0
- data/lib/literal/types/never_type.rb +12 -0
- data/lib/literal/types/nilable_type.rb +14 -0
- data/lib/literal/types/not_type.rb +14 -0
- data/lib/literal/types/procable_type.rb +12 -0
- data/lib/literal/types/range_type.rb +20 -0
- data/lib/literal/types/set_type.rb +10 -9
- data/lib/literal/types/string_type.rb +10 -0
- data/lib/literal/types/symbol_type.rb +10 -0
- data/lib/literal/types/truthy_type.rb +12 -0
- data/lib/literal/types/tuple_type.rb +12 -9
- data/lib/literal/types/union_type.rb +43 -9
- data/lib/literal/types/void_type.rb +12 -0
- data/lib/literal/types.rb +195 -54
- data/lib/literal/version.rb +1 -1
- data/lib/literal.rb +28 -10
- data/lib/literal.test.rb +5 -0
- metadata +41 -19
- data/CHANGELOG.md +0 -5
- data/CODE_OF_CONDUCT.md +0 -84
- data/Gemfile +0 -9
- data/Gemfile.lock +0 -29
- data/Rakefile +0 -12
- data/lib/literal/attributes.rb +0 -33
- data/lib/literal/initializer.rb +0 -11
- data/lib/literal/model.rb +0 -22
- data/literal.gemspec +0 -37
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6ab1d4d21286c3cc2a094362e0f1f6a94856738c7ba9a93d285607bb57ce49b5
|
4
|
+
data.tar.gz: 27c2f4fe6ecbb00fcaeb5faf5b4d13849c3ef8dcc6eec817775f55648fac4847
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ef894ddc2c2ee8ca82175d3f833ca0a05f479adf164766c681c4bc165a21840f8431da8c4ee6d4d971b10bf4f37427af40914795f2bdd14855927f818633abb7
|
7
|
+
data.tar.gz: 806aa654d1670c8842dae039c4e0845dbd1ea304bfabb634aefc0ea79b380889d8e249b3348274d11cc7ac01b173e3c9b468d13d4c44f321ceaa8ac83d5b11ec
|
data/README.md
CHANGED
@@ -1,112 +1,158 @@
|
|
1
|
-
# Literal
|
1
|
+
# A Literal Ruby Gem [WIP]
|
2
2
|
|
3
|
-
##
|
3
|
+
## Types
|
4
4
|
|
5
|
-
|
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
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
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.
|
10
13
|
|
11
|
-
|
12
|
-
attribute :age, Integer
|
13
|
-
end
|
14
|
-
```
|
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
15
|
|
16
|
-
|
16
|
+
If we want to check that a given value is an `Array`, we could do this:
|
17
17
|
|
18
18
|
```ruby
|
19
|
-
|
20
|
-
attribute :name, String
|
21
|
-
attribute :age, Integer
|
22
|
-
end
|
19
|
+
Array === [1, 2, 3]
|
23
20
|
```
|
24
21
|
|
25
|
-
|
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)`.
|
26
23
|
|
27
24
|
```ruby
|
28
|
-
|
29
|
-
attribute :name, String
|
30
|
-
attribute :age, Integer
|
31
|
-
end
|
25
|
+
_Array(Integer) === [1, 2, 3]
|
32
26
|
```
|
33
27
|
|
34
|
-
|
35
|
-
|
36
|
-
### Union
|
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.
|
37
29
|
|
38
30
|
```ruby
|
39
|
-
|
31
|
+
extend Literal::Types
|
40
32
|
```
|
41
33
|
|
42
|
-
|
34
|
+
This is recommended for applications, but not for libraries, as we don’t want to pollute the global namespace from library code.
|
43
35
|
|
44
|
-
|
45
|
-
_Boolean
|
46
|
-
```
|
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.
|
47
37
|
|
48
|
-
|
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.
|
49
45
|
|
50
46
|
```ruby
|
51
|
-
|
47
|
+
class Person
|
48
|
+
extend Literal::Properties
|
49
|
+
|
50
|
+
prop :name, String
|
51
|
+
prop :age, Integer
|
52
|
+
end
|
52
53
|
```
|
53
54
|
|
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`.
|
55
56
|
|
56
57
|
```ruby
|
57
|
-
|
58
|
+
class Person
|
59
|
+
extend Literal::Properties
|
60
|
+
|
61
|
+
prop :name, String, reader: :public
|
62
|
+
prop :age, Integer, writer: :protected
|
63
|
+
end
|
58
64
|
```
|
59
65
|
|
60
|
-
|
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)`:
|
61
67
|
|
62
68
|
```ruby
|
63
|
-
|
69
|
+
class Person
|
70
|
+
extend Literal::Properties
|
71
|
+
|
72
|
+
prop :name, String
|
73
|
+
prop :age, _Nilable(Integer)
|
74
|
+
end
|
64
75
|
```
|
65
76
|
|
66
|
-
|
77
|
+
Alternatively, you can give the property a default value. This default value must match the type of the property.
|
67
78
|
|
68
79
|
```ruby
|
69
|
-
|
80
|
+
class Person
|
81
|
+
extend Literal::Properties
|
82
|
+
|
83
|
+
prop :name, String, default: "John Doe"
|
84
|
+
prop :age, _Nilable(Integer)
|
85
|
+
end
|
70
86
|
```
|
71
87
|
|
72
|
-
|
73
|
-
An Enumerable containing exactly the specified types in order.
|
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.
|
74
89
|
|
75
90
|
```ruby
|
76
|
-
|
91
|
+
class Person
|
92
|
+
extend Literal::Properties
|
93
|
+
|
94
|
+
prop :name, String, default: -> { "John Doe" }
|
95
|
+
prop :age, _Nilable(Integer)
|
96
|
+
end
|
77
97
|
```
|
78
98
|
|
79
|
-
|
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.
|
80
102
|
|
81
103
|
```ruby
|
82
|
-
|
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
|
83
112
|
```
|
84
113
|
|
85
|
-
|
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
|
+
|
86
118
|
```ruby
|
87
|
-
|
119
|
+
class Person
|
120
|
+
extend Literal::Properties
|
121
|
+
|
122
|
+
prop :class, String, :positional
|
123
|
+
prop :end, Integer
|
124
|
+
end
|
88
125
|
```
|
89
126
|
|
90
|
-
|
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`.
|
91
128
|
|
92
129
|
```ruby
|
93
|
-
|
130
|
+
class Person < Literal::Object
|
131
|
+
prop :name, String
|
132
|
+
prop :age, Integer
|
133
|
+
end
|
94
134
|
```
|
95
135
|
|
96
|
-
|
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.
|
97
141
|
|
98
142
|
```ruby
|
99
|
-
|
143
|
+
class Person < Literal::Struct
|
144
|
+
prop :name, String
|
145
|
+
prop :age, Integer
|
146
|
+
end
|
100
147
|
```
|
101
148
|
|
102
|
-
|
103
|
-
You can of course just use `Integer` to specify an integer type. The special type `_Integer` allows you to limit that type with a range, while verifying that it's an integer and not something else that matches the range such as a float.
|
149
|
+
## Data
|
104
150
|
|
105
|
-
|
106
|
-
_Integer(18..)
|
107
|
-
```
|
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.
|
108
152
|
|
109
|
-
You can use these types together.
|
110
153
|
```ruby
|
111
|
-
|
154
|
+
class Person < Literal::Data
|
155
|
+
prop :name, String
|
156
|
+
prop :age, Integer
|
157
|
+
end
|
112
158
|
```
|
data/lib/literal/data.rb
CHANGED
@@ -1,12 +1,25 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Literal::Data < Literal::DataStructure
|
4
|
+
class << self
|
5
|
+
def prop(name, type, kind = :keyword, reader: :public, predicate: false, default: nil)
|
6
|
+
super(name, type, kind, reader:, writer: false, predicate:, default:)
|
7
|
+
end
|
8
|
+
|
9
|
+
def literal_properties
|
10
|
+
return @literal_properties if defined?(@literal_properties)
|
11
|
+
|
12
|
+
if superclass.is_a?(Literal::Data)
|
13
|
+
@literal_properties = superclass.literal_properties.dup
|
14
|
+
else
|
15
|
+
@literal_properties = Literal::Properties::DataSchema.new
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def __literal_property_class__
|
22
|
+
Literal::DataProperty
|
23
|
+
end
|
24
|
+
end
|
12
25
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Literal::DataProperty < Literal::Property
|
4
|
+
def generate_initializer_assign_value(buffer = +"")
|
5
|
+
buffer <<
|
6
|
+
"@" <<
|
7
|
+
@name.name <<
|
8
|
+
" = " <<
|
9
|
+
escaped_name <<
|
10
|
+
".frozen? ? " <<
|
11
|
+
escaped_name <<
|
12
|
+
" : " <<
|
13
|
+
escaped_name <<
|
14
|
+
".dup.freeze\n"
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @api private
|
4
|
+
class Literal::DataStructure
|
5
|
+
extend Literal::Properties
|
6
|
+
|
7
|
+
def self.from_pack(payload)
|
8
|
+
object = allocate
|
9
|
+
object.marshal_load(payload)
|
10
|
+
object
|
11
|
+
end
|
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
|
+
def [](key)
|
26
|
+
instance_variable_get("@#{key}")
|
27
|
+
end
|
28
|
+
|
29
|
+
def []=(key, value)
|
30
|
+
@literal_properties[key].check(value)
|
31
|
+
instance_variable_set("@#{key}", value)
|
32
|
+
end
|
33
|
+
|
34
|
+
def deconstruct
|
35
|
+
to_h.values
|
36
|
+
end
|
37
|
+
|
38
|
+
def deconstruct_keys(keys)
|
39
|
+
h = to_h
|
40
|
+
keys ? h.slice(*keys) : h
|
41
|
+
end
|
42
|
+
|
43
|
+
def as_pack
|
44
|
+
marshal_dump
|
45
|
+
end
|
46
|
+
|
47
|
+
def marshal_load(payload)
|
48
|
+
_version, attributes, was_frozen = payload
|
49
|
+
|
50
|
+
attributes.each do |key, value|
|
51
|
+
instance_variable_set("@#{key}", value)
|
52
|
+
end
|
53
|
+
|
54
|
+
freeze if was_frozen
|
55
|
+
end
|
56
|
+
|
57
|
+
def marshal_dump
|
58
|
+
[1, to_h, frozen?]
|
59
|
+
end
|
60
|
+
end
|
data/lib/literal/enum.rb
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Literal::Enum
|
4
|
+
extend Literal::Properties
|
5
|
+
|
6
|
+
class << self
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
attr_reader :members
|
10
|
+
|
11
|
+
def values = @values.keys
|
12
|
+
|
13
|
+
def prop(name, type, kind = :keyword, reader: :public, default: nil)
|
14
|
+
super(name, type, kind, reader:, writer: false, default:)
|
15
|
+
end
|
16
|
+
|
17
|
+
def inherited(subclass)
|
18
|
+
subclass.instance_exec do
|
19
|
+
@values = {}
|
20
|
+
@members = Set[]
|
21
|
+
@indexes = {}
|
22
|
+
@index = {}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def index(name, type, unique: true, &block)
|
27
|
+
@indexes[name] = [type, unique, block || name.to_proc]
|
28
|
+
end
|
29
|
+
|
30
|
+
def where(**kwargs)
|
31
|
+
unless kwargs.length == 1
|
32
|
+
raise ArgumentError.new("You can only specify one index when using `where`.")
|
33
|
+
end
|
34
|
+
|
35
|
+
key, value = kwargs.first
|
36
|
+
|
37
|
+
unless (type = @indexes.fetch(key)[0]) === value
|
38
|
+
raise Literal::TypeError.expected(value, to_be_a: type)
|
39
|
+
end
|
40
|
+
|
41
|
+
@index.fetch(key)[value]
|
42
|
+
end
|
43
|
+
|
44
|
+
def find_by(**kwargs)
|
45
|
+
unless kwargs.length == 1
|
46
|
+
raise ArgumentError.new("You can only specify one index when using `where`.")
|
47
|
+
end
|
48
|
+
|
49
|
+
key, value = kwargs.first
|
50
|
+
|
51
|
+
unless @indexes.fetch(key)[1]
|
52
|
+
raise ArgumentError.new("You can only use `find_by` on unique indexes.")
|
53
|
+
end
|
54
|
+
|
55
|
+
unless (type = @indexes.fetch(key)[0]) === value
|
56
|
+
raise Literal::TypeError.expected(value, to_be_a: type)
|
57
|
+
end
|
58
|
+
|
59
|
+
@index.fetch(key)[value]&.first
|
60
|
+
end
|
61
|
+
|
62
|
+
def _load(data)
|
63
|
+
self[Marshal.load(data)]
|
64
|
+
end
|
65
|
+
|
66
|
+
def const_added(name)
|
67
|
+
raise ArgumentError if frozen?
|
68
|
+
object = const_get(name)
|
69
|
+
|
70
|
+
if self === object
|
71
|
+
object.instance_variable_set(:@name, name)
|
72
|
+
@values[object.value] = object
|
73
|
+
@members << object
|
74
|
+
define_method("#{name.to_s.gsub(/([^A-Z])([A-Z]+)/, '\1_\2').downcase}?") { self == object }
|
75
|
+
object.freeze
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def new(*, **, &block)
|
80
|
+
raise ArgumentError if frozen?
|
81
|
+
new_object = super(*, **, &nil)
|
82
|
+
|
83
|
+
if block
|
84
|
+
new_object.instance_exec(&block)
|
85
|
+
end
|
86
|
+
|
87
|
+
new_object
|
88
|
+
end
|
89
|
+
|
90
|
+
def __after_defined__
|
91
|
+
raise ArgumentError if frozen?
|
92
|
+
|
93
|
+
@indexes.each do |name, (type, unique, block)|
|
94
|
+
index = @members.group_by(&block).freeze
|
95
|
+
|
96
|
+
index.each do |key, values|
|
97
|
+
unless type === key
|
98
|
+
raise Literal::TypeError.expected(key, to_be_a: type)
|
99
|
+
end
|
100
|
+
|
101
|
+
if unique && values.size > 1
|
102
|
+
raise ArgumentError.new("The index #{name} is not unique.")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
@index[name] = index
|
107
|
+
end
|
108
|
+
|
109
|
+
@values.freeze
|
110
|
+
@members.freeze
|
111
|
+
freeze
|
112
|
+
end
|
113
|
+
|
114
|
+
def each(&)
|
115
|
+
@members.each(&)
|
116
|
+
end
|
117
|
+
|
118
|
+
def each_value(&)
|
119
|
+
@values.each_key(&)
|
120
|
+
end
|
121
|
+
|
122
|
+
def [](value)
|
123
|
+
@values[value]
|
124
|
+
end
|
125
|
+
|
126
|
+
alias_method :cast, :[]
|
127
|
+
|
128
|
+
def fetch(...)
|
129
|
+
@values.fetch(...)
|
130
|
+
end
|
131
|
+
|
132
|
+
def to_proc
|
133
|
+
method(:cast).to_proc
|
134
|
+
end
|
135
|
+
|
136
|
+
def to_h(*args)
|
137
|
+
@values.dup
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def initialize(name, value, &block)
|
142
|
+
@name = name
|
143
|
+
@value = value
|
144
|
+
instance_exec(&block) if block
|
145
|
+
freeze
|
146
|
+
end
|
147
|
+
|
148
|
+
attr_reader :value
|
149
|
+
|
150
|
+
def name
|
151
|
+
"#{self.class.name}::#{@name}"
|
152
|
+
end
|
153
|
+
|
154
|
+
alias_method :inspect, :name
|
155
|
+
|
156
|
+
def deconstruct
|
157
|
+
[@value]
|
158
|
+
end
|
159
|
+
|
160
|
+
def deconstruct_keys(keys)
|
161
|
+
h = to_h
|
162
|
+
keys ? h.slice(*keys) : h
|
163
|
+
end
|
164
|
+
|
165
|
+
def _dump(level)
|
166
|
+
Marshal.dump(@value)
|
167
|
+
end
|
168
|
+
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
|
data/lib/literal/null.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Literal::Properties::DataSchema < Literal::Properties::Schema
|
4
|
+
def generate_initializer_body(buffer = +"")
|
5
|
+
buffer << "properties = self.class.literal_properties.properties_index\n"
|
6
|
+
generate_initializer_handle_properties(@sorted_properties, buffer)
|
7
|
+
buffer << "after_initialize if respond_to?(:after_initialize)\nfreeze\n"
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @api private
|
4
|
+
class Literal::Properties::Schema
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def initialize(properties_index: {}, sorted_properties: [])
|
8
|
+
@properties_index = properties_index
|
9
|
+
@sorted_properties = sorted_properties
|
10
|
+
@mutex = Mutex.new
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :properties_index
|
14
|
+
|
15
|
+
def [](key)
|
16
|
+
@properties_index[key]
|
17
|
+
end
|
18
|
+
|
19
|
+
def <<(value)
|
20
|
+
@mutex.synchronize do
|
21
|
+
@properties_index[value.name] = value
|
22
|
+
@sorted_properties = @properties_index.values.sort!
|
23
|
+
end
|
24
|
+
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def dup
|
29
|
+
self.class.new(
|
30
|
+
properties_index: @properties_index.dup,
|
31
|
+
sorted_properties: @sorted_properties.dup,
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
def each(&)
|
36
|
+
@sorted_properties.each(&)
|
37
|
+
end
|
38
|
+
|
39
|
+
def size
|
40
|
+
@sorted_properties.size
|
41
|
+
end
|
42
|
+
|
43
|
+
def generate_initializer(buffer = +"")
|
44
|
+
buffer << "def initialize(#{generate_initializer_params})\n"
|
45
|
+
generate_initializer_body(buffer)
|
46
|
+
buffer << "end\n"
|
47
|
+
end
|
48
|
+
|
49
|
+
def generate_to_h(buffer = +"")
|
50
|
+
buffer << "def to_h\n" << "{\n"
|
51
|
+
|
52
|
+
sorted_properties = @sorted_properties
|
53
|
+
i, n = 0, sorted_properties.size
|
54
|
+
while i < n
|
55
|
+
property = sorted_properties[i]
|
56
|
+
buffer << property.name.name << ": @" << property.name.name << ",\n"
|
57
|
+
i += 1
|
58
|
+
end
|
59
|
+
|
60
|
+
buffer << "}\n" << "end\n"
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def generate_initializer_params(buffer = +"")
|
66
|
+
sorted_properties = @sorted_properties
|
67
|
+
i, n = 0, sorted_properties.size
|
68
|
+
while i < n
|
69
|
+
property = sorted_properties[i]
|
70
|
+
|
71
|
+
case property.kind
|
72
|
+
when :*
|
73
|
+
buffer << "*" << property.escaped_name
|
74
|
+
when :**
|
75
|
+
buffer << "**" << property.escaped_name
|
76
|
+
when :&
|
77
|
+
buffer << "&" << property.escaped_name
|
78
|
+
when :positional
|
79
|
+
if property.default?
|
80
|
+
buffer << property.escaped_name << " = Literal::Null"
|
81
|
+
elsif property.type === nil # optional
|
82
|
+
buffer << property.escaped_name << " = nil"
|
83
|
+
else # required
|
84
|
+
buffer << property.escaped_name
|
85
|
+
end
|
86
|
+
else # keyword
|
87
|
+
if property.default?
|
88
|
+
buffer << property.name.name << ": Literal::Null"
|
89
|
+
elsif property.type === nil
|
90
|
+
buffer << property.name.name << ": nil" # optional
|
91
|
+
else # required
|
92
|
+
buffer << property.name.name << ":"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
i += 1
|
97
|
+
buffer << ", " if i < n
|
98
|
+
end
|
99
|
+
|
100
|
+
buffer
|
101
|
+
end
|
102
|
+
|
103
|
+
def generate_initializer_body(buffer = +"")
|
104
|
+
buffer << "properties = self.class.literal_properties.properties_index\n"
|
105
|
+
generate_initializer_handle_properties(@sorted_properties, buffer)
|
106
|
+
buffer << "after_initialize if respond_to?(:after_initialize)\n"
|
107
|
+
end
|
108
|
+
|
109
|
+
def generate_initializer_handle_properties(properties, buffer = +"")
|
110
|
+
i, n = 0, properties.size
|
111
|
+
while i < n
|
112
|
+
properties[i].generate_initializer_handle_property(buffer)
|
113
|
+
i += 1
|
114
|
+
end
|
115
|
+
|
116
|
+
buffer
|
117
|
+
end
|
118
|
+
end
|