From 389f7a238b34e2251eb5b982917bcb4faf87c1e3 Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Tue, 11 Jun 2024 11:05:28 +0200 Subject: [PATCH] v0.2.0 --- CHANGELOG.md | 5 + Gemfile.lock | 2 +- README.md | 175 +++++++++++++++++++++++------ lib/shale/builder/version.rb | 2 +- lib/tapioca/dsl/compilers/shale.rb | 36 +++++- 5 files changed, 179 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70c2c3e..5e4c752 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Gemfile.lock b/Gemfile.lock index 9464d1a..ae595b1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/README.md b/README.md index ea8f90b..6c8196b 100644 --- a/README.md +++ b/README.md @@ -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 ``` @@ -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. @@ -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. diff --git a/lib/shale/builder/version.rb b/lib/shale/builder/version.rb index 0fa6773..6231dfb 100644 --- a/lib/shale/builder/version.rb +++ b/lib/shale/builder/version.rb @@ -2,6 +2,6 @@ module Shale module Builder - VERSION = '0.1.9' + VERSION = '0.2.0' end end diff --git a/lib/tapioca/dsl/compilers/shale.rb b/lib/tapioca/dsl/compilers/shale.rb index e9c52b8..dac9984 100644 --- a/lib/tapioca/dsl/compilers/shale.rb +++ b/lib/tapioca/dsl/compilers/shale.rb @@ -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}])" @@ -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}=", @@ -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