literal 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +100 -54
  3. data/lib/literal/data.rb +24 -11
  4. data/lib/literal/data_property.rb +16 -0
  5. data/lib/literal/data_structure.rb +60 -0
  6. data/lib/literal/enum.rb +176 -0
  7. data/lib/literal/errors/argument_error.rb +5 -0
  8. data/lib/literal/errors/error.rb +4 -0
  9. data/lib/literal/errors/type_error.rb +10 -0
  10. data/lib/literal/null.rb +9 -0
  11. data/lib/literal/object.rb +5 -0
  12. data/lib/literal/properties/data_schema.rb +9 -0
  13. data/lib/literal/properties/schema.rb +118 -0
  14. data/lib/literal/properties.rb +91 -0
  15. data/lib/literal/property.rb +196 -0
  16. data/lib/literal/struct.rb +8 -34
  17. data/lib/literal/types/any_type.rb +10 -3
  18. data/lib/literal/types/array_type.rb +10 -9
  19. data/lib/literal/types/boolean_type.rb +18 -1
  20. data/lib/literal/types/callable_type.rb +12 -0
  21. data/lib/literal/types/class_type.rb +10 -9
  22. data/lib/literal/types/constraint_type.rb +16 -0
  23. data/lib/literal/types/descendant_type.rb +13 -0
  24. data/lib/literal/types/enumerable_type.rb +10 -9
  25. data/lib/literal/types/falsy_type.rb +12 -0
  26. data/lib/literal/types/float_type.rb +7 -10
  27. data/lib/literal/types/frozen_type.rb +14 -0
  28. data/lib/literal/types/hash_type.rb +11 -10
  29. data/lib/literal/types/integer_type.rb +7 -10
  30. data/lib/literal/types/interface_type.rb +11 -9
  31. data/lib/literal/types/intersection_type.rb +20 -0
  32. data/lib/literal/types/json_data_type.rb +21 -0
  33. data/lib/literal/types/lambda_type.rb +12 -0
  34. data/lib/literal/types/map_type.rb +16 -0
  35. data/lib/literal/types/never_type.rb +12 -0
  36. data/lib/literal/types/nilable_type.rb +14 -0
  37. data/lib/literal/types/not_type.rb +14 -0
  38. data/lib/literal/types/procable_type.rb +12 -0
  39. data/lib/literal/types/range_type.rb +20 -0
  40. data/lib/literal/types/set_type.rb +10 -9
  41. data/lib/literal/types/string_type.rb +10 -0
  42. data/lib/literal/types/symbol_type.rb +10 -0
  43. data/lib/literal/types/truthy_type.rb +12 -0
  44. data/lib/literal/types/tuple_type.rb +12 -9
  45. data/lib/literal/types/union_type.rb +43 -9
  46. data/lib/literal/types/void_type.rb +12 -0
  47. data/lib/literal/types.rb +195 -54
  48. data/lib/literal/version.rb +1 -1
  49. data/lib/literal.rb +28 -10
  50. data/lib/literal.test.rb +5 -0
  51. metadata +41 -19
  52. data/CHANGELOG.md +0 -5
  53. data/CODE_OF_CONDUCT.md +0 -84
  54. data/Gemfile +0 -9
  55. data/Gemfile.lock +0 -29
  56. data/Rakefile +0 -12
  57. data/lib/literal/attributes.rb +0 -33
  58. data/lib/literal/initializer.rb +0 -11
  59. data/lib/literal/model.rb +0 -22
  60. data/literal.gemspec +0 -37
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Literal::Properties
4
+ autoload :Schema, "literal/properties/schema"
5
+ autoload :DataSchema, "literal/properties/data_schema"
6
+
7
+ include Literal::Types
8
+
9
+ def prop(name, type, kind = :keyword, reader: false, writer: false, predicate: false, default: nil, &coercion)
10
+ if default && !(Proc === default || default.frozen?)
11
+ raise Literal::ArgumentError.new("The default must be a frozen object or a Proc.")
12
+ end
13
+
14
+ unless Literal::Property::VISIBILITY_OPTIONS.include?(reader)
15
+ raise Literal::ArgumentError.new("The reader must be one of #{Literal::Property::VISIBILITY_OPTIONS.map(&:inspect).join(', ')}.")
16
+ end
17
+
18
+ unless Literal::Property::VISIBILITY_OPTIONS.include?(writer)
19
+ raise Literal::ArgumentError.new("The writer must be one of #{Literal::Property::VISIBILITY_OPTIONS.map(&:inspect).join(', ')}.")
20
+ end
21
+
22
+ unless Literal::Property::VISIBILITY_OPTIONS.include?(predicate)
23
+ raise Literal::ArgumentError.new("The predicate must be one of #{Literal::Property::VISIBILITY_OPTIONS.map(&:inspect).join(', ')}.")
24
+ end
25
+
26
+ if reader && :class == name
27
+ raise Literal::ArgumentError.new(
28
+ "The `:class` property should not be defined as a reader because it breaks Ruby's `Object#class` method, which Literal itself depends on.",
29
+ )
30
+ end
31
+
32
+ unless Literal::Property::KIND_OPTIONS.include?(kind)
33
+ raise Literal::ArgumentError.new("The kind must be one of #{Literal::Property::KIND_OPTIONS.map(&:inspect).join(', ')}.")
34
+ end
35
+
36
+ property = __literal_property_class__.new(
37
+ name:,
38
+ type:,
39
+ kind:,
40
+ reader:,
41
+ writer:,
42
+ predicate:,
43
+ default:,
44
+ coercion:,
45
+ )
46
+
47
+ literal_properties << property
48
+ __define_literal_methods__(property)
49
+ include(__literal_extension__)
50
+ end
51
+
52
+ def literal_properties
53
+ return @literal_properties if defined?(@literal_properties)
54
+
55
+ if superclass.is_a?(Literal::Properties)
56
+ @literal_properties = superclass.literal_properties.dup
57
+ else
58
+ @literal_properties = Literal::Properties::Schema.new
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def __literal_property_class__
65
+ Literal::Property
66
+ end
67
+
68
+ def __define_literal_methods__(new_property)
69
+ __literal_extension__.module_eval(
70
+ __generate_literal_methods__(new_property),
71
+ )
72
+ end
73
+
74
+ def __literal_extension__
75
+ if defined?(@__literal_extension__)
76
+ @__literal_extension__
77
+ else
78
+ @__literal_extension__ = Module.new
79
+ end
80
+ end
81
+
82
+ def __generate_literal_methods__(new_property, buffer = +"")
83
+ buffer << "# frozen_string_literal: true\n"
84
+ literal_properties.generate_initializer(buffer)
85
+ literal_properties.generate_to_h(buffer)
86
+ new_property.generate_writer_method(buffer) if new_property.writer
87
+ new_property.generate_reader_method(buffer) if new_property.reader
88
+ new_property.generate_predicate_method(buffer) if new_property.predicate
89
+ buffer
90
+ end
91
+ end
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Literal::Property
4
+ ORDER = { :positional => 0, :* => 1, :keyword => 2, :** => 3, :& => 4 }.freeze
5
+ RUBY_KEYWORDS = %i[alias and begin break case class def do else elsif end ensure false for if in module next nil not or redo rescue retry return self super then true undef unless until when while yield].to_h { |k| [k, "__#{k}__"] }.freeze
6
+
7
+ VISIBILITY_OPTIONS = Set[false, :private, :protected, :public].freeze
8
+ KIND_OPTIONS = Set[:positional, :*, :keyword, :**, :&].freeze
9
+
10
+ include Comparable
11
+
12
+ def initialize(name:, type:, kind:, reader:, writer:, predicate:, default:, coercion:)
13
+ @name = name
14
+ @type = type
15
+ @kind = kind
16
+ @reader = reader
17
+ @writer = writer
18
+ @predicate = predicate
19
+ @default = default
20
+ @coercion = coercion
21
+ end
22
+
23
+ attr_reader :name, :type, :kind, :reader, :writer, :predicate, :default, :coercion
24
+
25
+ def optional?
26
+ default? || @type === nil
27
+ end
28
+
29
+ def required?
30
+ !optional?
31
+ end
32
+
33
+ def keyword?
34
+ @kind == :keyword
35
+ end
36
+
37
+ def positional?
38
+ @kind == :positional
39
+ end
40
+
41
+ def splat?
42
+ @kind == :*
43
+ end
44
+
45
+ def double_splat?
46
+ @kind == :**
47
+ end
48
+
49
+ def block?
50
+ @kind == :&
51
+ end
52
+
53
+ def default?
54
+ nil != @default
55
+ end
56
+
57
+ def <=>(other)
58
+ ORDER[@kind] <=> ORDER[other.kind]
59
+ end
60
+
61
+ def coerce(value, context:)
62
+ context.instance_exec(value, &@coercion)
63
+ end
64
+
65
+ def ruby_keyword?
66
+ !!RUBY_KEYWORDS[@name]
67
+ end
68
+
69
+ def escaped_name
70
+ RUBY_KEYWORDS[@name] || @name.name
71
+ end
72
+
73
+ def default_value
74
+ case @default
75
+ when Proc then @default.call
76
+ else @default
77
+ end
78
+ end
79
+
80
+ def check(value)
81
+ Literal.check(value, @type)
82
+ end
83
+
84
+ def generate_reader_method(buffer = +"")
85
+ buffer <<
86
+ (@reader ? @reader.name : "public") <<
87
+ " def " <<
88
+ @name.name <<
89
+ "\nvalue = @" <<
90
+ @name.name <<
91
+ "\nvalue\nend\n"
92
+ end
93
+
94
+ if Literal::TYPE_CHECKS_DISABLED
95
+ def generate_writer_method(buffer = +"")
96
+ buffer <<
97
+ (@writer ? @writer.name : "public") <<
98
+ " def " <<
99
+ @name.name <<
100
+ "=(value)\n" <<
101
+ "@#{@name.name} = value\nend\n"
102
+ end
103
+ else # type checks are enabled
104
+ def generate_writer_method(buffer = +"")
105
+ buffer <<
106
+ (@writer ? @writer.name : "public") <<
107
+ " def " <<
108
+ @name.name <<
109
+ "=(value)\n" <<
110
+ "self.class.literal_properties[:" <<
111
+ @name.name <<
112
+ "].check(value)\n" <<
113
+ "@#{@name.name} = value\nend\n"
114
+ end
115
+ end
116
+
117
+ def generate_predicate_method(buffer = +"")
118
+ buffer <<
119
+ (@predicate ? @predicate.name : "public") <<
120
+ " def " <<
121
+ @name.name <<
122
+ "?\n" <<
123
+ "!!@" <<
124
+ @name.name <<
125
+ "\n" <<
126
+ "end\n"
127
+ end
128
+
129
+ def generate_initializer_handle_property(buffer = +"")
130
+ buffer << "# " << @name.name << "\n" <<
131
+ "property = properties[:" << @name.name << "]\n"
132
+
133
+ if @kind == :keyword && ruby_keyword?
134
+ generate_initializer_escape_keyword(buffer)
135
+ end
136
+
137
+ if default?
138
+ generate_initializer_assign_default(buffer)
139
+ end
140
+
141
+ if @coercion
142
+ generate_initializer_coerce_property(buffer)
143
+ end
144
+
145
+ unless Literal::TYPE_CHECKS_DISABLED
146
+ generate_initializer_check_type(buffer)
147
+ end
148
+
149
+ generate_initializer_assign_value(buffer)
150
+ end
151
+
152
+ private
153
+
154
+ def generate_initializer_escape_keyword(buffer = +"")
155
+ buffer <<
156
+ escaped_name <<
157
+ " = binding.local_variable_get(:" <<
158
+ @name.name <<
159
+ ")\n"
160
+ end
161
+
162
+ def generate_initializer_coerce_property(buffer = +"")
163
+ buffer <<
164
+ escaped_name <<
165
+ "= property.coerce(" <<
166
+ escaped_name <<
167
+ ", context: self)\n"
168
+ end
169
+
170
+ def generate_initializer_assign_default(buffer = +"")
171
+ buffer <<
172
+ "if " <<
173
+ ((@kind == :&) ? "nil" : "Literal::Null") <<
174
+ " == " <<
175
+ escaped_name <<
176
+ "\n" <<
177
+ escaped_name <<
178
+ " = property.default_value\nend\n"
179
+ end
180
+
181
+ def generate_initializer_check_type(buffer = +"")
182
+ buffer <<
183
+ "unless property.type === " << escaped_name << "\n" <<
184
+ "raise Literal::TypeError.expected(" << escaped_name << ", to_be_a: property.type)\n" <<
185
+ "end\n"
186
+ end
187
+
188
+ def generate_initializer_assign_value(buffer = +"")
189
+ buffer <<
190
+ "@" <<
191
+ @name.name <<
192
+ " = " <<
193
+ escaped_name <<
194
+ "\n"
195
+ end
196
+ end
@@ -1,35 +1,9 @@
1
- class Literal::Struct
2
- extend Literal::Types
3
- include Literal::Initializer
4
-
5
- def initialize(...)
6
- @attributes = {}
7
- super
8
- end
9
-
10
- def self.__attributes__
11
- return @__attributes__ if defined?(@__attributes__)
12
- @__attributes__ = superclass.is_a?(self) ? superclass.__attributes__.dup : []
13
- end
14
-
15
- def self.attribute(name, type, writer: :private)
16
- __attributes__ << name
17
-
18
- writer_name = :"#{name}="
19
-
20
- define_method writer_name do |value|
21
- raise Literal::TypeError, "Expected #{name}: `#{value.inspect}` to be: `#{type.inspect}`." unless type === value
22
- @attributes[name] = value
23
- end
24
-
25
- define_method name do
26
- @attributes[name]
27
- end
28
-
29
- name
30
- end
31
-
32
- def to_h
33
- @attributes.dup
34
- end
1
+ # frozen_string_literal: true
2
+
3
+ class Literal::Struct < Literal::DataStructure
4
+ class << self
5
+ def prop(name, type, kind = :keyword, reader: :public, writer: :public, default: nil)
6
+ super
7
+ end
8
+ end
35
9
  end
