Parlour
Parlour is a Ruby type information generator, merger and parser, supporting both Sorbet RBI files and Ruby 3/Steep RBS files. It consists of three key parts:
-
The generator, which outputs beautifully formatted RBI/RBS files, created using an intuitive DSL.
-
The plugin/build system, which allows multiple Parlour plugins to generate RBIs for the same codebase. These are combined automatically as much as possible, but any other conflicts can be resolved manually through prompts.
-
The parser (currently RBI-only), which can read an RBI and convert it back into a tree of generator objects.
Why should I use this?
-
Parlour enables much easier creation of RBI/RBS generators, as formatting is all handled for you, and you don't need to write your own CLI.
-
You can use many plugins together seamlessly, running them all with a single command and consolidating all of their definitions into a single output file.
-
You can effortlessly build tools which need to access types within an RBI; no need to write your own parser!
-
You can generate RBI/RBS to ship with your gem for consuming projects to use (see "RBIs within gems" in Sorbet's docs).
Please read the wiki to get started!
Feature Support
Feature | RBI | RBS |
---|---|---|
GENERATION | ||
Classes | ✅ | ⚠️ (missing extension ) |
Modules | ✅ | ⚠️ (missing extension ) |
Interfaces | ✅ | ✅ |
Attributes | ✅ | ✅ |
Methods | ✅ | ✅ |
Overloaded methods | ❌* | ✅ |
Structs | ✅ | ✅† |
Enums | ✅ | ✅† |
Generation with plugins | ✅ | ❌ |
MANIPULATION | ||
Parsing | ✅ | ❌ |
Merging | ✅ | ❌ |
-
✅ - Well supported
-
⚠️ - Some missing features
-
❌ - Not currently supported
-
* Only supported in stdlib types anyway
-
† Not natively supported; available as a one-way conversion from RBI
Creating Type Information
Each file format has its own type information generator class, so there are two
different generators you can use: RbiGenerator
and RbsGenerator
. Both
generators are similar to use, however they provide different object types and
parameters to match the functionality of their underlying type systems.
You can also convert your type information between formats; see converting between formats.
Using Just the Generator
Here's a quick example of how you can generate some type information. Here
we'll generate an RBI using the RbiGenerator
classes:
require 'parlour'
generator = Parlour::RbiGenerator.new
generator.root.create_module('A') do |a|
a.create_class('Foo') do |foo|
foo.create_method('add_two_integers', parameters: [
Parlour::RbiGenerator::Parameter.new('a', type: 'Integer'),
Parlour::RbiGenerator::Parameter.new('b', type: 'Integer')
], return_type: 'Integer')
end
a.create_class('Bar', superclass: 'Foo')
end
generator.rbi # => Our RBI as a string
This will generate the following RBI:
module A
class Foo
sig { params(a: Integer, b: Integer).returns(Integer) }
def add_two_integers(a, b); end
end
class Bar < Foo
end
end
Using the RBS generator looks similar, but has an intermediary
MethodSignature
class to support RBS' method overloading:
require 'parlour'
generator = Parlour::RbsGenerator.new
generator.root.create_module('A') do |a|
a.create_class('Foo') do |foo|
foo.create_method('add_two_integers', [
Parlour::RbsGenerator::MethodSignature.new(
[
Parlour::RbsGenerator::Parameter.new('a', type: 'Integer'),
Parlour::RbsGenerator::Parameter.new('b', type: 'Integer')
],
'Integer'
)
])
end
a.create_class('Bar', superclass: 'Foo')
end
generator.rbs # => Our RBS as a string
This generates an equivalent RBS file:
module A
class Foo
def add_two_integers: (Integer a, Integer b) -> Integer
end
class Bar < Foo
end
end
Writing a Plugin
Plugins are better than using the generator alone, as your plugin can be combined with others to produce larger files without conflicts.
We could write the above example as an RBI plugin like this:
require 'parlour'
class MyPlugin < Parlour::Plugin
def generate(root)
root.create_module('A') do |a|
a.create_class('Foo') do |foo|
foo.create_method('add_two_integers', parameters: [
Parlour::RbiGenerator::Parameter.new('a', type: 'Integer'),
Parlour::RbiGenerator::Parameter.new('b', type: 'Integer')
], return_type: 'Integer')
end
a.create_class('Bar', superclass: 'Foo')
end
end
end
(Obviously, your plugin will probably examine a codebase somehow, to be more useful!)
You can then run several plugins, combining their output and saving it into one
RBI file, using the command-line tool. The command line tool is configurated
using a .parlour
YAML file. For example, if that code was in a file
called plugin.rb
, then using this .parlour
file and then running parlour
would save the RBI into output.rbi
:
output_file:
rbi: output.rbi
relative_requires:
- plugin.rb
- app/models/*.rb
plugins:
MyPlugin: {}
The {}
indicates that this plugin needs no extra configuration. If it did need
configuration, this could be specified like so:
plugins:
MyPlugin:
foo: something
bar: something else
You can also use plugins from gems. If that plugin was published as a gem called
parlour-gem
:
output_file:
rbi: output.rbi
requires:
- parlour-gem
plugins:
MyPlugin: {}
The real power of this is the ability to use many plugins at once:
output_file:
rbi: output.rbi
requires:
- gem1
- gem2
- gem3
plugins:
Gem1::Plugin: {}
Gem2::Plugin: {}
Gem3::Plugin: {}
Currently, only plugins which generate RBI files are supported. However, you can use Parlour's type conversion to convert the RBI types into RBS types:
output_file:
rbi: output.rbi
rbs: output.rbs
Using Types
The most important part of your type information is the types themselves, which
you'll be specifying for method parameters, method returns, and attributes.
These include simple types like String
, up to more complex types like
"an array of elements which are one of Integer
, String
, or nil".
There are two ways to represent these types in Parlour:
-
As generalized types; that is, instances of classes in the
Parlour::Types
namespace. This is the recommended way, as it is format-agnostic and can be compiled to RBI or RBS. For more information about these types and how to use them, see this wiki page. -
As strings of code written in the format that your type system expects. The given strings are directly inserted into the final type file. These types are not portable across formats, and as such are not recommended and may be phased out in the future.
Currently most type values within Parlour are typed as Types::TypeLike
,
which accepts a String
or a Types::Type
subclass.
include Parlour
# Two ways to express an attribute called 'example', which is:
# an array of nilable strings or integers
# 1. With generalised types - type is agnostic to the underlying type system
root.create_attr_accessor('example', type:
Types::Array.new(
Types::Nilable.new(
Types::Union.new(['String', 'Integer'])
)
)
)
# 2. With string types - format depends on type system
# If using RBI...
root.create_attr_accessor('example', type:
'T::Array[T.nilable(T.any(String, Integer))]'
)
# If using RBS...
root.create_attr_accessor('example', type:
'Array[?(String | Integer)]'
)
Generalizing String Types
If you have loaded an RBI project or created a structure of nodes on an
RbiGenerator
, you can use #generalize_from_rbi!
on your root namespace
to attempt to permanently convert the RBI string types into generalized types:
# Build up an RBI tree with string types
root.create_attr_accessor('example', type:
'T::Array[T.nilable(T.any(String, Integer))]'
)
# Generalize it
root.generalize_from_rbi!
# Take a look at our generalized type!
pp root.children.first.type
# => #<Parlour::Types::Array:0x0000557cdcebfdf8
# @element=
# #<Parlour::Types::Nilable:0x0000557cdcef8c70
# @type=
# #<Parlour::Types::Union:0x0000557cdcea0a70
# @types=
# [#<Parlour::Types::Raw:0x0000557cdcea1920 @str="String">,
# #<Parlour::Types::Raw:0x0000557cdcea0ae8 @str="Integer">]>>>
Parsing RBIs
You can either parse individual RBI files, or point Parlour to the root of a project and it will locate, parse and merge all RBI files.
Note that Parlour isn't limited to just RBIs; it can parse inline sigs
out
of your Ruby source too!
require 'parlour'
# Return the object tree of a particular file
Parlour::TypeLoader.load_file('path/to/your/file.rbis')
# Return the object tree for an entire Sorbet project - slow but thorough!
Parlour::TypeLoader.load_project('root/of/the/project')
The structure of the returned object trees is identical to those you would
create when generating an RBI, built of instances of RbiObject
subclasses.
Generating RBI for a Gem
Include parlour
as a development_dependency in your .gemspec
:
spec.add_development_dependency 'parlour'
Run Parlour from the command line:
bundle exec parlour
Parlour is configured to use sane defaults assuming a standard gem structure
to generate an RBI that Sorbet will automatically find when your gem is included
as a dependency. If you require more advanced configuration you can add a
.parlour
YAML file in the root of your project (see this project's .parlour
file as an example).
To disable the parsing step entire and just run plugins you can set parser: false
in your .parlour
file.
Converting Between Formats
For more information, see the wiki page.
Currently, only RBI to RBS conversion is supported, and if you've used string types (or are using a freshly-loaded project) you must generalize them first.
Then, all you need to do is create an RbsGenerator
(which the converter will
add your converted types to) and a Conversion::RbiToRbs
instance (which
performs the conversion). Then you can convert each object at your
RbiGenerator
's root namespace:
rbi_gen = Parlour::RbiGenerator.new
# Then, after populating the RbiGenerator with types...
# Create an RbsGenerator and a converter
rbs_gen = Parlour::RbsGenerator.new
converter = Parlour::Conversion::RbiToRbs.new(rbs_gen)
# Convert each item at the root of the RbiGenerator and it to the root of the RbsGenerator
converter.convert_all(rbi_gen.root, rbs_gen.root)
Parlour Plugins
Have you written an awesome Parlour plugin? Please submit a PR to add it to this list!
- Sord - Generate RBIs from YARD documentation
- parlour-datamapper - Simple plugin for generating DataMapper model types
- sorbet-rails - Generate RBIs for Rails models, routes, mailers, etc.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/AaronC81/parlour. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
After making changes, you may wish to regenerate the RBI definitions in the sorbet
folder by running these srb rbi
commands:
srb rbi gems
srb rbi sorbet-typed
You should also regenerate the parlour.rbi file by running bundle exec parlour
. Don't edit this file manually, as your changes will be overwritten!
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the Parlour project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.