literal 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|