[Ruby 3.0] A memo that I added a type definition to a library I wrote

Introduction

Type definitions were introduced in Ruby 3.0. Ruby 3 \ .0 \ .0 released

I wanted to check the procedure when actually adding to the existing code, so the library (Gem) I created for studying just before seemed to be small and easy to introduce, so I added a type definition to this So, I checked the flow until the Gem user actually uses the type definition. kuredev/simple_ping: Simpe Ping Client for Ruby

Generally, referring to the flow of the following article, I will summarize what I confirmed and investigated in each step. I tried to write a static type definition of Ruby 3 \ .0 in a library like TypeScript -Narazaka :: Blog

Add type definition to library

Create RBS automatically

First of all, we need to create an RBS file to add the type definition. Since it is difficult to make from scratch by hand, we will first consider creating a rough one automatically. There are three main ways to create RBS automatically,

If possible, I wanted the argument/return type information to be created automatically, so I first tried using TypeProf.

reference

Characteristics of each method to generate RBS from Ruby -pockestrap

[Ruby 3 \ .0] Memo -memo \ .log to try various RBS generation methods

Attempt with TypeProf

However, when I actually tried to output the type definition with typeprof, the following error occurred.

% typeprof lib/simple_ping/client.rb 
# Analysis Error
A constant `MonitorMixin' is used but not defined in RBS

The message says that there is no MonitorMixin module definition in RBS. I'm using the logger library in the library, but it seems that Logger :: LogDevice in it is using MonitorMixin.

Typedefing a simple code like the one below still gave the same error, so it seemed to be reproducible.

I haven't gotten too deep into it, but many of the Ruby standard libraries already provide RBS type definition information, but I wonder if typeprof also failed to interpret the type because there is no definition for MonitorMixin yet. I think.

tmp.rb


require "logger"

class Kure
  def self.run
    logger = Logger.new
    logger.info "hoge"
  end
end

Attempt by rbs prototype runtime

It seemed that it could not be solved immediately, so this time I decided to prepare RBS with rbs prototype runtime. I also tried rbs prototype rb, but I chose this because the result was simpler with rbs prototype runtime due to the format and the presence or absence of comments.

If you want to create RBS with rbs prototype runtime, you need the code to execute the program, so prepare the following code at the top of the repository.

sample_run.rb


require_relative "./lib/simple_ping"

ping_client = SimplePing::Client.new(src_ip_addr: "172.31.7.56")
puts ping_client.exec(dest_ip_addr: "8.8.8.8")

Execute as follows.

% sudo rbs prototype runtime -R sample_run.rb "SimplePing::*"
Execution result
class SimplePing::Client
  public

  def exec: (dest_ip_addr: untyped, ?data: untyped) -> untyped

  private

  def initialize: (src_ip_addr: untyped, ?log_level: untyped) -> untyped

  def logger: () -> untyped

  def socket: () -> untyped
end

SimplePing::Client::TIMEOUT_TIME: Integer

class SimplePing::ICMP
  public

  def data: () -> untyped

  def data=: (untyped) -> untyped

  def id: () -> untyped

  def id=: (untyped) -> untyped

  def is_type_destination_unreachable?: () -> untyped

  def is_type_echo?: () -> untyped

  def is_type_echo_reply?: () -> untyped

  def is_type_redirect?: () -> untyped

  def seq_number: () -> untyped

  def seq_number=: (untyped) -> untyped

  def successful_reply?: (untyped icmp) -> untyped

  def to_trans_data: () -> untyped

  def type: () -> untyped

  def type=: (untyped) -> untyped

  private

  def carry_up: (untyped num) -> untyped

  def checksum: () -> untyped

  def gen_data: () -> untyped

  def gen_id: () -> untyped

  def gen_seq_number: () -> untyped

  def initialize: (type: untyped, ?code: untyped, ?id: untyped, ?seq_number: untyped, ?data: untyped) -> untyped
end

SimplePing::ICMP::TYPE_ICMP_DESTINATION_UNREACHABLE: Integer

SimplePing::ICMP::TYPE_ICMP_ECHO_REPLY: Integer

SimplePing::ICMP::TYPE_ICMP_ECHO_REQUEST: Integer

SimplePing::ICMP::TYPE_ICMP_REDIRECT: Integer

class SimplePing::RecvMessage
  public

  def code: () -> untyped

  def data: () -> untyped

  def id: () -> untyped

  def seq_number: () -> untyped

  def to_icmp: () -> untyped

  def type: () -> untyped

  private

  def initialize: (untyped mesg) -> untyped
end

Confirmation of automatic creation result

Save the above result as an RBS file in the sig directory, and use steep check to check if there is a problem with the type information.

steep preparation


% gem install steep
% mkdir sig
% vim sig/simple_ping.rbs #Paste the above result
% steep init
% vim Steepfile #Described below
target :lib do
  signature "sig"
  check "lib"
  check "./"
end

Run


% steep check
sig/simple_ping.rbs:1:0...13:3  UnknownTypeNameError: name=::SimplePing
sig/simple_ping.rbs:17:0...61:3 UnknownTypeNameError: name=::SimplePing
sig/simple_ping.rbs:71:0...89:3 UnknownTypeNameError: name=::SimplePing
sig/simple_ping.rbs:15:0...15:41        UnknownTypeNameError: name=::SimplePing
sig/simple_ping.rbs:63:0...63:60        UnknownTypeNameError: name=::SimplePing
sig/simple_ping.rbs:65:0...65:47        UnknownTypeNameError: name=::SimplePing
sig/simple_ping.rbs:67:0...67:49        UnknownTypeNameError: name=::SimplePing
sig/simple_ping.rbs:69:0...69:45        UnknownTypeNameError: name=::SimplePing

Since it is the type definition information created automatically, I would like it to succeed, but it has failed. I was angry that the :: SimplePing was missing (defined in the code but not in the RBS file). Since an error occurred each time I made corrections including this, I will manually correct the RBS file in some respects. (There may be some errors that can be avoided by the argument of the command when generating rbs)

Manually fix

This time I tried to fix it as follows.

――If you do not write the top three lines, an error will occur, so I wrote them. --The bottom four lines are the constructors and methods of the SimplePing :: Clinet class. The information generated by the rbs command basically has an argument/return type of untyped, but since I wanted to write the argument type etc. for this class that the user of this Gem uses directly, from untyped I modified it and modified it to the specification type.

simple_ping.rbs


+ module SimplePing
+ end
+ SimplePing::VERSION: String
- def initialize: (src_ip_addr: untyped, ?log_level: untyped) -> untyped
+ def initialize: (src_ip_addr: String, ?log_level: Integer) -> void
- def exec: (dest_ip_addr: untyped, ?data: untyped) -> untyped
+ def exec: (dest_ip_addr: String, ?data: String) -> bool

Check the steep again and confirm that it works without any problem.

Run


% steep check
#OK if nothing is output

Operation check

Try writing the wrong code on purpose to see if type checking works. Let's modify the execution file prepared above as follows.

sample_run.rb


require_relative "./lib/simple_ping"

ping_client = SimplePing::Client.new(src_ip_addr: 100) #Originally"IP Address"The part specified by is Integer
puts ping_client.exec(dest_ip_addr: 100) #Originally"IP Address"The part specified by is Integer

When I did a steep check, he pointed out that it was incorrect according to the type definition information.

% steep check 
sample_run.rb:3:50: IncompatibleAssignment: lhs_type=::String, rhs_type=::Integer (100)
  ::Integer <: ::String
   ::Numeric <: ::String
    ::Object <: ::String
     ::BasicObject <: ::String
==> ::BasicObject <: ::String does not hold
sample_run.rb:4:36: IncompatibleAssignment: lhs_type=::String, rhs_type=::Integer (100)
  ::Integer <: ::String
   ::Numeric <: ::String
    ::Object <: ::String
     ::BasicObject <: ::String
==> ::BasicObject <: ::String does not hold

By the way, if you put the Steep plugin in VS Code, it will point out even while writing the code. It's convenient!

image.png

Install Gem from outside and try using the added type definition

release

Put the created definition information and release it. I tried writing a static type definition of Ruby 3 \ .0 in a library like TypeScript -Narazaka :: Blog As mentioned in the article, put the definition file in the sig directory. If you use it, it will be read automatically when you use it, so put the created RBS file and release it.

% git add sig/simple_ping.rbs
% git commit -m "xxx"
% [Release work below...]

Use from outside

Try installing and using the released Gem in another environment. If you want to refer to the type definition of the library put in Bundler, it seems that you need to execute Steep put in Bundler, so put Steep in Bundler as well.

% bundle init
% vim Gemfile #Add the following. Version should be read as appropriate
gem "simple_ping", "0.1.1"
gem "steep"
% bundle install --path vendor/bundle

After installation, prepare for steep.

% bundle exec steep init
% vim Steepfile
target :lib do
  signature "sig"
  check "./"
  library "simple_ping"
end

If you write like library" simple_ping ", it seems that RBS under sig installed by Bundler will be read. In this environment, I dare to write the code using Gem in the wrong form as before.

sample_run.rb


require "simple_ping"

SimplePing::Client.new(src_ip_addr: 1) #Specify with Integer where it should be specified with a character string

Try running steep check. He pointed out an error in the type information! I will omit the image, but it will warn you properly on VS Code.

% bundle exec steep check
sample_run.rb:3:36: IncompatibleAssignment: lhs_type=::String, rhs_type=::Integer (1)
  ::Integer <: ::String
   ::Numeric <: ::String
    ::Object <: ::String
     ::BasicObject <: ::String
==> ::BasicObject <: ::String does not hold

That's all, thank you for your hard work.

Other reference articles

Type check with Ruby! Introduction to RBS to understand by moving ~ Understand with sample code! Major new features and changes in Ruby 3 \ .0 Part 1 ~ -Qiita

[Ruby] I made a simple Ping client -Qiita

Recommended Posts

[Ruby 3.0] A memo that I added a type definition to a library I wrote
I tried to write code like a type declaration in Ruby
[Ruby] I want to do a method jump!
I made a Ruby extension library in C
[Ruby] I want to make a program that displays today's day of the week!
A memo that I was addicted to when making batch processing with Spring Boot
I made a library that works like a Safari tab !!
I want to add a reference type column later
I want to create a generic annotation for a type
I tried to convert a string to a LocalDate type in Java
I want to create a Parquet file even in Ruby
I wrote a C parser (like) using PEG in Ruby
[ruby] Creating a program that responds only to specific conditions
I wrote a code to convert numbers to romaji in TDD
Personal memo Progate Ruby I (2)
Personal memo Progate Ruby I (1)
ruby exercise memo I (puts)
The story of making a binding for libui, a GUI library for Ruby that is easy to install
How to deal with the type that I thought about writing a Java program for 2 years
A story that I struggled to challenge a competition professional with Java
I want to add a browsing function with ruby on rails
I want to use swipeback on a screen that uses XLPagerTabStrip
[Ruby] I want to put an array in a variable. I want to convert to an array
I get a Ruby version error when I try to start Rails.
I wrote a Stalin sort that feels like a mess in Java
Ruby: I made a FizzBuzz program!
A memo that touched Spring Boot
I tried to make a parent class of a value object in Ruby
A memo that was soberly addicted to the request of multipart / form-data
[Rails] I tried to implement a transaction that combines multiple DB processes
I was addicted to a simple test of Jedis (Java-> Redis library)
I was a little addicted to running old Ruby environment and old Rails
I tried to make a Web API that connects to DB with Quarkus
A memo that enabled VS Code + JUnit 5 to be used on Windows 10
Created a library that makes it easy to handle Android Shared Prefences
I thought about the best way to create a ValueObject in Ruby
[Ruby] I tried to summarize the methods that frequently appear in paiza
[Ruby] I tried to summarize the methods that frequently appear in paiza ②