crystalruby 0.1.1 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: db2722469a6d98fb5696b55dd0980337db19ce92fde42b4c1c023cfa6fd877d0
4
- data.tar.gz: a2914be29451a124cca3dcdd83173e8f4713c5e1dbd1121c1c4179e6e8a55ea6
3
+ metadata.gz: 157d03e574c1ef5e06073e71c29e259741c26373113f2ae51c692b4aaef429f9
4
+ data.tar.gz: 94c1f670b52a39b78c961a3050c2587c540a1cb1593c30e87d6bd05d90a4d736
5
5
  SHA512:
6
- metadata.gz: 9253f9b6b953021570fb31ee958e4ca2c798d3e586d5be8da59d5f26b895f1bce22672bbb1c0c14322732d61d9c4fe780d6833d9410d9e16732683862fd3330f
7
- data.tar.gz: d14edd179ed85907f90038b303f409a6c3868b2cf2bbfd60695350886d67ee5bf9d98b2ee3e1d05421e3d55b9b3ce155f7d9b0d04d2c6bee0165f36e1598abc7
6
+ metadata.gz: 1b7972639977a290fd9a42b737edd1f3eec28a240a6e0e773619190bf646ec0707520d317ddf6e6152f507753b16fa66db0d2903c18fd23e58ebce4422e02555
7
+ data.tar.gz: bc3d03de05c21a14a6d6d9b6de76ff04efcd660999c0dd2c166cc306707d0f93be01053e34ba8a531c9e0a9154e887bb1c6796b141564e290e25d537eeab88b4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.3] - 2024-04-10
4
+
5
+ - Support exceptions thrown in Crystal being caught in Ruby
6
+ - Support complex Ruby type passing (while preserving type checking), using `JSON` as serialization format.
7
+
3
8
  ## [0.1.0] - 2024-04-07
4
9
 
5
10
  - Initial release
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # CrystalRuby
1
+ # crystalruby
2
2
 
3
- Crystal Ruby is a gem that allows you to write Crystal code, inlined in Ruby. All you need is a modern crystal compiler installed on your system.
3
+ `crystalruby` is a gem that allows you to write Crystal code, inlined in Ruby. All you need is a modern crystal compiler installed on your system.
4
4
 
5
5
  You can then turn simple methods into Crystal methods as easily as demonstrated below:
6
6
 
@@ -120,8 +120,10 @@ end
120
120
 
121
121
  ## Types
122
122
 
123
- Currently only primitive types are supported. Structures are a WIP.
124
- To see the list of currently supported type mappings of FFI types to crystal types, you can check: `CrystalRuby::Typemaps::CRYSTAL_TYPE_MAP`
123
+ Currently primitive types are supported.
124
+ Composite types are supported using JSON serialization.
125
+ C-Structures are a WIP.
126
+ To see the list of currently supported primitive type mappings of FFI types to crystal types, you can check: `CrystalRuby::Typemaps::CRYSTAL_TYPE_MAP`
125
127
  E.g.
126
128
 
127
129
  ```ruby
@@ -151,7 +153,54 @@ CrystalRuby::Typemaps::CRYSTAL_TYPE_MAP
151
153
  :string=>"String"}
152
154
  ```
153
155
 