@@ -1,5 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
1
4
  module Literal::Types::AnyType
2
- def self.===(value)
3
- true
4
- end
5
+ def self.inspect = "_Any"
6
+
7
+ def self.===(value)
8
+ !(nil === value)
9
+ end
10
+
11
+ freeze
5
12
  end
@@ -1,13 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
1
4
  class Literal::Types::ArrayType
2
- def initialize(type)
3
- @type = type
4
- end
5
+ def initialize(type)
6
+ @type = type
7
+ end
5
8
 
6
- def inspect
7
- "Array(#{@type.inspect})"
8
- end
9
+ def inspect = "_Array(#{@type.inspect})"
9
10
 
10
- def ===(value)
11
- value.is_a?(::Array) && value.all? { |item| @type === item }
12
- end
11
+ def ===(value)
12
+ Array === value && value.all? { |item| @type === item }
13
+ end
13
14
  end
@@ -1 +1,18 @@
1
- Literal::Types::BooleanType = Literal::Types::UnionType.new(true, false)
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ module Literal::Types::BooleanType
5
+ COERCION = proc { |value| !!value }
6
+
7
+ def self.inspect = "_Boolean"
8
+
9
+ def self.===(value)
10
+ true == value || false == value
11
+ end
12
+
13
+ def to_proc
14
+ COERCION
15
+ end
16
+
17
+ freeze
18
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ module Literal::Types::CallableType
5
+ def self.inspect = "_Callable"
6
+
7
+ def self.===(value)
8
+ value.respond_to?(:call)
9
+ end
10
+
11
+ freeze
12
+ end
@@ -1,13 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
1
4
  class Literal::Types::ClassType
