Skip to content

Commit

Permalink
v0.2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Verseth committed Jun 11, 2024
1 parent 241f766 commit 389f7a2
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 41 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## [0.2.0] - 2024-06-11

- Add support for `return_type` and `setter_type` in custom primitive shale types
- Add a more thorough description of sorbet and tapioca support in the README

## [0.1.9] - 2024-06-03

- Fix the signature of `new` class method
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
shale-builder (0.1.9)
shale-builder (0.2.0)
booleans (>= 0.1)
shale (< 2.0)
sorbet-runtime (> 0.5)
Expand Down
175 changes: 140 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,7 @@ class Amount < Shale::Mapper
include Shale::Builder

attribute :value, Shale::Type::Float
attribute :currency, Shale::Type::String, doc: <<~DOC
This is some custom documentation that can be used by sorbet.
It will be used by the tapioca DSL compiler
to generate the RBI documentation for this attribute.
DOC
attribute :currency, Shale::Type::String
end
```

Expand All @@ -74,36 +70,6 @@ amount = Amount.build do |a|
end
```

If you use sorbet and run `bundle exec tapioca dsl` you'll get the following RBI file.

```rb
# typed: true

class Amount
include ShaleAttributeMethods

module ShaleAttributeMethods
sig { returns(T.nilable(Float)) }
def value; end

sig { params(value: T.nilable(Float)).returns(T.nilable(Float)) }
def value=(value); end

# This is some custom documentation that can be used by sorbet.
# It will be used by the tapioca DSL compiler
# to generate the RBI documentation for this attribute.
sig { returns(T.nilable(String)) }
def currency; end

# This is some custom documentation that can be used by sorbet.
# It will be used by the tapioca DSL compiler
# to generate the RBI documentation for this attribute.
sig { params(value: T.nilable(String)).returns(T.nilable(String)) }
def currency=(value); end
end
end
```
### Building nested objects

It's kind of pointless when you've got a flat structure.
Expand Down Expand Up @@ -247,6 +213,145 @@ transaction = Transaction.build do |t|
end
```

### Sorbet support

Shale-builder adds support for sorbet and tapioca.

You can leverage an additional `doc` keyword argument in `attribute` definitions.
It will be used to generate a comment in the RBI file.

```rb
require 'shale/builder'

class Amount < Shale::Mapper
include Shale::Builder

attribute :value, Shale::Type::Float
attribute :currency, Shale::Type::String, doc: <<~DOC
This is some custom documentation that can be used by sorbet.
It will be used by the tapioca DSL compiler
to generate the RBI documentation for this attribute.
DOC
end
```

If you use sorbet and run `bundle exec tapioca dsl` you'll get the following RBI file.

```rb
# typed: true

# DO NOT EDIT MANUALLY
# This is an autogenerated file for dynamic methods in `Amount`.
# Please instead update this file by running `bin/tapioca dsl Amount`.

class Amount
include ShaleAttributeMethods

module ShaleAttributeMethods
sig { returns(T.nilable(Float)) }
def value; end

sig { params(value: T.nilable(Float)).returns(T.nilable(Float)) }
def value=(value); end

# This is some custom documentation that can be used by sorbet.
# It will be used by the tapioca DSL compiler
# to generate the RBI documentation for this attribute.
sig { returns(T.nilable(String)) }
def currency; end

# This is some custom documentation that can be used by sorbet.
# It will be used by the tapioca DSL compiler
# to generate the RBI documentation for this attribute.
sig { params(value: T.nilable(String)).returns(T.nilable(String)) }
def currency=(value); end
end
end
```
#### Primitive types
If you define custom primitive types in Shale by inheriting from `Shale::Type::Value`
you can describe the return type of the getter of the field that uses this primitive type by defining the `return_type` method that returns a sorbet type.
```rb
def self.return_type = T.nilable(String)
```
You can also describe the accepted argument type in the setter by defining the `setter_type` method that returns a sorbet type.
```rb
def self.setter_type = T.any(String, Float, Integer)
```
Here is a full example.
```rb
# typed: true
require 'shale/builder'

# Cast from XML string to BigDecimal.
# And from BigDecimal to XML string.
class BigDecimalShaleType < Shale::Type::Value
class << self
extend T::Sig

# the return type of the field that uses this class as its type
def return_type = T.nilable(BigDecimal)
# the type of the argument given to a setter of the field
# that uses this class as its type
def setter_type = T.any(BigDecimal, String, NilClass)

# Decode from XML.
sig { params(value: T.any(BigDecimal, String, NilClass)).returns(T.nilable(BigDecimal)) }
def cast(value)
return if value.nil?