154
- ### Installing shards and writing non-embedded Crystal code
156
+ ## Composite Types (using JSON serialization)
157
+
158
+ The library allows you to pass complex nested structures using JSON as a serialization format.
159
+ The type signatures for composite types can use ordinary Crystal Type syntax.
160
+ Type conversion is applied automatically.
161
+
162
+ E.g.
163
+
164
+ ```ruby
165
+ crystalize [a: json{ Int64 | Float64 | Nil }, b: json{ String | Array(Bool) } ] => :void
166
+ def complex_argument_types
167
+ puts "Got #{a} and #{b}"
168
+ end
169
+
170
+ crystalize [] => json{ Int32 | String | Hash(String, Array(NamedTuple(hello: Int32)) | Time)}
171
+ def complex_return_type
172
+ return {
173
+ "hello" => [
174
+ {
175
+ hello: 1,
176
+ },
177
+ ],
178
+ "world" => Time.utc
179
+ }
180
+ end
181
+ ```
182
+
183
+ Type signatures validations are applied to both arguments and return types.
184
+
185
+ ```ruby
186
+ [1] pry(main)> Foo.complex_argument_types(nil, "test")
187
+ Got and test
188
+ => nil
189
+
190
+ [2] pry(main)> Foo.complex_argument_types(88, [true, false, true])
191
+ Got 88 and [true, false, true]
192
+ => nil
193
+
194
+ [3] pry(main)> Foo.complex_argument_types(88, [true, false, 88])
195
+ ArgumentError: Expected Bool but was Int at line 1, column 15
196
+ from crystalruby.rb:303:in `block in compile!'
197
+ ```
198
+
199
+ ## Exceptions
200
+
201
+ Exceptions thrown in Crystal code can be caught in Ruby.
202
+
203
+ ## Installing shards and writing non-embedded Crystal code
155
204
 
156
205
  You can use any Crystal shards and write ordinary, stand-alone Crystal code.
157
206
 
@@ -169,6 +218,60 @@ Remember to require these installed shards after installing them. E.g. inside `.
169
218
 
170
219
  You can edit the default paths for crystal source and library files from within the `./crystalruby.yaml` config file.
171
220
 
221
+ ### Wrapping Crystal code in Ruby
222
+
223
+ Sometimes you may want to wrap a Crystal method in Ruby, so that you can use Ruby before the Crystal code to prepare arguments, or after the Crystal code, to apply transformations to the result. A real-life example of this might be an ActionController method, where you might want to use Ruby to parse the request, perform auth etc., and then use Crystal to perform some heavy computation, before returning the result from Ruby.
224
+ To do this, you simply pass a block to the `crystalize` method, which will serve as the Ruby entry point to the function. From within this block, you can invoke `super` to call the Crystal method, and then apply any Ruby transformations to the result.
225
+
226
+ ```ruby
227
+ module MyModule
228
+ crystalize [a: :int32, b: :int32] => :int32 do |a, b|
229
+ # In this example, we perform automated conversion to integers inside Ruby.
230
+ # Then add 1 to the result of the Crystal method.
231
+ result = super(a.to_i, b.to_i)
232
+ result + 1
233
+ end
234
+ def add(a, b)
235
+ a + b
236
+ end
237
+ end
238
+
239
+ MyModule.add("1", "2")
240
+ ```
241
+
242
+ ### Release Builds
243
+
244
+ You can control whether CrystalRuby builds in debug or release mode by setting following config option
245
+
246
+ ```ruby
247
+ CrystalRuby.configure do |config|
248
+ config.debug = false
249
+ end
250
+ ```
251
+
252
+ By default, Crystal code is only JIT compiled. In production, you likely want to compile the Crystal code ahead of time. To do this, you can create a dedicated file which
253
+
254
+ - Preloads all files Ruby code with embedded crystal
255
+ - Forces compilation.
256
+
257
+ E.g.
258
+
259
+ ```ruby
260
+ # E.g. crystalruby_build.rb
261
+ require "crystalruby"
262
+
263
+ CrystalRuby.configure do |config|
264
+ config.debug = false
265
+ end
266
+
267
+ require_relative "foo"
268
+ require_relative "bar"
269
+
270
+ CrystalRuby.compile!
271
+ ```
272
+
273
+ Then you can run this file as part of your build step, to ensure all Crystal code is compiled ahead of time.
274
+
172
275
  ### Troubleshooting
173
276
 
174
277
  The logic to detect when to JIT recompile is not robust and can end up in an inconsistent state. To remedy this it is useful to clear out all generated assets and build from scratch.
@@ -179,6 +282,26 @@ To do this execute:
179
282
  bundle exec crystalruby clean