2
- def initialize(type)
3
- @type = type
4
- end
5
+ def initialize(type)
6
+ @type = type
7
+ end
5
8
 
6
- def inspect
7
- "Class(#{@type.name})"
8
- end
9
+ def inspect = "_Class(#{@type.name})"
9
10
 
10
- def ===(value)
11
- value.is_a?(::Class) && (value == @type || value < @type)
12
- end
11
+ def ===(value)
12
+ Class === value && (value == @type || value < @type)
13
+ end
13
14
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ class Literal::Types::ConstraintType
5
+ def initialize(*object_constraints, **property_constraints)
6
+ @object_constraints = object_constraints
7
+ @property_constraints = property_constraints
8
+ end
9
+
10
+ def inspect = "_Constraint(#{@object_constraints.inspect}, #{@property_constraints.inspect})"
11
+
12
+ def ===(value)
13
+ @object_constraints.all? { |t| t === value } &&
14
+ @property_constraints.all? { |a, t| t === value.public_send(a) }
15
+ end
16
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Literal::Types::DescendantType
4
+ def initialize(type)
5
+ @type = type
6
+ end
7
+
8
+ def inspect = "_Descendant(#{@type})"
9
+
10
+ def ===(value)
11
+ Module === value && value < @type
12
+ end
13
+ end
@@ -1,13 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
1
4
  class Literal::Types::EnumerableType
