typisch 0.1.5
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.
- data/README.md +55 -0
- data/lib/typisch/boolean.rb +13 -0
- data/lib/typisch/constructor.rb +112 -0
- data/lib/typisch/datetime.rb +41 -0
- data/lib/typisch/dsl.rb +242 -0
- data/lib/typisch/errors.rb +11 -0
- data/lib/typisch/meta.rb +57 -0
- data/lib/typisch/named_placeholder.rb +67 -0
- data/lib/typisch/null.rb +17 -0
- data/lib/typisch/numeric.rb +80 -0
- data/lib/typisch/object.rb +72 -0
- data/lib/typisch/poset_algorithms.rb +18 -0
- data/lib/typisch/registry.rb +146 -0
- data/lib/typisch/sequence.rb +107 -0
- data/lib/typisch/serialization.rb +80 -0
- data/lib/typisch/string.rb +69 -0
- data/lib/typisch/subtyping.rb +64 -0
- data/lib/typisch/tuple.rb +74 -0
- data/lib/typisch/type.rb +138 -0
- data/lib/typisch/type_checking.rb +12 -0
- data/lib/typisch/typed.rb +133 -0
- data/lib/typisch/union.rb +75 -0
- data/lib/typisch/version.rb +3 -0
- data/lib/typisch.rb +34 -0
- metadata +166 -0
data/README.md
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# Typisch
|
2
|
+
|
3
|
+
A schema language / type system / validation framework, for semi-structured data and for data in dynamic languages.
|
4
|
+
|
5
|
+
The initial implementation is in Ruby, but the aim is that it be capable of describing data equally well in a range of dynamic languages
|
6
|
+
and semi-structured data serialization languages -- Ruby, Python, Javascript, YAML, JSON, ...
|
7
|
+
|
8
|
+
*N.B.* still a work in progress at this stage, but enough works that it may be interesting to poke around!
|
9
|
+
|
10
|
+
## What distinguishes it?
|
11
|
+
|
12
|
+
It aims to be more than just an ad-hoc data validation library; rather, a proper type system with formal foundations.
|
13
|
+
|
14
|
+
This means that it's able to do more than just validate data; it can settle questions phrased in terms of the
|
15
|
+
schemas themselves -- like "is this schema a subtype of this other schema?", or "compute the intersection of
|
16
|
+
these two schemas" -- in a well-defined and sound way.
|
17
|
+
|
18
|
+
This is nice, since if you're going to bother specifying a schema for your data at all (as opposed to just
|
19
|
+
writing some validation code), you're probably doing so because you want to be able to reason statically
|
20
|
+
about the structure of the data. Having solid foundations makes that easier and more pleasant.
|
21
|
+
|
22
|
+
## As a type system, it features
|
23
|
+
|
24
|
+
- Record types with structural subtyping
|
25
|
+
- Nominal subtyping based on a hierarchy of type tags (which can be based on the subclassing graph of the host language)
|
26
|
+
- Tagged union types (arbitrary untagged unions may be computed too, but some type information may be lost where the tags overlap)
|
27
|
+
- Equi-recursive types, eg "Person {parent: Person}", which can be used to type-check cyclic object graphs if required
|
28
|
+
- Parameterised polymorphic types for Sequences, Tuples and other collection types
|
29
|
+
- A numeric tower with subtyping for the primitive numeric types
|
30
|
+
- Refinement types like "Integer greater than 0", "String of at most 10 characters", "Float from the following set of allowed values" etc
|
31
|
+
- Decidable subtyping for all the above
|
32
|
+
- Ability to compute unions and intersections of types
|
33
|
+
|
34
|
+
If that sounds surprisingly powerful, bear in mind there's one very common type system feature which it *lacks*: function types, or typing
|
35
|
+
of code. Typisch only cares about typing data, which makes its life significantly easier.
|
36
|
+
|
37
|
+
Usually type systems for data are called 'schema languages', their types 'schemas', and type-checking 'validation'.
|
38
|
+
Forgive me if I use these terms somewhat interchangeably here.
|
39
|
+
|
40
|
+
## Semi-structured data and subtyping
|
41
|
+
|
42
|
+
One way to characterise semi-structured data would be: data whose datatype admits structural subtyping.
|
43
|
+
|
44
|
+
Structural subtyping allows extra fields beyond those specifically required, to be present on an object without cause for concern
|
45
|
+
-- as may frequently be the case with "duck-typed" data in dynamic languages, and data serialised in extensible schemas in formats
|
46
|
+
like JSON.
|
47
|
+
|
48
|
+
Sometimes you only care to validate, to serialize or to process a *subset* of a large structured object graph.
|
49
|
+
Structural typing provides a rather nice way to describe these subsets, as *supertypes* of the more complete datatype.
|
50
|
+
|
51
|
+
So, a good notion of subtyping seems useful for a type system designed to cope well with semi-structured data.
|
52
|
+
|
53
|
+
## Why Typisch?
|
54
|
+
|
55
|
+
Well, it combines Type and Schema. It's also german for "typical", as in "typical, another bloody schema language".
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module Typisch
|
2
|
+
# All types except Top, Bottom and Unions (which may necessarily involve more than one constructor type) have a
|
3
|
+
# tag associated with them which is used at runtime to distinguish its instances somewhat from instances
|
4
|
+
# of other types.
|
5
|
+
#
|
6
|
+
# This is an abstract superclass; each subclass of Type::Constructor is assumed to implement its own, distinct
|
7
|
+
# lattice of types. For simple atomic types like Bool, there will only be one tag, "Bool", in that lattice.
|
8
|
+
#
|
9
|
+
# While the type lattices of different subclass of Type::Constructor are non-overlapping, within a subclass
|
10
|
+
# (such as Type::Object or Type::Numeric) there may be a non-trivial type lattice, eg for Numeric,
|
11
|
+
# Int < Float, and for Object, the type lattice is based on a nominal tag inheritance hierarchy in
|
12
|
+
# the host language together with structural subtyping rules for object properties.
|
13
|
+
#
|
14
|
+
# A list of 'reserved tags' is maintained globally, and any Type::Constructor subtype which allows custom
|
15
|
+
# user-specified tags to be used should ensure that they don't match any reserved tags.
|
16
|
+
class Type::Constructor < Type
|
17
|
+
CONSTRUCTOR_TYPE_SUBCLASSES = []
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def inherited(subclass)
|
21
|
+
# we add any non-abstract subclasses to this list, which is used to
|
22
|
+
# construct the Any type. For now Constructor::Singleton is the only
|
23
|
+
# other abstract Type::Constructor subclass:
|
24
|
+
unless subclass == Type::Constructor::Singleton
|
25
|
+
CONSTRUCTOR_TYPE_SUBCLASSES << subclass
|
26
|
+
end
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
# This should be the top in the type lattice for this class of taged types.
|
31
|
+
# Its tag should be the top_tag above.
|
32
|
+
# You are passed the overall_top, ie the top type of the overall type lattice,
|
33
|
+
# to use; this is needed by parameterised types which want to parameterise their
|
34
|
+
# top type by the overall top, eg Top = Foo | Bar | Sequence[Top] | ...
|
35
|
+
def top_type(overall_top)
|
36
|
+
raise NotImplementedError
|
37
|
+
end
|
38
|
+
|
39
|
+
# This gets called by the subtyper on a Type::Constructor subclass, with two instances of
|
40
|
+
# that subclass.
|
41
|
+
# It should return true or false; if it needs to check some subgoals,
|
42
|
+
# say on child types of the ones passed in, it should use the supplied
|
43
|
+
# 'recursively_check_subtype' block rather than calling itself recursively
|
44
|
+
# directly. This hides away the details of the corecursive subtyping algorithm
|
45
|
+
# for you.
|
46
|
+
def check_subtype(x, y, &recursively_check_subtype)
|
47
|
+
raise NotImplementedError
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# the distinct type lattice within which this type lives.
|
52
|
+
# the type system as a whole can be seen as a set of non-overlapping type lattices, together
|
53
|
+
# with tagged unions drawn from them.
|
54
|
+
#
|
55
|
+
# the interface for a type lattice is just that it responds to 'check_subtype'; by default
|
56
|
+
# the class of a type implements this interface
|
57
|
+
def type_lattice
|
58
|
+
self.class
|
59
|
+
end
|
60
|
+
|
61
|
+
def check_type(instance)
|
62
|
+
shallow_check_type(instance)
|
63
|
+
end
|
64
|
+
|
65
|
+
# the tag of this particular type
|
66
|
+
def tag
|
67
|
+
raise NotImplementedError
|
68
|
+
end
|
69
|
+
|
70
|
+
# these are here so as to implement a common interface with Type::Union
|
71
|
+
def alternative_types
|
72
|
+
[self]
|
73
|
+
end
|
74
|
+
|
75
|
+
# A class of constructor type of which there is only one type, and
|
76
|
+
# hence only one tag.
|
77
|
+
#
|
78
|
+
# Will have no supertype besides Any, and no subtype besides
|
79
|
+
# Nothing.
|
80
|
+
#
|
81
|
+
# (abstract superclass; see Boolean or Null for example subclasses).
|
82
|
+
class Singleton < Type::Constructor
|
83
|
+
class << self
|
84
|
+
private :new
|
85
|
+
|
86
|
+
def tag
|
87
|
+
raise NotImplementedError
|
88
|
+
end
|
89
|
+
|
90
|
+
def top_type(*)
|
91
|
+
@top_type ||= new
|
92
|
+
end
|
93
|
+
|
94
|
+
def check_subtype(x, y)
|
95
|
+
true
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def subexpression_types
|
100
|
+
[]
|
101
|
+
end
|
102
|
+
|
103
|
+
def tag
|
104
|
+
self.class.tag
|
105
|
+
end
|
106
|
+
|
107
|
+
def to_s(*)
|
108
|
+
@name.inspect
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module Typisch
|
5
|
+
class Type::Date < Type::Constructor::Singleton
|
6
|
+
def self.tag; "Date"; end
|
7
|
+
Registry.register_global_type(:date, top_type)
|
8
|
+
|
9
|
+
# You could add your own to the list, if you have some alternative ruby implementation
|
10
|
+
# with a Date-like interface which you want to typecheck.
|
11
|
+
VALID_IMPLEMENTATION_CLASSES = [::Date]
|
12
|
+
|
13
|
+
def shallow_check_type(instance)
|
14
|
+
case instance when *VALID_IMPLEMENTATION_CLASSES then true else false end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Type::Time < Type::Constructor::Singleton
|
19
|
+
def self.tag; "Time"; end
|
20
|
+
Registry.register_global_type(:time, top_type)
|
21
|
+
Registry.register_global_type(:datetime, top_type)
|
22
|
+
|
23
|
+
# You could add your own to the list, if you have some alternative ruby implementation
|
24
|
+
# with a Time-like interface which you want to typecheck here.
|
25
|
+
#
|
26
|
+
# Maybe allow DateTime too under ruby? its interface is slightly different though.
|
27
|
+
# Typisch types are aiming to not be overly coupled to the structure of implementation
|
28
|
+
# classes in Ruby though - eg when serializing to JSON we don't really care about
|
29
|
+
# ruby's DateTime vs Time quirks. So, not sure whether to add DateTime here and pretend
|
30
|
+
# its interface is similar enough to that of Time, or just pretend it doesn't exist.
|
31
|
+
#
|
32
|
+
# Or maybe we could just define this as 'anything which respond_to?(:to_time)' or similar.
|
33
|
+
# Although annoyingly DateTime and Time don't even have to_time / to_datetime methods to
|
34
|
+
# convert between them. Poor stdlib design :(
|
35
|
+
VALID_IMPLEMENTATION_CLASSES = [::Time]
|
36
|
+
|
37
|
+
def shallow_check_type(instance)
|
38
|
+
case instance when *VALID_IMPLEMENTATION_CLASSES then true else false end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/typisch/dsl.rb
ADDED
@@ -0,0 +1,242 @@
|
|
1
|
+
module Typisch
|
2
|
+
|
3
|
+
# Apologies this is a bit messy. Should probably do a bit of a tidy-up
|
4
|
+
# once the dust has settled around the DSL syntax.
|
5
|
+
#
|
6
|
+
# It's a layer ontop of the core type model though - could be worse, it
|
7
|
+
# could be horribly intertwined with the model itself :)
|
8
|
+
|
9
|
+
module DSL
|
10
|
+
def registry
|
11
|
+
raise NotImplementedError
|
12
|
+
end
|
13
|
+
|
14
|
+
def register(name, *type_args, &type_block_arg)
|
15
|
+
result = registry[name] = type(*type_args, &type_block_arg)
|
16
|
+
if @pending_annotations
|
17
|
+
result.annotations.merge!(@pending_annotations)
|
18
|
+
@pending_annotations = nil
|
19
|
+
end
|
20
|
+
result
|
21
|
+
end
|
22
|
+
|
23
|
+
def register_type_for_class(klass, *object_type_args, &object_type_block_arg)
|
24
|
+
name = :"#{klass}"
|
25
|
+
type = register(name, :object, klass, *object_type_args, &object_type_block_arg)
|
26
|
+
registry.register_type_for_class(klass, type)
|
27
|
+
type
|
28
|
+
end
|
29
|
+
|
30
|
+
def register_version_type_for_class(klass, version, *object_type_args, &object_type_block_arg)
|
31
|
+
name = :"#{klass}__#{version}"
|
32
|
+
type = register(name, :object, klass, *object_type_args, &object_type_block_arg)
|
33
|
+
registry.register_version_type_for_class(klass, version, type)
|
34
|
+
type
|
35
|
+
end
|
36
|
+
|
37
|
+
# annotations apply to the next register'd type.
|
38
|
+
#
|
39
|
+
# annotate "Some description", :some_other => 'annotations'
|
40
|
+
# annotate :description => "Some description", :some_other => 'annotations'
|
41
|
+
def annotate(description_or_options, options=nil)
|
42
|
+
if description_or_options.is_a?(::String)
|
43
|
+
options ||= {}; options[:description] = description_or_options
|
44
|
+
else
|
45
|
+
options = description_or_options
|
46
|
+
end
|
47
|
+
@pending_annotations ||= {}
|
48
|
+
@pending_annotations.merge!(options)
|
49
|
+
end
|
50
|
+
|
51
|
+
def type(arg, *more_args, &block_arg)
|
52
|
+
case arg
|
53
|
+
when Type
|
54
|
+
arg
|
55
|
+
when ::Symbol
|
56
|
+
if more_args.empty? && !block_arg
|
57
|
+
registry[arg]
|
58
|
+
elsif more_args.last.is_a?(::Hash) && !block_arg && (version = more_args.last[:version])
|
59
|
+
registry[:"#{arg}__#{version}"]
|
60
|
+
else
|
61
|
+
send(arg, *more_args, &block_arg)
|
62
|
+
end
|
63
|
+
when ::Module
|
64
|
+
type(:"#{arg}", *more_args, &block_arg)
|
65
|
+
else
|
66
|
+
raise ArgumentError, "expected Type or type name or class, but was given #{arg.class}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def string(refinements=nil)
|
71
|
+
refinements ? Type::String.new(refinements) : registry[:string]
|
72
|
+
end
|
73
|
+
|
74
|
+
def sequence(*args)
|
75
|
+
seq_options = {}
|
76
|
+
if (opts = args.last and opts.is_a?(::Hash))
|
77
|
+
seq_options[:slice] = opts.delete(:slice) if opts.has_key?(:slice)
|
78
|
+
seq_options[:total_length] = opts.delete(:total_length) if opts.has_key?(:total_length)
|
79
|
+
args.pop if opts.empty?
|
80
|
+
end
|
81
|
+
Type::Sequence.new(type(*args), seq_options)
|
82
|
+
end
|
83
|
+
|
84
|
+
def tuple(*types)
|
85
|
+
Type::Tuple.new(*types.map {|t| type(t)})
|
86
|
+
end
|
87
|
+
|
88
|
+
def object(*args, &block)
|
89
|
+
klass, properties = _normalize_object_args(::Object, *args)
|
90
|
+
_object(klass, properties, &block)
|
91
|
+
end
|
92
|
+
|
93
|
+
def _normalize_object_args(default_class, klass_or_properties=nil, properties=nil)
|
94
|
+
case klass_or_properties
|
95
|
+
when ::Hash then [default_class, klass_or_properties]
|
96
|
+
when ::NilClass then [default_class, {}]
|
97
|
+
when ::Module then [klass_or_properties, properties || {}]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# back-end for object, which takes args in a normalized format
|
102
|
+
def _object(klass, properties, derive_from=nil, &block)
|
103
|
+
if block
|
104
|
+
object_context = if derive_from
|
105
|
+
DerivedObjectContext.new(self, derive_from)
|
106
|
+
else
|
107
|
+
ObjectContext.new(self)
|
108
|
+
end
|
109
|
+
object_context.instance_eval(&block)
|
110
|
+
properties.merge!(object_context.properties)
|
111
|
+
end
|
112
|
+
properties.keys.each do |k|
|
113
|
+
type_args, type_block_arg = properties[k]
|
114
|
+
properties[k] = type(*type_args, &type_block_arg)
|
115
|
+
end
|
116
|
+
type = Type::Object.new(klass.to_s, properties)
|
117
|
+
if block && (prop_annot = object_context.property_annotations)
|
118
|
+
type.annotations[:properties] = prop_annot
|
119
|
+
end
|
120
|
+
type
|
121
|
+
end
|
122
|
+
|
123
|
+
def union(*types)
|
124
|
+
Type::Union.new(*types.map {|t| type(t)})
|
125
|
+
end
|
126
|
+
|
127
|
+
def nullable(t)
|
128
|
+
union(type(t), :null)
|
129
|
+
end
|
130
|
+
|
131
|
+
def derived_from(original_type, *args, &block_arg)
|
132
|
+
if args.empty? && !block_arg
|
133
|
+
original_type
|
134
|
+
elsif args.last.is_a?(::Hash) && (version = args.last[:version]) && original_type.name
|
135
|
+
# slightly messy; we rely on the convention that 'versions' of named types are registered
|
136
|
+
# as :"#{name}__#{version}", this allows you to specify or override the version when
|
137
|
+
# deriving from a type, even if it is still just a named placeholder, by manipulating the
|
138
|
+
# name.
|
139
|
+
original_name = original_type.name.to_s.sub(/__.*$/, '')
|
140
|
+
:"#{original_name}__#{version}"
|
141
|
+
else
|
142
|
+
original_type = type(original_type).target
|
143
|
+
case original_type
|
144
|
+
when Type::Object
|
145
|
+
klass, properties = _normalize_object_args(original_type.class_or_module, *args)
|
146
|
+
_object(klass, properties, original_type, &block_arg)
|
147
|
+
when Type::Sequence
|
148
|
+
slice_overrides = (args.last.is_a?(::Hash) && args.last[:slice]) ? args.pop : {}
|
149
|
+
Type::Sequence.new(derived_from(original_type.type, *args, &block_arg), slice_overrides)
|
150
|
+
when Type::Union
|
151
|
+
non_null = original_type.excluding_null
|
152
|
+
raise "DSL doesn't support deriving from union types (except simple unions with null)" if Type::Union === non_null
|
153
|
+
nullable(derived_from(non_null, *args, &block_arg))
|
154
|
+
else
|
155
|
+
raise "DSL doesn't support deriving from #{original_type.class} types" if args.length > 0 || block_arg
|
156
|
+
original_type
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
class ObjectContext
|
162
|
+
attr_reader :properties, :property_annotations
|
163
|
+
|
164
|
+
def initialize(parent_context)
|
165
|
+
@parent_context = parent_context
|
166
|
+
@properties = {}
|
167
|
+
end
|
168
|
+
|
169
|
+
# property annotations apply to the next declared property.
|
170
|
+
#
|
171
|
+
# annotate "Some description", :some_other => 'annotations'
|
172
|
+
# annotate :description => "Some description", :some_other => 'annotations'
|
173
|
+
def annotate(description_or_options, options=nil)
|
174
|
+
if description_or_options.is_a?(::String)
|
175
|
+
options ||= {}; options[:description] = description_or_options
|
176
|
+
else
|
177
|
+
options = description_or_options
|
178
|
+
end
|
179
|
+
@pending_annotations ||= {}
|
180
|
+
@pending_annotations.merge!(options)
|
181
|
+
end
|
182
|
+
|
183
|
+
def property(name, *type_args, &type_block_arg)
|
184
|
+
raise Error, "property #{name.inspect} declared twice" if @properties[name]
|
185
|
+
@properties[name] = [type_args, type_block_arg]
|
186
|
+
if @pending_annotations
|
187
|
+
@property_annotations ||= {}
|
188
|
+
@property_annotations[name] = @pending_annotations
|
189
|
+
@pending_annotations = nil
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def method_missing(name, *args, &block)
|
194
|
+
@parent_context.respond_to?(name) ? @parent_context.send(name, *args, &block) : super
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
class DerivedObjectContext < ObjectContext
|
199
|
+
def initialize(c, original_object_type)
|
200
|
+
super(c)
|
201
|
+
@original_object_type = original_object_type
|
202
|
+
end
|
203
|
+
|
204
|
+
def derive_properties(*names)
|
205
|
+
names.each {|n| derive_property(n)}
|
206
|
+
end
|
207
|
+
|
208
|
+
# use a property from the original object type being derived from
|
209
|
+
def derive_property(name, *derive_args, &derive_block)
|
210
|
+
type = @original_object_type[name] or raise "use_property: no such property #{name.inspect} on the original type"
|
211
|
+
derived_type = derived_from(type, *derive_args, &derive_block)
|
212
|
+
property name, derived_type
|
213
|
+
end
|
214
|
+
|
215
|
+
# derives all properties from the original type which haven't already been derived.
|
216
|
+
# this is done for you by Typisch::Typed::ClassMethods#register_subtype.
|
217
|
+
def derive_all_properties
|
218
|
+
@original_object_type.property_names_to_types.each do |name, type|
|
219
|
+
property(name, type) unless @properties[name]
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def derive_all_properties_except(*props)
|
224
|
+
@original_object_type.property_names_to_types.each do |name, type|
|
225
|
+
next if props.include?(name)
|
226
|
+
property(name, type) unless @properties[name]
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
class DSLContext
|
233
|
+
include DSL
|
234
|
+
|
235
|
+
attr_reader :registry
|
236
|
+
|
237
|
+
def initialize(registry)
|
238
|
+
@registry = registry
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Typisch
|
2
|
+
class Error < StandardError; end
|
3
|
+
class TypeDeclarationError < Error; end
|
4
|
+
class NameResolutionError < TypeDeclarationError
|
5
|
+
def initialize(type_name)
|
6
|
+
super("Problem resolving named placeholder type: cannot find type with name #{type_name} in registry")
|
7
|
+
end
|
8
|
+
end
|
9
|
+
class SerializationError < Error; end
|
10
|
+
class CyclicSerialization < SerializationError; end
|
11
|
+
end
|
data/lib/typisch/meta.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
module Typisch
|
2
|
+
META_TYPES = Registry.new
|
3
|
+
META_TYPES.register do
|
4
|
+
|
5
|
+
register(:"Typisch::Type", :union,
|
6
|
+
:"Typisch::Type::String",
|
7
|
+
:"Typisch::Type::Numeric",
|
8
|
+
:"Typisch::Type::Boolean",
|
9
|
+
:"Typisch::Type::Null",
|
10
|
+
:"Typisch::Type::Date",
|
11
|
+
:"Typisch::Type::Time",
|
12
|
+
:"Typisch::Type::Object",
|
13
|
+
:"Typisch::Type::Sequence",
|
14
|
+
:"Typisch::Type::Tuple",
|
15
|
+
:"Typisch::Type::Union"
|
16
|
+
)
|
17
|
+
|
18
|
+
register_type_for_class(Type::Boolean)
|
19
|
+
register_type_for_class(Type::Null)
|
20
|
+
register_type_for_class(Type::Date)
|
21
|
+
register_type_for_class(Type::Time)
|
22
|
+
|
23
|
+
register_type_for_class(Type::Numeric) do
|
24
|
+
property :tag, :string
|
25
|
+
end
|
26
|
+
|
27
|
+
register_type_for_class(Type::String) do
|
28
|
+
property :values, nullable(sequence(:string))
|
29
|
+
property :max_length, nullable(:integer)
|
30
|
+
end
|
31
|
+
|
32
|
+
register_type_for_class(::Range) do
|
33
|
+
property :begin, :integer
|
34
|
+
property :end, :integer
|
35
|
+
end
|
36
|
+
|
37
|
+
register_type_for_class(Type::Sequence) do
|
38
|
+
property :type, :"Typisch::Type"
|
39
|
+
property :slice, nullable(:Range)
|
40
|
+
property :total_length, nullable(:boolean)
|
41
|
+
end
|
42
|
+
|
43
|
+
register_type_for_class(Type::Tuple) do
|
44
|
+
property :types, sequence(:"Typisch::Type")
|
45
|
+
end
|
46
|
+
|
47
|
+
register_type_for_class(Type::Object) do
|
48
|
+
property :property_names_to_types, sequence(tuple(:string, :"Typisch::Type"))
|
49
|
+
property :tag, :string
|
50
|
+
end
|
51
|
+
|
52
|
+
register_type_for_class(Type::Union) do
|
53
|
+
property :alternative_types, sequence(:"Typisch::Type")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Typisch
|
2
|
+
# This is a proxy wrapper for a type, which we can use as a placeholder for a named
|
3
|
+
# type which hasn't yet been declared. Helps when it comes to cyclic references etc.
|
4
|
+
#
|
5
|
+
# (You can view this as a free variable, where the scope of all free variables is
|
6
|
+
# implicitly closed over at the top level by the 'registry'. We don't keep variables
|
7
|
+
# lying around as symbolic things in a syntax tree though, we're just using them as
|
8
|
+
# temporary placeholders on the way to rewriting it as a syntax *graph*).
|
9
|
+
#
|
10
|
+
# Once a register_types block has finished, the registry ensures that all references
|
11
|
+
# in the type graph to NamedPlaceholders are replaced with references to their targets.
|
12
|
+
class Type::NamedPlaceholder < Type
|
13
|
+
def initialize(name, registry)
|
14
|
+
@registry = registry
|
15
|
+
@name = name
|
16
|
+
end
|
17
|
+
|
18
|
+
def target
|
19
|
+
return @target if @target
|
20
|
+
@target = @registry[@name]
|
21
|
+
case @target when NilClass, Type::NamedPlaceholder
|
22
|
+
raise NameResolutionError.new(@name.inspect)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_writer :target
|
27
|
+
|
28
|
+
def target=(target)
|
29
|
+
@target = target.target
|
30
|
+
end
|
31
|
+
private :target=
|
32
|
+
|
33
|
+
# this is slightly naughty - we actually pretend to be of the class
|
34
|
+
# of our target object.
|
35
|
+
#
|
36
|
+
# note that TargetClass === placeholder will still return false.
|
37
|
+
|
38
|
+
def class
|
39
|
+
target.class
|
40
|
+
end
|
41
|
+
|
42
|
+
def is_a?(klass)
|
43
|
+
target.is_a?(klass)
|
44
|
+
end
|
45
|
+
alias :kind_of? :is_a?
|
46
|
+
|
47
|
+
def instance_of?(klass)
|
48
|
+
target.instance_of?(klass)
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_s(*)
|
52
|
+
@name.inspect
|
53
|
+
end
|
54
|
+
|
55
|
+
# let us proxy these through
|
56
|
+
undef :alternative_types, :check_type, :shallow_check_type, :subexpression_types
|
57
|
+
undef :excluding_null, :annotations, :canonicalize!
|
58
|
+
|
59
|
+
def method_missing(name, *args, &block)
|
60
|
+
target.respond_to?(name) ? target.send(name, *args, &block) : super
|
61
|
+
end
|
62
|
+
|
63
|
+
def respond_to?(name, include_private=false)
|
64
|
+
super || target.respond_to?(name, include_private)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/typisch/null.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Typisch
|
2
|
+
class Type::Null < Type::Constructor::Singleton
|
3
|
+
def self.tag
|
4
|
+
"Null"
|
5
|
+
end
|
6
|
+
|
7
|
+
def shallow_check_type(instance)
|
8
|
+
instance.nil?
|
9
|
+
end
|
10
|
+
|
11
|
+
def excluding_null
|
12
|
+
Type::Nothing::INSTANCE
|
13
|
+
end
|
14
|
+
|
15
|
+
Registry.register_global_type(:null, top_type)
|
16
|
+
end
|
17
|
+
end
|