180
283
  ```
181
284
 
285
+ ## Design Goals
286
+
287
+ `crystalruby`'s primary purpose is provide ergonomic access to Crystal from Ruby, over FFI.
288
+ For simple usage, advanced knowledge of Crystal should not be required.
289
+
290
+ However, the abstraction it provides should remain simple, transparent, and easy to hack on and it should not preclude users from supplementing its capabilities with a more direct integration using ffi primtives.
291
+
292
+ It should support escape hatches to allow it to coexist with code that performs a more direct [FFI](https://github.com/ffi/ffi) integration to implement advanced functionality not supported by `crystalruby`.
293
+
294
+ The library is currently in its infancy. Planned additions are:
295
+
296
+ - Replace existing checksum process, with one that combines results of inline and external crystal to more accurately detect when recompilation is necessary.
297
+ - Support for automatic serialization of nested data structures (holding _ONLY_ primitives), using JSON as our serialization protocol (prioritizing portability over raw serialization performance. JSON generation and parsing is bundled into the stdlib in both languages).
298
+ - Simple mixin/concern that utilises `FFI::Struct` for bi-directional passing of Ruby objects and Crystal objects (by value).
299
+ - Install command to generate a sample build script, and supports build command (which simply verifies then invokes this script)
300
+ - Call Ruby from Crystal using FFI callbacks (implement `.expose_to_crystal`)
301
+ - Support long-lived synchronized objects (through use of synchronized memory arena to prevent GC).
302
+ - Support for passing `crystalruby` types by reference (need to contend with GC).
303
+ - Explore mechanisms to safely expose true parallelism using [FFI over Ractors](https://github.com/ffi/ffi/wiki/Ractors)
304
+
182
305
  ## Installation
183
306
 
184
307
  To get started, add this line to your application's Gemfile:
@@ -199,7 +322,7 @@ Or install it yourself as:
199
322
  $ gem install crystalruby
200
323
  ```
201
324
 
202
- Crystal Ruby requires some basic initialization options inside a crystalruby.yaml file in the root of your project.
325
+ `crystalruby` requires some basic initialization options inside a crystalruby.yaml file in the root of your project.
203
326
  You can run `crystalruby init` to generate a configuration file with sane defaults.
204
327
 