2
- def initialize(type)
3
- @type = type
4
- end
5
+ def initialize(type)
6
+ @type = type
7
+ end
5
8
 
6
- def inspect
7
- "Enumerable(#{@type.inspect})"
8
- end
9
+ def inspect = "_Enumerable(#{@type.inspect})"
9
10
 
10
- def ===(value)
11
- value.is_a?(::Enumerable) && value.all? { |item| @type === item }
12
- end
11
+ def ===(value)
12
+ Enumerable === value && value.all? { |item| @type === item }
13
+ end
13
14
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ module Literal::Types::FalsyType
5
+ def self.inspect = "_Falsy"
6
+
7
+ def self.===(value)
8
+ !value
9
+ end
10
+
11
+ freeze
12
+ end
@@ -1,13 +1,10 @@
1
- class Literal::Types::FloatType
2
- def initialize(range)
3
- @range = range
4
- end
1
+ # frozen_string_literal: true
5
2
 
6
- def inspect
7
- "Float(#{@range})"
8
- end
3
+ # @api private
4
+ class Literal::Types::FloatType < Literal::Types::ConstraintType
5
+ def inspect = "_Float(#{@range})"
9
6
 
10
- def ===(other)
11
- other.is_a?(::Float) && @range === other
12
- end
7
+ def ===(value)
8
+ Float === value && super
9
+ end
13
10
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ class Literal::Types::FrozenType
5
+ def initialize(type)
6
+ @type = type
7
+ end
8
+
9
+ def inspect = "_Frozen(#{@type.inspect})"
10
+
11
+ def ===(value)
12
+ value.frozen? && @type === value
13
+ end
14
+ end
@@ -1,14 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
1
4
  class Literal::Types::HashType