BigDecimal(value)
end

# Encode to XML.
#
# @param value: Value to convert to XML
sig { params(value: T.nilable(BigDecimal)).returns(T.nilable(String)) }
def as_xml_value(value)
return if value.nil?

value.to_s('F')
end
end
end

class Amount < Shale::Mapper
include Shale::Builder

# `value` uses BigDecimalShaleType as its type
attribute :value, BigDecimalShaleType
end
```

After running `bundle exec tapioca dsl` you'll get the following RBI file.

```rb
# typed: true

# DO NOT EDIT MANUALLY
# This is an autogenerated file for dynamic methods in `Amount`.
# Please instead update this file by running `bin/tapioca dsl Amount`.

class Amount
include ShaleAttributeMethods

module ShaleAttributeMethods
sig { returns(T.nilable(::BigDecimal)) }
def value; end

sig { params(value: T.nilable(T.any(::BigDecimal, ::String))).returns(T.nilable(T.any(::BigDecimal, ::String))) }
def value=(value); end
end
end
```
## Development
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
Expand Down
2 changes: 1 addition & 1 deletion lib/shale/builder/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

module Shale
module Builder
VERSION = '0.1.9'
VERSION = '0.2.0'
end
end
36 changes: 32 additions & 4 deletions lib/tapioca/dsl/compilers/shale.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def decorate
attribute_names = constant.attributes.keys.sort
attribute_names.each do |attribute_name|
attribute = T.let(constant.attributes[attribute_name], ::Shale::Attribute)
non_nilable_type, nilable_type = shale_type_to_sorbet_type(attribute)
non_nilable_type, nilable_type = shale_type_to_sorbet_return_type(attribute)
type = nilable_type
if attribute.collection?
type = "T.nilable(T::Array[#{non_nilable_type}])"
Expand Down Expand Up @@ -69,6 +69,12 @@ def decorate
mod.create_method(attribute.name, return_type: type, comments: comments)
end

non_nilable_type, nilable_type = shale_type_to_sorbet_setter_type(attribute)
type = nilable_type
if attribute.collection?
type = "T.nilable(T::Array[#{non_nilable_type}])"
end

# setter
mod.create_method(
"#{attribute.name}=",
Expand Down Expand Up @@ -106,22 +112,44 @@ def shale_builder_defined? = Boolean(defined?(::Shale::Builder))
)

sig { params(attribute: ::Shale::Attribute).returns([String, String]) }
def shale_type_to_sorbet_type(attribute)
def shale_type_to_sorbet_return_type(attribute)
return_type = SHALE_TYPES_MAP[attribute.type]
return complex_shale_type_to_sorbet_type(attribute) unless return_type
return complex_shale_type_to_sorbet_return_type(attribute) unless return_type
return [T.must(return_type.name), T.must(return_type.name)] if attribute.collection? || attribute.default.is_a?(return_type)

[T.must(return_type.name), "T.nilable(#{return_type.name})"]
end

sig { params(attribute: ::Shale::Attribute).returns([String, String]) }
def complex_shale_type_to_sorbet_type(attribute)
def complex_shale_type_to_sorbet_return_type(attribute)
return [T.cast(attribute.type.to_s, String), "T.nilable(#{attribute.type})"] unless attribute.type.respond_to?(:return_type)

return_type_string = attribute.type.return_type.to_s
[return_type_string, return_type_string]
end

sig { params(attribute: ::Shale::Attribute).returns([String, String]) }
def shale_type_to_sorbet_setter_type(attribute)
setter_type = SHALE_TYPES_MAP[attribute.type]
return complex_shale_type_to_sorbet_setter_type(attribute) unless setter_type
return [T.must(setter_type.name), T.must(setter_type.name)] if attribute.collection? || attribute.default.is_a?(setter_type)

[T.must(setter_type.name), "T.nilable(#{setter_type.name})"]
end

sig { params(attribute: ::Shale::Attribute).returns([String, String]) }
def complex_shale_type_to_sorbet_setter_type(attribute)
if attribute.type.respond_to?(:setter_type)
setter_type_string = attribute.type.setter_type.to_s
[setter_type_string, setter_type_string]
elsif attribute.type.respond_to?(:return_type)
return_type_string = attribute.type.return_type.to_s
[return_type_string, return_type_string]
else
[T.cast(attribute.type.to_s, String), "T.nilable(#{attribute.type})"]
end
end

end
end
end

0 comments on commit 389f7a2

Please sign in to comment.