Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/chanzuckerberg/sorbet-coerce
A type coercion lib works with Sorbet's static type checker and type definitions
https://github.com/chanzuckerberg/sorbet-coerce
cast coerce coercion sorbet type type-coercion
Last synced: 3 months ago
JSON representation
A type coercion lib works with Sorbet's static type checker and type definitions
- Host: GitHub
- URL: https://github.com/chanzuckerberg/sorbet-coerce
- Owner: chanzuckerberg
- License: mit
- Created: 2019-10-05T05:18:53.000Z (about 5 years ago)
- Default Branch: main
- Last Pushed: 2024-06-05T21:08:18.000Z (5 months ago)
- Last Synced: 2024-07-19T10:15:08.807Z (4 months ago)
- Topics: cast, coerce, coercion, sorbet, type, type-coercion
- Language: Ruby
- Homepage: https://rubygems.org/gems/sorbet-coerce
- Size: 299 KB
- Stars: 29
- Watchers: 11
- Forks: 12
- Open Issues: 10
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Security: SECURITY.md
Awesome Lists containing this project
README
# sorbet-coerce
[![Gem Version](https://badge.fury.io/rb/sorbet-coerce.svg)](https://badge.fury.io/rb/sorbet-coerce)
[![GitHub Action: CI](https://github.com/chanzuckerberg/sorbet-coerce/actions/workflows/ci.yml/badge.svg)](https://github.com/chanzuckerberg/sorbet-coerce/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/chanzuckerberg/sorbet-coerce/branch/master/graph/badge.svg)](https://codecov.io/gh/chanzuckerberg/sorbet-coerce)A type coercion lib works with [Sorbet](https://sorbet.org)'s static type checker and type definitions; raises an error if the coercion fails.
It provides a simple and generic way of coercing types in a sorbet-typed project. It is particularly useful when we're dealing with external API responses and controller parameters.
## Installation
1. Follow the steps [here](https://sorbet.org/docs/adopting) to set up the latest version of Sorbet and run `srb tc`.
2. Add `sorbet-coerce` to your Gemfile and install them with `Bundler`.
```ruby
# -- Gemfile --gem 'sorbet-coerce'
``````sh
❯ bundle install
```## Usage
`TypeCoerce` takes a valid sorbet type and coerce the input value into that type. It'll return a statically-typed object or throws errors when the coercion process cannot be handled as expected (more details in the [Errors](#errors) section).
```ruby
converted = TypeCoerce[].new.from()T.reveal_type(converted) #
```### Supported Types
- Simple Types
- Custom Types: If the values can be coerced by `.new`
- `T.untyped` (an escape hatch to ignore & return the given value)
- `T::Boolean`
- `T::Enum`
- `T.nilable()`
- `T::Array[]`
- `T::Hash[, ]`
- `T::Set[]`
- `T.any(, ...)`
- Subclasses of `T::Struct`We don't support
- Experimental features (tuples and shapes)
- passing in variables *as* types (ex `TypeCoerce[var]`) - please use at your own risk!
- for more information, see [this Slack thread in the Sorbet Slack](https://sorbet-ruby.slack.com/archives/CHN2L03NH/p1658784723127889)### Examples
- Simple Types```ruby
TypeCoerce[T::Boolean].new.from('false')
# => falseTypeCoerce[T::Boolean].new.from('true')
# => trueTypeCoerce[Date].new.from('2019-08-05')
# => #TypeCoerce[DateTime].new.from('2019-08-05')
# => #TypeCoerce[Float].new.from('1')
# => 1.0TypeCoerce[Integer].new.from('1')
# => 1TypeCoerce[String].new.from(1)
# => "1"TypeCoerce[Symbol].new.from('a')
# => :aTypeCoerce[Time].new.from('2019-08-05')
# => 2019-08-05 00:00:00 -0700
```- `T.nilable`
```ruby
TypeCoerce[T.nilable(Integer)].new.from('')
# => nil
TypeCoerce[T.nilable(Integer)].new.from(nil)
# => nil
TypeCoerce[T.nilable(Integer)].new.from('')
# => nil
```The behaviour for converting `''` for the `T.nilable(String)` type depends on an option flag called `coerce_empty_to_nil` (new in [v0.6.0](https://github.com/chanzuckerberg/sorbet-coerce/releases/tag/v0.6.0)):
```ruby
# default behaviour
TypeCoerce[T.nilable(String)].new.from('')
# => ""# using the coerce_empty_to_nil flag
TypeCoerce[T.nilable(String)].new.from('', coerce_empty_to_nil: true)
# => nil
```- `T::Array`
```ruby
TypeCoerce[T::Array[Integer]].new.from([1.0, '2.0'])
# => [1, 2]
```- `T::Struct`
```ruby
class Params < T::Struct
const :id, Integer
const :role, String, default: 'wizard'
endTypeCoerce[Params].new.from({id: '1'})
# =>
```
More examples: [nested params](https://github.com/chanzuckerberg/sorbet-coerce/blob/ffe1bed4de11ca832d9f76a157349ea03bbf29a1/spec/nested_spec.rb#L18-L26)## Errors
We will get `CoercionError`, `ShapeError`, or `TypeError` when the coercion doesn't work successfully.#### `TypeCoerce::CoercionError` (configurable)
It raises a coercion error when it fails to convert a value into the specified type (i.e. `'bad string args' to Integer`). This can be configured globally or at each call-site. When configured to `true`, it will fill the result with `nil` instead of raising the errors.
```ruby
TypeCoerce::Configuration.raise_coercion_error = false # default to true
```
We can use an inline flag to overwrite the global configuration:
```ruby
TypeCoerce[T.nilable(Integer)].new.from('abc', raise_coercion_error: false)
# => nil
```#### `TypeCoerce::ShapeError` (NOT configurable)
It raises a shape error when the shape of the input does not match the shape of input type (i.e. `'1' to T::Array[Integer]` or to `T::Struct`). This cannot be configured and always raise an error.#### `TypeError` (configurable)
It raises a type error when the coerced input does not match the input type. This error is raised by Sorbet and can be configured through [`T::Configuration`](https://sorbet.org/docs/tconfiguration).#### Soft Errors vs. Hard Errors
In an environment where type errors and coercion errors are configured to be silent (referred to as soft errors), when the coercion fails, `TypeCoerce` will fill the result with `nil` instead of actually raising the errors (referred to hard errors).With hard errors,
```ruby
class Params < T::Struct
const :a, Integer
endTypeCoerce[Integer].new.from(nil)
# => TypeError Exception: T.let: Expected type Integer, got type NilClassTypeCoerce[Integer].new.from('abc')
# => TypeCoerce::CoercionError Exception: Could not coerce value ("abc") of type (String) to desired type (Integer)TypeCoerce[T.nilable(Integer)].new.from('abc', raise_coercion_error: false)
# => nilTypeCoerce[Params].new.from({a: 'abc'}, raise_coercion_error: false)
# => TypeError Exception: Parameter 'a': Can't set Params.a to nil (instance of NilClass) - need a Integer
```With soft errors,
```ruby
TypeCoerce[Integer].new.from('abc', raise_coercion_error: false)
# => nilTypeCoerce[Params].new.from({a: 'abc'}, raise_coercion_error: false) # require sorbet version ~> 0.4.4948
# =>TypeCoerce[Params].new.from({a: 'abc'}, raise_coercion_error: true)
# TypeCoerce::CoercionError Exception: Could not coerce value ("abc") of type (String) to desired type (Integer)
```## `null`, `''`, and `undefined`
Sorbet-coerce is designed in the context of web development. When coercing into a `T::Struct`, the values that need to be coerced are often JSON-like. Suppose we send a JavaScript object
```javascript
json_js = {"a": "1", "null_field": null, "blank_field": "", "missing_key": undefined} // javascript
```
to the server side and get a JSON hash
```ruby
json_rb = {"a" => "1", "null_field" => nil, "blank_field" => ""} # ruby, note `missing_key` is removed from the hash
```
We expect the object to have shape
```ruby
class Params < T::Struct
const :a, Integer
const :null_field, T.nilable(Integer)
const :blank_field, T.nilable(Integer)
const :missing_key, T::Array[Integer], default: []
end
```Then we coerce the object `json_rb` into an instance of `Params`.
```ruby
params = TypeCoerce[Params].new.from(json_rb)
# =>
```
- When `json_js["null_field"]` is `null`, `params.null_field` is `nil`
- When `json_js["blank_field"]` is `""`, `params.blank_field` is `nil`
- When `json_js["missing_key"]` is `undefined`, `params.missing_key` will use the default value `[]`## Contributing
Contributions and ideas are welcome! Please see [our contributing guide](CONTRIBUTING.md) and don't hesitate to open an issue or send a pull request to improve the functionality of this gem.
This project adheres to the Contributor Covenant [code of conduct](https://github.com/chanzuckerberg/.github/tree/master/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [email protected].
## License
This project is licensed under [MIT](https://github.com/chanzuckerberg/sorbet-coerce/blob/master/LICENSE).