2
- def initialize(key_type, value_type)
3
- @key_type = key_type
4
- @value_type = value_type
5
- end
5
+ def initialize(key_type, value_type)
6
+ @key_type = key_type
7
+ @value_type = value_type
8
+ end
6
9
 
7
- def inspect
8
- "Hash(#{@key_type.inspect}, #{@value_type.inspect})"
9
- end
10
+ def inspect = "_Hash(#{@key_type.inspect}, #{@value_type.inspect})"
10
11
 
11
- def ===(value)
12
- value.is_a?(::Hash) && value.all? { |key, value| @key_type === key && @value_type === value }
13
- end
12
+ def ===(value)
13
+ Hash === value && value.all? { |k, v| @key_type === k && @value_type === v }
14
+ end
14
15
  end
@@ -1,13 +1,10 @@
1
- class Literal::Types::IntegerType
2
- def initialize(range)
3
- @range = range
4
- end
1
+ # frozen_string_literal: true
5
2
 
6
- def inspect
7
- "Integer(#{@range})"
8
- end
3
+ # @api private
4
+ class Literal::Types::IntegerType < Literal::Types::ConstraintType
5
+ def inspect = "_Integer(#{@range})"
9
6
 
10
- def ===(other)
11
- other.is_a?(::Integer) && @range === other
12
- end
7
+ def ===(value)
8
+ Integer === value && super
9
+ end
13
10
  end
@@ -1,13 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
1
4
  class Literal::Types::InterfaceType
2
- def initialize(*methods)
3
- @methods = methods
4
- end
5
+ def initialize(*methods)
6
+ raise Literal::ArgumentError.new("_Interface type must have at least one method.") if methods.size < 1
7
+ @methods = methods
8
+ end
5
9
 
6
- def inspect
7
- "Interface(#{@methods.map(&:inspect).join(", ")})"
8
- end
10
+ def inspect = "_Interface(#{@methods.map(&:inspect).join(', ')})"
9
11
 
10
- def ===(other)
11
- @methods.all? { |method| other.respond_to?(method) }
12
- end
12
+ def ===(value)
13
+ @methods.all? { |m| value.respond_to?(m) }
14
+ end
13
15
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ class Literal::Types::IntersectionType
5
+ def initialize(*types)
6
+ raise Literal::ArgumentError.new("_Intersection type must have at least one type.") if types.size < 1
7
+
8
+ @types = types
9
+ end
10
+
11
+ def inspect = "_Intersection(#{@types.map(&:inspect).join(', ')})"
12
+
13
+ def ===(value)
14
+ @types.all? { |type| type === value }
15
+ end
16
+
17
+ def nil?
18
+ @types.all?(&:nil?)
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ module Literal::Types::JSONDataType
5
+ def self.inspect = "_JSONData"
6
+
7
+ def self.===(value)
8
+ case value
9
+ when String, Integer, Float, true, false, nil
10
+ true
11
+ when Hash
12
+ value.all? { |k, v| String === k && self === v }
13
+ when Array
14
+ value.all?(self)
15
+ else
16
+ false
17
+ end
18
+ end
19
+
20
+ freeze
21
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ module Literal::Types::LambdaType
5
+ def self.inspect = "_Lambda"
6
+
7
+ def self.===(value)
8
+ Proc === value && value.lambda?
9
+ end
10
+
11
+ freeze
12
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ class Literal::Types::MapType
5
+ def initialize(**shape)
6
+ @shape = shape
7
+ end
8
+
9
+ def inspect
10
+ "_Map(#{@shape.inspect})"
11
+ end
12
+
13
+ def ===(other)
14
+ @shape.all? { |k, t| t === other[k] }
15
+ end
16
+ end