From 57513c40cd9efc217bc8167a9392bbcf2bd16ee2 Mon Sep 17 00:00:00 2001 From: Lorenzo Zabot Date: Wed, 26 Jul 2023 17:27:36 +0200 Subject: [PATCH 1/5] docs: improve documentation --- README.md | 20 +++++++++++--------- aasm_rbs.gemspec | 31 +++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 9 deletions(-) create mode 100644 aasm_rbs.gemspec diff --git a/README.md b/README.md index 570b87e..13cbee1 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,23 @@ # AASM RBS Generator -Easily generate `RBS` signatures of all the `AASM`-automatically generated methods and constants of your ruby classes. +Easily generate RBS signatures for all the AASM automatically generated methods and constants of your ruby classes. ## Description -If you have found this gem, you probably are adding a type system with `RBS` on top of your `Ruby` project or `Rails` application, and you are managing the state of some of your classes with the `AASM` gem. +If you have found this gem, you probably are adding a type system with RBS on top of your Ruby project or Rails application, and you are managing the state of some of your classes with the AASM gem. -If you have no idea about what `AASM` is, you should take a look at their [README]() first. +If you have no idea about what AASM is, I encourage you to take a look at their [README](/~https://github.com/aasm/aasm) first. -You should now know that when you `include AASM` on a Ruby class and you define states, events and transitions, your classes will automatically get a few things, including: +You should now know that when you `include AASM` inside of a Ruby class and you define states, events and transitions, your classes will automatically get a few things, including: - a constant for every state - instance methods for every state -- scopes for every state if the class is an `ActiveRecord` model +- scopes for every state if the class is an `ActiveRecord` model and the [automatic scopes](/~https://github.com/aasm/aasm#automatic-scopes) feature was not disabled manually - instance methods for every event -The problem is that when writing `RBS` you should write the signatures for the previous things one-by-one and it can get really frustrating/boring when dealing with large classes. +The problem is that when writing RBS you should write the signatures for the previous things one-by-one and it can get really frustrating/boring when dealing with large classes. With this small gem, you can now generate all those signatures automatically with a single command, and save time for doing something more meaningful. ## Installation -Add the following line to your application's `Gemfile`: +Add the following line to your application's `Gemfile` in the `development` group: ```rb gem 'aasm_rbs' @@ -26,11 +26,13 @@ gem 'aasm_rbs' Then, execute `bundle install` in order to load the gem's code. ## Usage -Generating the `RBS` signatures is as easy as launching the following command from the command-line: +Generating the RBS signatures is as easy as launching the following command from the command-line: ``` bundle exec aasm_rbs ClassName ``` -The generated signatures should appear in `stdout`. +The generated signatures will appear in `stdout`. +## License +The gem is available as open source under the terms of the [MIT License](https://opensource.org/license/mit/). diff --git a/aasm_rbs.gemspec b/aasm_rbs.gemspec new file mode 100644 index 0000000..11ba657 --- /dev/null +++ b/aasm_rbs.gemspec @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require_relative 'lib/aasm_rbs/version' + +Gem::Specification.new do |spec| + spec.name = 'aasm_rbs' + spec.version = AasmRbs::VERSION + spec.summary = 'AASM RBS' + spec.description = 'Easily generate RBS signatures for all the AASM automatically generated methods and constants of your ruby classes.' + spec.license = 'MIT' + + spec.required_ruby_version = '>= 3.0.0' + + spec.author = 'Lorenzo Zabot' + spec.email = ['lorenzozabot@gmail.com'] + spec.homepage = '/~https://github.com/Uaitt/aasm_rbs' + + spec.metadata = { + 'allowed_push_host' => 'https://rubygems.org', + 'homepage_uri' => spec.homepage, + 'source_code_uri' => spec.homepage, + 'rubygems_mfa_required' => 'true' + } + + spec.files = Dir['lib/**/*', 'LICENSE', 'README.md'] + spec.require_paths = ['lib'] + spec.executables = 'exe/aasm_rbs' + spec.bindir = 'exe' + + spec.add_runtime_dependency 'aasm', '~> 5' +end From 605e1ec756e640fa58559f9074fe915f6303e74f Mon Sep 17 00:00:00 2001 From: Lorenzo Zabot Date: Wed, 26 Jul 2023 17:28:38 +0200 Subject: [PATCH 2/5] feat: rescue exceptions for better user-experience --- lib/aasm_rbs.rb | 2 ++ lib/aasm_rbs/output.rb | 11 ++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/aasm_rbs.rb b/lib/aasm_rbs.rb index 736a449..727a1c9 100644 --- a/lib/aasm_rbs.rb +++ b/lib/aasm_rbs.rb @@ -13,5 +13,7 @@ def self.run(klass_name) output.new_line output.add_events(events) output.finalize + rescue StandardError + print "aasm_rbs received an invalid class name." end end diff --git a/lib/aasm_rbs/output.rb b/lib/aasm_rbs/output.rb index 25c0f40..aaebc14 100644 --- a/lib/aasm_rbs/output.rb +++ b/lib/aasm_rbs/output.rb @@ -2,9 +2,6 @@ module AasmRbs class Output - attr_reader :klass - attr_accessor :data - def initialize(klass) @klass = klass superclass = klass.superclass == Object ? nil : " < #{klass.superclass}" @@ -14,7 +11,8 @@ def initialize(klass) def add_states(states) add_state_constants(states) create_scopes = klass.aasm.state_machine.config.create_scopes - add_state_scopes(states) if klass.respond_to?(:aasm_create_scope) && create_scopes + active_record_model = klass.respond_to?(:aasm_create_scope) + add_state_scopes(states) if active_record_model && create_scopes add_predicate_states_methods(states) end @@ -37,13 +35,16 @@ def finalize private + attr_reader :klass + attr_accessor :data + def add_state_constants(states) states.each { |state| self.data += " STATE_#{state.upcase}: String\n" } self.data += "\n" end def add_state_scopes(states) - states.each { |state| self.data += " def self.#{state}: () -> #{klass}::ActiveRecord_Relation\n" } + states.each { |state| self.data += " def self.#{state}: () -> ::ActiveRecord_Relation\n" } self.data += "\n" end From f9a98f667ca5b7379f5735269ca8112617054a47 Mon Sep 17 00:00:00 2001 From: Lorenzo Zabot Date: Wed, 26 Jul 2023 17:29:01 +0200 Subject: [PATCH 3/5] refactor: minor improvements to RBS --- sig/aasm_rbs.rbs | 8 ++++---- sig/aasm_rbs/output.rbs | 8 +++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/sig/aasm_rbs.rbs b/sig/aasm_rbs.rbs index 8c1b960..722a3e9 100644 --- a/sig/aasm_rbs.rbs +++ b/sig/aasm_rbs.rbs @@ -1,10 +1,10 @@ module AasmRbs - def self.run: (String) -> String + def self.run: (String) -> String? end -# There is yet no official RBS signatures for the AASM module that gets -# included in a class. Therefore, the only thing we can do in order to not -# make Steep unhappy is to declare a dummy signature ourself. +# There are yet no official RBS signatures for the AASM module that gets +# included in our classes. Therefore, the only thing we can do in order to not +# make Steep unhappy is to declare a dummy signature ourself for the #aasm method. class Class def aasm: () -> untyped end diff --git a/sig/aasm_rbs/output.rbs b/sig/aasm_rbs/output.rbs index 81fd262..44da055 100644 --- a/sig/aasm_rbs/output.rbs +++ b/sig/aasm_rbs/output.rbs @@ -1,8 +1,5 @@ module AasmRbs class Output - attr_reader klass: Class - attr_accessor data: String - def initialize: (Class) -> String def add_states: (Array[String]) -> Array[String] @@ -11,6 +8,11 @@ module AasmRbs def new_line: () -> String def finalize: () -> String + private + + attr_reader klass: Class + attr_accessor data: String + def add_state_constants: (Array[String]) -> String def add_state_scopes: (Array[String]) -> String def add_predicate_states_methods: (Array[String]) -> Array[String] From 28ba1326667704db2aab37987daba017f822bbc8 Mon Sep 17 00:00:00 2001 From: Lorenzo Zabot Date: Wed, 26 Jul 2023 17:29:43 +0200 Subject: [PATCH 4/5] test: improve specs and add Refund class --- spec/aasm_rbs/output_spec.rb | 73 ++++++++++++++++++++++++------ spec/aasm_rbs_spec.rb | 42 +++++++++++++++-- spec/classes/application_record.rb | 7 +++ spec/classes/refund.rb | 22 +++++++++ spec/classes/user.rb | 6 +-- 5 files changed, 127 insertions(+), 23 deletions(-) create mode 100644 spec/classes/application_record.rb create mode 100644 spec/classes/refund.rb diff --git a/spec/aasm_rbs/output_spec.rb b/spec/aasm_rbs/output_spec.rb index 0d835ae..c7ba14a 100644 --- a/spec/aasm_rbs/output_spec.rb +++ b/spec/aasm_rbs/output_spec.rb @@ -29,23 +29,22 @@ def cleaning?: () -> bool it 'correctly adds the states signatures to the data' do states = klass.aasm.states.map(&:name) output.add_states(states) - output.finalize - expect(output.data).to eq(expected_rbs) + expect(output.finalize).to eq(expected_rbs) end end - context 'with an ActiveRecord model' do + context 'with an ActiveRecord model that enables AASM automatic scopes' do let(:klass) { User } let(:expected_rbs) do <<~RBS - class User < ActiveRecord::Base + class User < ApplicationRecord STATE_PENDING: String STATE_APPROVED: String STATE_REJECTED: String - def self.pending: () -> User::ActiveRecord_Relation - def self.approved: () -> User::ActiveRecord_Relation - def self.rejected: () -> User::ActiveRecord_Relation + def self.pending: () -> ::ActiveRecord_Relation + def self.approved: () -> ::ActiveRecord_Relation + def self.rejected: () -> ::ActiveRecord_Relation def pending?: () -> bool def approved?: () -> bool @@ -57,8 +56,30 @@ def rejected?: () -> bool it 'correctly adds the states signatures to the data' do states = klass.aasm.states.map(&:name) output.add_states(states) - output.finalize - expect(output.data).to eq(expected_rbs) + expect(output.finalize).to eq(expected_rbs) + end + end + + context 'with an ActiveRecord model that disables AASM automatic scopes' do + let(:klass) { Refund } + let(:expected_rbs) do + <<~RBS + class Refund < ApplicationRecord + STATE_PENDING: String + STATE_PROCESSED: String + STATE_FAILED: String + + def pending?: () -> bool + def processed?: () -> bool + def failed?: () -> bool + end + RBS + end + + it 'correctly adds the states signatures to the data' do + states = klass.aasm.states.map(&:name) + output.add_states(states) + expect(output.finalize).to eq(expected_rbs) end end end @@ -88,16 +109,15 @@ def may_sleep?: (*untyped) -> bool it 'correctly adds the events signatures to the data' do events = klass.aasm.events.map(&:name) output.add_events(events) - output.finalize - expect(output.data).to eq(expected_rbs) + expect(output.finalize).to eq(expected_rbs) end end - context 'with an ActiveRecord model' do + context 'with an ActiveRecord model that enables AASM automatic scopes' do let(:klass) { User } let(:expected_rbs) do <<~RBS - class User < ActiveRecord::Base + class User < ApplicationRecord def approve: (*untyped) -> bool def approve!: (*untyped) -> bool def approve_without_validation!: (*untyped) -> bool @@ -113,8 +133,31 @@ def may_reject?: (*untyped) -> bool it 'correctly adds the events signatures to the data' do events = klass.aasm.events.map(&:name) output.add_events(events) - output.finalize - expect(output.data).to eq(expected_rbs) + expect(output.finalize).to eq(expected_rbs) + end + end + + context 'with an ActiveRecord model that disables AASM automatic scopes' do + let(:klass) { Refund } + let(:expected_rbs) do + <<~RBS + class Refund < ApplicationRecord + def process: (*untyped) -> bool + def process!: (*untyped) -> bool + def process_without_validation!: (*untyped) -> bool + def may_process?: (*untyped) -> bool + def fail: (*untyped) -> bool + def fail!: (*untyped) -> bool + def fail_without_validation!: (*untyped) -> bool + def may_fail?: (*untyped) -> bool + end + RBS + end + + it 'correctly adds the events signatures to the data' do + events = klass.aasm.events.map(&:name) + output.add_events(events) + expect(output.finalize).to eq(expected_rbs) end end end diff --git a/spec/aasm_rbs_spec.rb b/spec/aasm_rbs_spec.rb index ee20c65..e3f2ff5 100644 --- a/spec/aasm_rbs_spec.rb +++ b/spec/aasm_rbs_spec.rb @@ -5,6 +5,7 @@ require_relative '../lib/aasm_rbs' require_relative 'classes/job' require_relative 'classes/user' +require_relative 'classes/refund' RSpec.describe AasmRbs do describe '.run' do @@ -43,18 +44,18 @@ def may_sleep?: (*untyped) -> bool end end - context 'with an ActiveRecord model' do + context 'with an ActiveRecord model that enables AASM automatic scopes' do let(:klass_name) { 'User' } let(:expected_rbs) do <<~RBS - class User < ActiveRecord::Base + class User < ApplicationRecord STATE_PENDING: String STATE_APPROVED: String STATE_REJECTED: String - def self.pending: () -> User::ActiveRecord_Relation - def self.approved: () -> User::ActiveRecord_Relation - def self.rejected: () -> User::ActiveRecord_Relation + def self.pending: () -> ::ActiveRecord_Relation + def self.approved: () -> ::ActiveRecord_Relation + def self.rejected: () -> ::ActiveRecord_Relation def pending?: () -> bool def approved?: () -> bool @@ -77,5 +78,36 @@ def may_reject?: (*untyped) -> bool expect(actual_output).to eq(expected_rbs) end end + + context 'with an ActiveRecord model that disables AASM automatic scopes' do + let(:klass_name) { 'Refund' } + let(:expected_rbs) do + <<~RBS + class Refund < ApplicationRecord + STATE_PENDING: String + STATE_PROCESSED: String + STATE_FAILED: String + + def pending?: () -> bool + def processed?: () -> bool + def failed?: () -> bool + + def process: (*untyped) -> bool + def process!: (*untyped) -> bool + def process_without_validation!: (*untyped) -> bool + def may_process?: (*untyped) -> bool + def fail: (*untyped) -> bool + def fail!: (*untyped) -> bool + def fail_without_validation!: (*untyped) -> bool + def may_fail?: (*untyped) -> bool + end + RBS + end + + it 'returns the right RBS' do + actual_output = described_class.run(klass_name) + expect(actual_output).to eq(expected_rbs) + end + end end end diff --git a/spec/classes/application_record.rb b/spec/classes/application_record.rb new file mode 100644 index 0000000..8657ee1 --- /dev/null +++ b/spec/classes/application_record.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'active_record' + +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/spec/classes/refund.rb b/spec/classes/refund.rb new file mode 100644 index 0000000..2e1739e --- /dev/null +++ b/spec/classes/refund.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'aasm' +require_relative 'application_record' + +class Refund < ApplicationRecord + include AASM + + aasm column: :state, create_scopes: false do + state :pending, initial: true + state :processed + state :failed + + event :process do + transitions from: :pending, to: :processed + end + + event :fail do + transitions from: %i[pending processed], to: :failed + end + end +end diff --git a/spec/classes/user.rb b/spec/classes/user.rb index a89e04f..9d170c4 100644 --- a/spec/classes/user.rb +++ b/spec/classes/user.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true require 'aasm' -require 'active_record' +require_relative 'application_record' -class User < ActiveRecord::Base +class User < ApplicationRecord include AASM - aasm column: 'state' do + aasm column: :state do state :pending, initial: true state :approved state :rejected From a72f44409ff056aec7bf0d89b7629f67f3490a13 Mon Sep 17 00:00:00 2001 From: Lorenzo Zabot Date: Wed, 26 Jul 2023 17:31:50 +0200 Subject: [PATCH 5/5] refactor: explicitly puts to --- aasm-rbs.gemspec | 31 ------------------------------- exe/aasm_rbs | 6 +++--- 2 files changed, 3 insertions(+), 34 deletions(-) delete mode 100644 aasm-rbs.gemspec diff --git a/aasm-rbs.gemspec b/aasm-rbs.gemspec deleted file mode 100644 index cab2dac..0000000 --- a/aasm-rbs.gemspec +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -require_relative 'lib/aasm_rbs/version' - -Gem::Specification.new do |spec| - spec.name = 'aasm_rbs' - spec.version = AasmRbs::VERSION - spec.summary = 'AASM RBS' - spec.description = 'Easily generate RBS signatures for all the Ruby classes that implement a state-machine with AASM' - spec.license = 'MIT' - - spec.required_ruby_version = '>= 3.0.0' - - spec.author = 'Lorenzo Zabot' - spec.email = ['lorenzozabot@gmail.com'] - spec.homepage = '/~https://github.com/Uaitt/aasm_rbs' - - spec.metadata = { - 'allowed_push_host' => 'https://rubygems.org', - 'homepage_uri' => spec.homepage, - 'source_code_uri' => spec.homepage, - 'rubygems_mfa_required' => 'true' - } - - spec.files = Dir['lib/**/*', 'LICENSE', 'README.md'] - spec.require_paths = ['lib'] - spec.executables = 'exe/aasm_rbs' - spec.bindir = 'exe' - - spec.add_runtime_dependency 'aasm', '~> 5' -end diff --git a/exe/aasm_rbs b/exe/aasm_rbs index c21e1ec..16106d7 100755 --- a/exe/aasm_rbs +++ b/exe/aasm_rbs @@ -2,6 +2,6 @@ require_relative '../lib/aasm_rbs' -puts '' -puts AasmRbs.run(ARGV[0] || '') -puts '' +$stdout.puts '' +$stdout.puts AasmRbs.run(ARGV[0] || '') +$stdout.puts ''