205
328
  ```bash
@@ -221,7 +344,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
221
344
 
222
345
  ## Contributing
223
346
 
224
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/crystalruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/crystalruby/blob/master/CODE_OF_CONDUCT.md).
347
+ Bug reports and pull requests are welcome on GitHub at https://github.com/wouterken/crystalruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/wouterken/crystalruby/blob/master/CODE_OF_CONDUCT.md).
225
348
 
226
349
  ## License
227
350
 
@@ -229,4 +352,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
229
352
 
230
353
  ## Code of Conduct
231
354
 
232
- Everyone interacting in the Crystalruby project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/crystalruby/blob/master/CODE_OF_CONDUCT.md).
355
+ Everyone interacting in the `crystalruby` project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/wouterken/crystalruby/blob/master/CODE_OF_CONDUCT.md).
data/crystalruby.gemspec CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
12
12
  spec.description = "Embed Crystal code directly in Ruby."
13
13
  spec.homepage = "https://github.com/wouterken/crystalruby"
14
14
  spec.license = "MIT"
15
- spec.required_ruby_version = ">= 3.0.0"
15
+ spec.required_ruby_version = ">= 2.7.2"
16
16
 
17
17
  spec.metadata["homepage_uri"] = spec.homepage
18
18
  spec.metadata["source_code_uri"] = spec.homepage
@@ -32,10 +32,10 @@ Gem::Specification.new do |spec|
32
32
 
33
33
  # Uncomment to register a new dependency of your gem
34
34
  # spec.add_dependency "example-gem", "~> 1.0"
35
- spec.add_dependency 'ffi'
36
- spec.add_dependency 'digest'
37
- spec.add_dependency 'fileutils'
38
- spec.add_dependency 'method_source'
35
+ spec.add_dependency "digest"
36
+ spec.add_dependency "ffi"
37
+ spec.add_dependency "fileutils"
38
+ spec.add_dependency "method_source"
39
39
  # For more information and examples about making a new gem, check out our
40
40
  # guide at: https://bundler.io/guides/creating_gem.html
41
41
  end
data/exe/crystalruby CHANGED
@@ -13,6 +13,7 @@ def init
13
13
  crystal_lib_dir: "./crystalruby/lib"
14
14
  crystal_main_file: "main.cr"
15
15
  crystal_lib_name: "crlib"
16
+ crystal_codegen_dir: "generated"
16
17
  YAML
17
18
 
18
19
  # Create the file at the root of the current directory
@@ -1,5 +1,5 @@
1
- require 'singleton'
2
- require 'yaml'
1
+ require "singleton"
2
+ require "yaml"
3
3
 
4
4
  module CrystalRuby
5
5
  def self.config
@@ -9,15 +9,20 @@ module CrystalRuby
9
9
  # Define a nested Config class
10
10
  class Config
11
11
  include Singleton
12
- attr_accessor :debug, :crystal_src_dir, :crystal_lib_dir, :crystal_main_file, :crystal_lib_name
12
+ attr_accessor :debug, :crystal_src_dir, :crystal_lib_dir, :crystal_main_file, :crystal_lib_name, :crystal_codegen_dir
13
13
 
14
14
  def initialize
15
- # Set default configuration options
16
15
  @debug = true
17
- if File.exist?("crystalruby.yaml")
18
- @crystal_src_dir, @crystal_lib_dir, @crystal_main_file, @crystal_lib_name =
19
- YAML.safe_load_file("crystalruby.yaml").values_at("crystal_src_dir","crystal_lib_dir","crystal_main_file", "crystal_lib_name")
16
+ config = if File.exist?("crystalruby.yaml")
17
+ YAML.safe_load(IO.read("crystalruby.yaml")) rescue {}
18
+ else
19
+ {}
20
20
  end
21
+ @crystal_src_dir = config.fetch("crystal_src_dir", "./crystalruby/src")
22
+ @crystal_lib_dir = config.fetch("crystal_lib_dir", "./crystalruby/lib")
23
+ @crystal_main_file = config.fetch("crystal_main_file", "main.cr")
24
+ @crystal_lib_name = config.fetch("crystal_lib_name", "crlib")
25
+ @crystal_codegen_dir = config.fetch("crystal_codegen_dir", "generated")
21
26
  end
22
27
  end
23
28
 
@@ -0,0 +1,13 @@
1
+ require 'pry-byebug'
2
+ module CrystalRuby
3
+ module Template
4
+ Dir[File.join(File.dirname(__FILE__), "templates", "*.cr")].each do |file|
5
+ template_name = File.basename(file, File.extname(file)).capitalize
6
+ const_set(template_name, File.read(file))
7
+ end
8
+
9
+ def self.render(template, context)
10
+ template % context
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ module %{module_name}
2
+ def self.%{fn_name}(%{fn_args}) : %{fn_ret_type}
3
+ %{fn_body}
4
+ end
5
+ end
6
+
7
+ fun %{lib_fn_name}(%{lib_fn_args}): %{lib_fn_ret_type}
8
+ begin
9
+ %{convert_lib_args}
10
+ begin
11
+ return_value = %{module_name}.%{fn_name}(%{arg_names})
12
+ return %{convert_return_type}
13
+ rescue ex
14
+ CrystalRuby.report_error("RuntimeError", ex.message.to_s)
15
+ end
16
+ rescue ex
17
+ CrystalRuby.report_error("ArgumentError", ex.message.to_s)
18
+ end
19
+ return %{error_value}
20
+ end
@@ -0,0 +1,38 @@
1
+ FAKE_ARG = "crystal"
2
+ alias Callback = (Pointer(UInt8), Pointer(UInt8) -> Void)
3
+
4
+ module CrystalRuby
5
+ @@initialized = false
6
+ def self.init
7
+ if @@initialized
8
+ return
9
+ end
10
+ @@initialized = true
11
+ GC.init
12
+ ptr = FAKE_ARG.to_unsafe
13
+ LibCrystalMain.__crystal_main(1, pointerof(ptr))
14
+ end
15
+
16
+ def self.attach_rb_error_handler(cb : Callback)
17
+ @@rb_error_handler = cb
18
+ end
19
+
20
+ def self.report_error(error_type : String, str : String)
21
+ handler = @@rb_error_handler
22
+ if handler
23
+ handler.call(error_type.to_unsafe, str.to_unsafe)
24
+ end
25
+ end
26
+ end
27
+
28
+
29
+ fun init(): Void
30
+ CrystalRuby.init
31
+ end
32
+
33
+ fun attach_rb_error_handler(cb : Callback) : Void
34
+ CrystalRuby.attach_rb_error_handler(cb)
35
+ end
36
+
37
+ %{type_modules}
38
+ %{requires}
@@ -0,0 +1,179 @@
1
+ require_relative "types"
2
+
3
+ module CrystalRuby
4
+ module TypeBuilder
5
+ module_function
6
+
7
+ def with_injected_type_dsl(context, &block)
8
+ with_constants(context) do
9
+ with_methods(context, &block)
10
+ end
11
+ end
12
+
13
+ def with_methods(context)
14
+ restores = []
15
+ %i[Array Hash NamedTuple Tuple].each do |method_name|
16
+ old_method = begin
17
+ context.instance_method(method_name)
18
+ rescue StandardError
19
+ nil
20
+ end
21
+ restores << [context, method_name, old_method]
22
+ context.define_singleton_method(method_name) do |*args|
23
+ Types.send(method_name, *args)
24
+ end
25
+ end
26
+ yield
27
+ ensure
28
+ restores.each do |context, method_name, old_method|
29
+ context.define_singleton_method(method_name, old_method) if old_method
30
+ end
31
+ end
32
+
33
+ def with_constants(context)
34
+ previous_const_pairs = CrystalRuby::Types.constants.map do |type|
35
+ [type, begin
36
+ context.const_get(type)
37
+ rescue StandardError
38
+ nil
39
+ end]
40
+ end
41
+ CrystalRuby::Types.constants.each do |type|
42
+ begin
43
+ context.send(:remove_const, type)
44
+ rescue StandardError
45
+ nil
46
+ end
47
+ context.const_set(type, CrystalRuby::Types.const_get(type))
48
+ end
49
+ yield
50
+ ensure
51
+ previous_const_pairs.each do |const_name, const_value|
52
+ begin
53
+ context.send(:remove_const, const_name)
54
+ rescue StandardError
55
+ nil
56
+ end
57
+ context.const_set(const_name, const_value)
58
+ end
59
+ end
60
+
61
+ def build
62
+ result = yield
63
+ Types::Type.validate!(result)
64
+ Types::Typedef(result)
65
+ end
66
+ end
67
+ end
68
+
69
+ # class UnionType
70
+ # attr_reader :inner
71
+
72
+ # def initialize(*inner)
73
+ # @inner = inner
74
+ # end
75
+
76
+ # def |(other)
77
+ # UnionType.new(*inner, *other.inner)
78
+ # end
79
+
80
+ # def inspect
81
+ # elements = inner.map(&:inspect).join(" | ")
82
+ # end
83
+ # end
84
+
85
+ # class Type
86
+ # attr_reader :inner, :contains
87
+
88
+ # def initialize(name)
89
+ # @name = name
90
+ # @contains = contains
91
+ # @inner = [self]
92
+ # end
93
+
94
+ # def |(other)
95
+ # UnionType.new(*inner, *other.inner)
96
+ # end
97
+
98
+ # def inspect
99
+ # if @contains
100
+ # "#{@name}(#{@contains.inspect})"
101
+ # else
102
+ # @name
103
+ # end
104
+ # end
105
+ # end
106
+
107
+ # module_function
108
+
109
+ # %w[
110
+ # Bool Uint8 Uint16 Uint32 Uint64 Int8 Int16 Int32 Int64 Float32 Float64 String Time Symbol
111
+ # Null
112
+ # ].map do |t|
113
+ # cls = Class.new(Type)
114
+ # const_set(t, cls)
115
+ # define_method(t.downcase) do
116
+ # cls.new(t)
117
+ # end
118
+ # end
119
+
120
+ # def build(&blk)
121
+ # instance_exec(&blk)
122
+ # end
123
+
124
+ # def hash(key_type, value_type)
125
+ # Hash.new(key_type, value_type)
126
+ # end
127
+
128
+ # def array(type)
129
+ # Array.new(type)
130
+ # end
131
+
132
+ # def tuple(*types)
133
+ # Tuple.new(*types)
134
+ # end
135
+
136
+ # def named_tuple(type_hash)
137
+ # NamedTuple.new(type_hash)
138
+ # end
139
+
140
+ # def NamedTuple(type_hash)
141
+ # NamedTuple.new(type_hash)
142
+ # end
143
+
144
+ # class Hash < Type
145
+ # HASH_KEY_TYPES = %w[String Symbol].freeze
146
+ # def initialize(key_type, value_type)
147
+ # super("Hash")
148
+ # @key_type = key_type
149
+ # @value_type = value_type
150
+ # raise "Invalid key type" unless [Uint8, Uint16, Uint32, Uint64, Int8, Int16, Int32, Int64,
151
+ # String].include?(key_type)
152
+ # raise "Invalid value type" unless value_type.is_a?(Type)
153
+ # end
154
+ # end
155
+
156
+ # class Array < Type
157
+ # def initialize(value_type)
158
+ # super("Array")
159
+ # @value_type = value_type
160
+ # raise "Invalid value type" unless value_type.is_a?(Type)
161
+ # end
162
+ # end
163
+
164
+ # class NamedTuple < Type
165
+ # def initialize(types_hash)
166
+ # raise "keys must be symbols" unless types_hash.keys.all? { |k| k.is_a?(Symbol) }
167
+ # raise "Invalid value type" unless types_hash.values.all? { |v| v.is_a?(Type) }
168
+
169
+ # super("NamedTuple")
170
+ # @types_hash = types_hash
171
+ # end
172
+ # end
173
+
174
+ # class Tuple < Type
175
+ # def initialize(*value_types)
176
+ # super("Tuple")
177
+ # raise "Invalid value type" unless value_types.all? { |v| v.is_a?(Type) }
178
+ # end
179
+ # end
@@ -26,6 +26,32 @@ module CrystalRuby
26
26
  :string => "String" # String type
27
27
  }
28
28
 
29
+ ERROR_VALUE = {
30
+ :char => "0", # In Crystal, :char is typically represented as Int8
31
+ :uchar => "0", # Unsigned char
32
+ :int8 => "0", # Same as :char
33
+ :uint8 => "0", # Same as :uchar
34
+ :short => "0", # Short integer
35
+ :ushort => "0", # Unsigned short integer
36
+ :int16 => "0", # Same as :short
37
+ :uint16 => "0", # Same as :ushort
38
+ :int => "0", # Integer, Crystal defaults to 32 bits
39
+ :uint => "0", # Unsigned integer
40
+ :int32 => "0", # 32-bit integer
41
+ :uint32 => "0", # 32-bit unsigned integer
42
+ :long => "0", # Long integer, size depends on the platform (32 or 64 bits)
43
+ :ulong => "0", # Unsigned long integer, size depends on the platform
44
+ :int64 => "0", # 64-bit integer
45
+ :uint64 => "0", # 64-bit unsigned integer
46
+ :long_long => "0", # Same as :int64
47
+ :ulong_long => "0", # Same as :uint64
48
+ :float => "0.0", # Floating point number (single precision)
49
+ :double => "0.0", # Double precision floating point number
50
+ :bool => "false", # Boolean type
51
+ :void => "Void", # Void type
52
+ :string => '""' # String type
53
+ }
54
+
29
55
  C_TYPE_MAP = CRYSTAL_TYPE_MAP.merge({
30
56
  :string => "UInt8*"
31
57
  })
@@ -0,0 +1,14 @@
1
+ module CrystalRuby::Types
2
+ Array = Type.new(
3
+ :Array,
4
+ error: "Array type must have a type parameter. E.g. Array(Float64)"
5
+ )
6
+
7
+ def self.Array(type)
8
+ Type.validate!(type)
9
+ Type.new("Array", inner_types: [type], accept_if: [::Array]
10
+ ) do |a|
11
+ a.map!{|v| type.interpret!(v) }
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ module CrystalRuby::Types
2
+ Bool = Type.new(:Bool, accept_if: [::TrueClass, ::FalseClass])
3
+ end
@@ -0,0 +1,15 @@
1
+ module CrystalRuby::Types
2
+ Hash = Type.new(
3
+ :Hash,
4
+ error: "Hash type must have 2 type parameters. E.g. Hash(Float64, String)",
5
+ )
6
+
7
+ def self.Hash(key_type, value_type)
8
+ Type.validate!(key_type)
9
+ Type.validate!(value_type)
10
+ Type.new("Hash", inner_types: [key_type, value_type], accept_if: [::Hash]) do |h|
11
+ h.transform_keys!{|k| key_type.interpret!(k) }
12
+ h.transform_values!{|v| value_type.interpret!(v) }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ module CrystalRuby::Types
2
+ NamedTuple = Type.new(
3
+ :NamedTuple,
4
+ error: "NamedTuple type must contain one or more symbol -> type pairs. E.g. NamedTuple(hello: Int32, world: String)"
5
+ )
6
+
7
+ def self.NamedTuple(types_hash)
8
+ types_hash.keys.each do |key|
9
+ raise "NamedTuple keys must be symbols" unless key.kind_of?(::Symbol) || key.respond_to?(:to_sym)
10
+ end
11
+ types_hash.values.each do |value_type|
12
+ Type.validate!(value_type)
13
+ end
14
+ keys = types_hash.keys.map(&:to_sym)
15
+ values = types_hash.values
16
+ Type.new("NamedTuple", inner_types: values, inner_keys: keys, accept_if: [::Hash]) do |h|
17
+ h.transform_keys!{|k| k.to_sym }
18
+ raise "Invalid keys for named tuple" unless h.keys.length == keys.length
19
+ raise "Invalid keys for named tuple" unless h.keys.all?{|k| keys.include?(k)}
20
+ h.each do |key, value|
21
+ h[key] = values[keys.index(key)].interpret!(value)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,3 @@
1
+ module CrystalRuby::Types
2
+ Nil = Type.new(:Nil, accept_if: [::NilClass])
3
+ end
@@ -0,0 +1,5 @@
1
+ module CrystalRuby::Types
2
+ %i[Uint8 Uint16 Uint32 Uint64 Int8 Int16 Int32 Int64 Float32 Float64].each do |type_name|
3
+ const_set type_name, Type.new(type_name, accept_if: [::Numeric])
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module CrystalRuby::Types
2
+ String = Type.new(:String, accept_if: [::String])
3
+ end
@@ -0,0 +1,3 @@
1
+ module CrystalRuby::Types
2
+ Symbol = Type.new(:Symbol, accept_if: [::String, ::Symbol])
3
+ end
@@ -0,0 +1,6 @@
1
+ module CrystalRuby::Types
2
+ require 'date'
3
+ Time = Type.new(:Time, accept_if: [::Time, ::String]) do |v|
4
+ DateTime.parse(v)
5
+ end
6
+ end
@@ -0,0 +1,15 @@
1
+ module CrystalRuby::Types
2
+ Tuple = Type.new(
3
+ :Tuple,
4
+ error: "Tuple type must contain one or more types E.g. Tuple(Int32, String)"
5
+ )
6
+
7
+ def self.Tuple(*types)
8
+ types.each do |value_type|
9
+ Type.validate!(value_type)
10
+ end
11
+ Type.new("Tuple", inner_types: types, accept_if: [::Array]) do |a|
12
+ a.map!.with_index{|v, i| self.inner_types[i].interpret!(v) }
13
+ end
14
+ end
15
+ end