Keep a string until after a pattern, trim the trailing remainder
Creates and returns a new debounced version of the passed function that will postpone its execution until after wait milliseconds have elapsed since the last time it was invoked
Deferring loading of JS files until after React loads
> Creates a debounced function that delays invoking func until after wait milliseconds have elapsed since the last time the debounced function was invoked.
Creates a debounced function that delays invoking func until after wait milliseconds have elapsed since the last time the debounced function was invoked.
Useful TypeScript utilities.
A JS utility that delays the execution of a callback function until after a defined period of inactivity, preventing multiple rapid calls.
Debounce a function
This plugin defines a global device object, which describes the device's hardware and software. Although the object is in the global scope, it is not available until after the deviceready event.
Postpone Promises from resolving until after a specific number of milliseconds.
Gracefully handle a Promise using async/await.
Gracefully handle a Promise using async/await.
Creates a debounced function that delays invoking func until after wait milliseconds have elapsed since the last time the debounced function was invoked.
Run Cypress multiple times in a row
Dedicated types library for ramda
Read and write files atomically and reliably.
An efficient queue capable of managing thousands of concurrent animations.
A simple Node.js module to check if a TCP port is already bound.
A waiting plugin for Cypress
High-priority task queue for Node.js and browsers
just emit 'log' events on the process object
A stream that emits multiple other streams one after another (streams3)
Assists with onboarding new MetaMask users
Delay function calls until a set time elapses after the last invocation
Creates a debounced function that delays invoking func until after wait milliseconds have elapsed since the last time the debounced function was invoked.
Suspends execution until state changes via ::Wait.until! methods, timing-out after a configured period of time
Named after Goran Invanisevic, the tennis legend who won the Wimbledon after losing the final 3 times, Goran provides a simple syntax to run a block of code multiple times. E.g. * run block 'x' number of times * run until the block returns a non-nil value * run until the block does not raise an exception * run until the block returns a non-zero value, to a maximum of 3 times, and return nil if all runs return a 0 Goran is especially useful for running network calls which have unexpected outputs like 404, timeouts. It is an easy way to build in retry logic into these calls and handle cases where these calls do not succeed at all.
CircuitBreaker is a relatively simple Ruby mixin that will wrap a call to a given service in a circuit breaker pattern. The circuit starts off "closed" meaning that all calls will go through. However, consecutive failures are recorded and after a threshold is reached, the circuit will "trip", setting the circuit into an "open" state. In an "open" state, every call to the service will fail by raising CircuitBrokenException. The circuit will remain in an "open" state until the failure timeout has elapsed. After the failure_timeout has elapsed, the circuit will go into a "half open" state and the call will go through. A failure will immediately pop the circuit open again, and a success will close the circuit and reset the failure count. require 'circuit_breaker' class TestService include CircuitBreaker def call_remote_service() ... circuit_method :call_remote_service # Optional circuit_handler do |handler| handler.logger = Logger.new(STDOUT) handler.failure_threshold = 5 handler.failure_timeout = 5 end # Optional circuit_handler_class MyCustomCircuitHandler end
Often I want to evaluate what changed on an object after it has saved and has been transactionally committed. One use case is enqueuenig a background job when a specific attribute changed. Typically you should wait until after_commit to enqueue the job. This ensures that your workers will have access to the updates that were being made inside the transaction. This is a problem because ActiveRecord forgets the changes after_save. This gem keeps those changes around for later inspection and evaluation.
CircuitBreaker is a relatively simple Ruby mixin that will wrap a call to a given service in a circuit breaker pattern. The circuit starts off "closed" meaning that all calls will go through. However, consecutive failures are recorded and after a threshold is reached, the circuit will "trip", setting the circuit into an "open" state. In an "open" state, every call to the service will fail by raising CircuitBrokenException. The circuit will remain in an "open" state until the failure timeout has elapsed. After the failure_timeout has elapsed, the circuit will go into a "half open" state and the call will go through. A failure will immediately pop the circuit open again, and a success will close the circuit and reset the failure count. require 'circuit_breaker' class TestService include CircuitBreaker def call_remote_service() ... circuit_method :call_remote_service # Optional circuit_handler do |handler| handler.logger = Logger.new(STDOUT) handler.failure_threshold = 5 handler.failure_timeout = 5 end # Optional circuit_handler_class MyCustomCircuitHandler end
abstract_feature_branch is a Ruby gem that provides a unique variation on the Branch by Abstraction Pattern by Paul Hammant and the Feature Toggles Pattern by Martin Fowler to enhance team productivity and improve software fault tolerance. It provides the ability to wrap blocks of code with an abstract feature branch name, and then specify in a configuration file which features to be switched on or off. The goal is to build out upcoming features in the same source code repository branch (i.e. Continuous Integration and Trunk-Based Development), regardless of whether all are completed by the next release date or not, thus increasing team productivity by preventing integration delays. Developers then disable in-progress features until they are ready to be switched on in production, yet enable them locally and in staging environments for in-progress testing. This gives developers the added benefit of being able to switch a feature off after release should big problems arise for a high risk feature. abstract_feature_branch additionally supports Domain Driven Design's pattern of Bounded Contexts by allowing developers to configure context-specific feature files if needed. abstract_feature_branch is one of the simplest and most minimalistic "Feature Flags" Ruby gems out there as it enables you to get started very quickly by simply leveraging YAML files without having to set up a data store if you do not need it (albeit, you also have the option to use Redis as a very fast in-memory data store).
========================================================= FreeSWITCHeR Copyright (c) 2009 The Rubyists (Jayson Vaughn, Tj Vanderpoel, Michael Fellinger, Kevin Berry) Distributed under the terms of the MIT License. ========================================================== ABOUT ----- A ruby library for interacting with the "FreeSWITCH" (http://www.freeswitch.org) opensource telephony platform REQUIREMENTS ------------ * ruby (>= 1.8) * eventmachine (If you wish to use Outbound and Inbound listener) USAGE ----- An Outbound Event Listener Example that reads and returns DTMF input: -------------------------------------------------------------------- Simply just create a subclass of FSR::Listner::Outbound and all new calls/sessions will invoke the "session_initiated" callback method. <b>NOTE</b>: FSR uses blocks within the 'session_inititated' method to ensure that the next "freeswich command" is not executed until the previous "Freeswitch command" has finished. This is kicked off by "answer do" #!/usr/bin/ruby require 'fsr' require 'fsr/listener/outbound' class OutboundDemo < FSR::Listener::Outbound def session_initiated exten = @session.headers[:caller_caller_id_number] FSR::Log.info "*** Answering incoming call from #{exten}" answer do FSR::Log.info "***Reading DTMF from #{exten}" read("/home/freeswitch/freeswitch/sounds/music/8000/sweet.wav", 4, 10, "input", 7000) do |read_var| FSR::Log.info "***Success, grabbed #{read_var.strip} from #{exten}" # Tell the caller what they entered speak("Got the DTMF of: #{read_var}") do #Hangup the call hangup end end end end end FSR.start_oes! OutboundDemo, :port => 8084, :host => "127.0.0.1" An Inbound Event Socket Listener example using FreeSWITCHeR's hook system: -------------------------------------------------------------------------- #!/usr/bin/ruby require 'pp' require 'fsr' require "fsr/listener/inbound" # EXAMPLE 1 # This adds a hook on CHANNEL_CREATE events. You can also create a method to handle the event you're after. See the next example FSL::Inbound.add_event_hook(:CHANNEL_CREATE) {|event| FSR::Log.info "*** [#{event.content[:unique_id]}] Channel created - greetings from the hook!" } # EXAMPLE 2 # Define a method to handle CHANNEL_HANGUP events. def custom_channel_hangup_handler(event) FSR::Log.info "*** [#{event.content[:unique_id]}] Channel hangup. The event:" pp event end # This adds a hook for EXAMPLE 2 FSL::Inbound.add_event_hook(:CHANNEL_HANGUP) {|event| custom_channel_hangup_handler(event) } # Start FSR Inbound Listener FSR.start_ies!(FSL::Inbound, :host => "localhost", :port => 8021) An Inbound Event Socket Listener example using the on_event callback method instead of hooks: --------------------------------------------------------------------------------------------- #!/usr/bin/ruby require 'pp' require 'fsr' require "fsr/listener/inbound" class IesDemo < FSR::Listener::Inbound def on_event(event) pp event.headers pp event.content[:event_name] end end FSR.start_ies!(IesDemo, :host => "localhost", :port => 8021, :auth => "ClueCon") An example of using FSR::CommandSocket to originate a new call in irb: ---------------------------------------------------------------------- irb(main):001:0> require 'fsr' => true irb(main):002:0> FSR.load_all_commands => [:sofia, :originate] irb(main):003:0> sock = FSR::CommandSocket.new => #<FSR::CommandSocket:0xb7a89104 @server="127.0.0.1", @socket=#<TCPSocket:0xb7a8908c>, @port="8021", @auth="ClueCon"> irb(main):007:0> sock.originate(:target => 'sofia/gateway/carlos/8179395222', :endpoint => FSR::App::Bridge.new("user/bougyman")).run => {"Job-UUID"=>"732075a4-7dd5-4258-b124-6284a82a5ae7", "body"=>"", "Content-Type"=>"command/reply", "Reply-Text"=>"+OK Job-UUID: 732075a4-7dd5-4258-b124-6284a82a5ae7"} SUPPORT ------- Home page at http://code.rubyists.com/projects/fs #rubyists on FreeNode
== Terminal UIs, the Ruby Way RatatuiRuby[https://rubygems.org/gems/ratatui_ruby] is a RubyGem built on Ratatui[https://ratatui.rs], a leading TUI library written in Rust[https://rust-lang.org]. You get native performance with the joy of Ruby. gem install ratatui_ruby {rdoc-image:https://ratatui-ruby.dev/hero.gif}[https://www.ratatui-ruby.dev/docs/v0.10/examples/app_cli_rich_moments/README_md.html] === Rich Moments Add a spinner, a progress bar, or an inline menu to your CLI script. No full-screen takeover. Your terminal history stays intact. ==== Inline Viewports Standard TUIs erase themselves on exit. Your carefully formatted CLI output disappears. Users lose their scrollback. <b>Inline viewports</b> solve this. They occupy a fixed number of lines, render rich UI, then leave the output in place when done. Perfect for spinners, menus, progress indicators—any brief moment of richness. require "ratatui_ruby" RatatuiRuby.run(viewport: :inline, height: 1) do |tui| until connected? status = tui.paragraph(text: "\#{spin} Connecting...") tui.draw { |frame| frame.render_widget(status, frame.area) } end end === Build Something Real Full-screen applications with {keyboard and mouse input}[https://www.ratatui-ruby.dev/docs/v0.10/examples/app_all_events/README_md.html]. The managed loop sets up the terminal and restores it on exit, even after crashes. RatatuiRuby.run do |tui| loop do tui.draw do |frame| frame.render_widget( tui.paragraph(text: "Hello, RatatuiRuby!", alignment: :center), frame.area ) end case tui.poll_event in { type: :key, code: "q" } then break else nil end end end ==== Widgets included: [Layout] {Block}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_block/README_md.html], {Center}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_center/README_md.html], {Clear (Popup, Modal)}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_popup/README_md.html], {Layout (Split, Grid)}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_layout_split/README_md.html], {Overlay}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_overlay/README_md.html] [Data] {Bar Chart}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_barchart/README_md.html], {Chart}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_chart/README_md.html], {Gauge}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_gauge/README_md.html], {Line Gauge}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_line_gauge/README_md.html], {Sparkline}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_sparkline/README_md.html], {Table}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_table/README_md.html] [Text] {Cell}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_cell/README_md.html], {List}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_list/README_md.html], {Rich Text (Line, Span)}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_rich_text/README_md.html], {Scrollbar (Scroll)}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_scrollbar/README_md.html], {Tabs}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_tabs/README_md.html] [Graphics] {Calendar}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_calendar/README_md.html], {Canvas}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_canvas/README_md.html], {Map (World Map)}[https://www.ratatui-ruby.dev/docs/v0.10/examples/widget_map/README_md.html] Need something else? {Build custom widgets}[https://www.ratatui-ruby.dev/docs/v0.10/doc/concepts/custom_widgets_md.html] in Ruby! --- === Testing Built In TUI testing is tedious. You need a headless terminal, event injection, snapshot comparisons, and style assertions. RatatuiRuby bundles all of it. require "ratatui_ruby/test_helper" class TestColorPicker < Minitest::Test include RatatuiRuby::TestHelper def test_swatch_widget with_test_terminal(10, 3) do RatatuiRuby.draw do |frame| frame.render_widget(Swatch.new(:red), frame.area) end assert_cell_style 2, 1, char: "█", bg: :red end end end ==== What's inside: - <b>Headless terminal</b> — No real TTY needed - <b>Snapshots</b> — Plain text and rich (ANSI colors) - <b>Event injection</b> — Keys, mouse, paste, resize - <b>Style assertions</b> — Color, bold, underline at any cell - <b>Test doubles</b> — Mock frames and stub rects - <b>UPDATE_SNAPSHOTS=1</b> — Regenerate baselines in one command --- ==== Inline Menu Example require "ratatui_ruby" # This example renders an inline menu. Arrow keys select, enter confirms. # The menu appears in-place, preserving scrollback. When the user chooses, # the TUI closes and the script continues with the selected value. class RadioMenu CHOICES = ["Production", "Staging", "Development"] # ASCII strings are universally supported. PREFIXES = { active: "●", inactive: "○" } # Some terminals may not support Unicode. CONTROLS = "↑/↓: Select | Enter: Choose | Ctrl+C: Cancel" # Let users know what keys you handle. TITLES = ["Select Environment", # The default title position is top left. { content: CONTROLS, # Multiple titles can save space. position: :bottom, # Titles go on the top or bottom, alignment: :right }] # aligned left, right, or center def call # This method blocks until a choice is made. RatatuiRuby.run(viewport: :inline, height: 5) do |tui| # RatauiRuby.run manages the terminal. @tui = tui # The TUI instance is safe to store. show_menu until chosen? # You can use any loop keyword you like. end # `run` won't return until your block does, RadioMenu::CHOICES[@choice] # so you can use it synchronously. end # Classes like RadioMenu are convenient for private # CLI authors to offer "rich moments." def show_menu = @tui.draw do |frame| # RatatuiRuby gives you low-level access. widget = @tui.paragraph( # But the TUI facade makes it easy to use. text: menu_items, # Text can be spans, lines, or paragraphs. block: @tui.block(borders: :all, titles: TITLES) # Blocks give you boxes and titles, and hold ) # one or more widgets. We only use one here, frame.render_widget(widget, frame.area) # but "area" lets you compose sub-views. end def chosen? # You are responsible for handling input. interaction = @tui.poll_event # Every frame, you receive an event object: return choose if interaction.enter? # Key, Mouse, Resize, Paste, FocusGained, # FocusLost, or None objects. They come with move_by(-1) if interaction.up? # predicates, support pattern matching, and move_by(1) if interaction.down? # can be inspected for properties directly. quit! if interaction.ctrl_c? # Your application must handle every input, false # even interrupts and other exit patterns. end def choose # Here, the loop is about to exit, and the prepare_next_line # block will return. The inline viewport @choice # will be torn down and the terminal will end # be restored, but you are responsible for # positioning the cursor. def prepare_next_line # To ensure the next output is on a new area = @tui.viewport_area # line, query the viewport area and move RatatuiRuby.cursor_position = [0, area.y + area.height] # the cursor to the start of the last line. puts # Then print a newline. end def quit! # All of your familiar Ruby control flow prepare_next_line # keywords work as expected, so we can exit 0 # use them to leave the TUI. end def move_by(line_count) # You are in full control of your UX, so @choice = (@choice + line_count) % CHOICES.size # you can implement any logic you need: end # Would you "wrap around" here, or not? # def menu_items = CHOICES.map.with_index do |choice, i| # Notably, RatatuiRuby has no concept of "\#{prefix_for(i)} \#{choice}" # "menus" or "radio buttons". You are in end # full control, but it also means you must def prefix_for(choice_index) # implement the logic yourself. For larger return PREFIXES[:active] if choice_index == @choice # applications, consider using Rooibos, PREFIXES[:inactive] # an MVU framework built with RatatuiRuby. end # Or, use the upcoming ratatui-ruby-kit, # our object-oriented component library. def initialize = @choice = 0 # However, those are both optional, and end # designed for full-screen Terminal UIs. # RatatuiRuby will always give you the most choice = RadioMenu.new.call # control, and is enough for "rich CLI puts "You chose \#{choice}!" # moments" like this one. --- === Full App Solutions RatatuiRuby renders. For complex applications, add a framework that manages state and composition. ==== Rooibos[https://www.rooibos.run] (Framework) Model-View-Update architecture. Inspired by Elm, Bubble Tea, and React + Redux. Your UI is a pure function of state. - Functional programming with MVU - Commands work off the main thread - Messages, not callbacks, drive updates ==== {Kit}[https://sr.ht/~kerrick/ratatui_ruby/#chapter-3-the-object-path--kit] (Coming Soon) Component-based architecture. Encapsulate state, input handling, and rendering in reusable pieces. - OOP with stateful components - Separate UI state from domain logic - Built-in focus management & click handling Both use the same widget library and rendering engine. Pick the paradigm that fits your brain. --- === Why RatatuiRuby? Ruby deserves world-class terminal user interfaces. TUI developers deserve a world-class language. RatatuiRuby wraps Rust's Ratatui via native extension. The Rust library handles rendering. Your Ruby code handles design. >>> "Text UIs are seeing a renaissance with many new TUI libraries popping up. The Ratatui bindings have proven to be full featured and stable." — {Mike Perham}[https://www.mikeperham.com/], creator of Sidekiq[https://sidekiq.org/] and Faktory[https://contribsys.com/faktory/] ==== Why Rust? Why Ruby? Rust excels at low-level rendering. Ruby excels at expressing domain logic and UI. RatatuiRuby puts each language where it performs best. ==== Versus CharmRuby CharmRuby[https://charm-ruby.dev/] wraps Charm's Go libraries. Both projects give Ruby developers TUI options. [Integration] CharmRuby: Two runtimes, one process. RatatuiRuby: Native extension in Rust. [Runtime] CharmRuby: Go + Ruby (competing). RatatuiRuby: Ruby (Rust has no runtime). [Memory] CharmRuby: Two uncoordinated GCs. RatatuiRuby: One Garbage Collector. [Style] CharmRuby: The Elm Architecture (TEA). RatatuiRuby: TEA, OOP, or Imperative. --- === Links [Get Started] {Quickstart}[https://www.ratatui-ruby.dev/docs/v0.10/doc/getting_started/quickstart_md.html], {Examples}[https://www.ratatui-ruby.dev/docs/v0.10/examples/app_cli_rich_moments/README_md.html], {API Reference}[https://www.ratatui-ruby.dev/docs/v0.10/], {Guides}[https://www.ratatui-ruby.dev/docs/v0.10/doc/index_md.html] [Ecosystem] Rooibos[https://www.rooibos.run], {Kit}[https://sr.ht/~kerrick/ratatui_ruby/#chapter-3-the-object-path--kit] (Planned), {Framework}[https://sr.ht/~kerrick/ratatui_ruby/#chapter-5-the-framework] (Planned), {UI Widgets}[https://sr.ht/~kerrick/ratatui_ruby/#chapter-6-licensing] (Planned) [Community] {Forum}[https://forum.setdef.com/c/ratatui-ruby/6], {Announcements}[https://forum.setdef.com/tags/c/ratatui-ruby/6/announcement], {Discussion}[https://forum.setdef.com/tags/c/ratatui-ruby/6/discussion], {Bug Tracker}[https://forum.setdef.com/tags/c/ratatui-ruby/6/bug] [Contribute] {Contributing Guide}[https://man.sr.ht/~kerrick/ratatui_ruby/contributing.md], {Code of Conduct}[https://man.sr.ht/~kerrick/ratatui_ruby/code_of_conduct.md], {Project History}[https://man.sr.ht/~kerrick/ratatui_ruby/history/index.md], {Pull Requests}[https://forum.setdef.com/tags/c/ratatui-ruby/6/patch] --- [Website] https://www.ratatui-ruby.dev [Source] https://github.com/setdef/RatatuiRuby [RubyGems] https://rubygems.org/gems/ratatui_ruby [Upstream] https://ratatui.rs [Build Status] https://builds.sr.ht/~kerrick/ratatui_ruby © 2026 Kerrick Long · Library: LGPL-3.0-or-later · Website: CC-BY-NC-ND-4.0 · Snippets: MIT-0
# Trope **[Documentation][docs] - [Gem][gems] - [Source][source]** Prototyping language that transcompiles into pure Ruby code. 1. Build your concept in Trope. 2. Write specs. 3. Transcompile into Ruby. 4. Destroy Trope files. 5. Red, green, refactor. ## Install > NOTE: Trope is not released yet, the gem is just a placeholder. ### Bundler: `gem 'trope'` ### RubyGems: `gem install trope` ## Example Create `library.trope`: ```ruby object Book attr name <String> -!wd 'Unnamed book' attr isbn <Integer> -w attr library <Library> -w do before write { @library.books.delete(self) unless @library.nil? } after write { @library.books.push(self) unless @library.books.include?(self) } end end object Library attr books <Array> -d Array.new meth add_book do |attributes_or_book <Hash, Book>| book = attributes_or_book.is_a?(Book) ? attributes_or_book : Book.new(attributes_or_book) book.library = self @books << book end end ``` Now generate the Ruby code: ```sh $ trope compile libary.trope ``` Those 15 lines will be transcompiled into the following pure Ruby code in `library.rb`: ```ruby class Book class Error < RuntimeError; end class InvalidAttributesError < Error def to_s 'attributes must be a Hash or respond to #to_h' end end class MissingAttributeError < Error def initialize(attr_name, attr_class) @name, @class = attr_name.to_s, attr_class.to_s end def to_s "attribute '#@name' does not exist for #@class" end end class MissingNameError < Error def to_s 'name cannot be nil' end end class InvalidNameError < Error def to_s 'name must be an instance of String or respond to :to_s' end end class InvalidIsbnError < Error def to_s 'isbn must be an instance of Integer or respond to :to_i' end end class MissingLibraryError < Error def to_s 'library cannot be nil' end end class InvalidLibraryError < Error def to_s 'library must be an instance of Library' end end attr_reader *(@@_attributes = [:name, :isbn, :library]) def initialize(attributes={}) raise InvalidAttributesError unless attributes.is_a?(Hash) || attributes.respond_to?(:to_h) attributes = attributes.to_h unless attributes.is_a?(Hash) raise MissingNameError if attributes.has_key?(:name) && attributes[:name].nil? attributes[:name] = 'Unnamed book' unless attributes.has_key?(:name) attributes.each do |name, value| raise MissingAttributeError.new(name, self.class) unless @@_attributes.include?(name.to_sym) setter_method = "#{name}=" setter_method = "_#{setter_method}" unless self.class.method_defined?(setter_method) send(setter_method, value) end end def name=(value) raise MissingNameError if value.nil? raise InvalidNameError unless value.is_a?(String) || value.respond_to?(:to_s) value = value.to_i unless value.is_a?(Integer) @name = value end def isbn=(value) raise InvalidIsbnError unless value.is_a?(Integer) || value.respond_to?(:to_i) value = value.to_i unless value.is_a?(Integer) @isbn = value end def library=(value) raise InvalidLibraryError unless value.is_a?(Library) || value.nil? @library.books.delete(self) unless @library.nil? @library = value @library.books.push(self) unless @library.books.include?(self) @library end end class Library class Error < RuntimeError; end class InvalidAttributesError < Error def to_s 'attributes must be an instance of Hash or respond to #to_h' end end class MissingAttributeError < Error def initialize(attr_name, attr_class) @name, @class = attr_name.to_s, attr_class.to_s end def to_s "attribute '#@name' does not exist for #@class" end end class InvalidBooksError < Error def to_s 'books must be an instance of Array or respond to #to_a' end end attr_reader *(@@_attributes = [:books]) def initialize(attributes={}) raise InvalidAttributesError unless attributes.is_a?(Hash) || attributes.respond_to?(:to_h) attributes = attributes.to_h unless attributes.is_a?(Hash) attributes[:books] = Array.new unless attributes.has_key?(:books) attributes.each do |name, value| raise MissingAttributeError.new(name, self.class) unless @@_attributes.include?(name.to_sym) setter_method = "#{name}=" setter_method = "_#{setter_method}" unless self.class.method_defined?(setter_method) send(setter_method, value) end end def add_book(attributes_or_book={}) raise InvalidAttributesError unless attributes_or_book.is_a?(Hash) || attributes_or_book.respond_to?(:to_h) || attributes_or_book.is_a?(Book) attributes_or_book = attributes_or_book.to_h unless attributes_or_book.is_a?(Hash) || attributes_or_book.is_a?(Book) book = attributes_or_book.is_a?(Book) ? attributes_or_book : Book.new(attributes_or_book) book.library = self @books << book end protected def _books=(value) raise InvalidBooksError unless value.is_a?(Array) || value.respond_to?(:to_a) value = value.to_a unless value.is_a?(Array) @books = value end end ``` Using the transcompiled Ruby code will produce the expected results: ```ruby p library = Library.new # => #<Library:0x007fc55c0ce418 @books=[]> p library.add_book name: 'Book 1', isbn: 1 # => [#<Book:0x007fc55c0cde78 @name=0, @isbn=1, @library=#<Library:0x007fc55c0ce418 @books=[...]>>] p library # => #<Library:0x007fc55c0ce418 @books=[#<Book:0x007fc55c0cde78 @name=0, @isbn=1, @library=#<Library:0x007fc55c0ce418 ...>>]> p library.books.first # => #<Book:0x007fc55c0cde78 @name=0, @isbn=1, @library=#<Library:0x007fc55c0ce418 @books=[#<Book:0x007fc55c0cde78 ...>]>> p library.books.first.isbn = nil # => nil p library.books.first.name = nil # => Book::MissingNameError: name cannot be nil p library.books.first.library = nil # => Book::MissingLibraryError: library cannot be nil p library.books.first.isbn = ['array'] # => Book::InvalidIsbnError: isbn must be an instance of Integer or respond to :to_i p library = Library.new(books: 123) # => Library::InvalidBooksError: books must be an instance of Array or respond to #to_a ``` ### Breakdown ```ruby object Book attr name <String> -!wd 'Unnamed book' end ``` This says that I have an object `Book` that has an attribute `name` (`attr name`) that must either be an instance/subclass of `String` or be able to convert to an instance of `String` using `#to_s` (`<String>`). It is a required attribute that can never be set to nil (`!`), has a writer method (`w`), and defaults to 'Unnamed book'. The minus sign (`-`) indicates a 'switch' or 'option', must like most *nix command line programs. The example could also have been written like so: ```ruby object Book attr name <String> -! -w -d 'Unnamed book' end ``` The above examples will transcompile into the following: ```ruby class Book class Error < RuntimeError; end class InvalidAttributesError < Error def to_s 'attributes must be a Hash or respond to #to_h' end end class MissingAttributeError < Error def initialize(attr_name, attr_class) @name, @class = attr_name.to_s, attr_class.to_s end def to_s "attribute '#@name' does not exist for #@class" end end class MissingNameError < Error def to_s 'name cannot be nil' end end class InvalidNameError < Error def to_s 'name must be an instance of String or respond to :to_s' end end attr_reader *(@@_attributes = [:name]) @@_required_attributes = [:name] def initialize(attributes={}) raise InvalidAttributesError unless attributes.is_a?(Hash) || attributes.respond_to?(:to_h) attributes = attributes.to_h unless attributes.is_a?(Hash) raise MissingNameError if attributes.has_key?(:name) && attributes[:name].nil? attributes[:name] = 'Unnamed book' unless attributes.has_key?(:name) attributes.each do |name, value| raise MissingAttributeError.new(name, self.class) unless @@_attributes.include?(name.to_sym) setter_method = "#{name}=" setter_method = "_#{setter_method}" unless self.class.method_defined?(setter_method) send(setter_method, value) end end def name=(value) raise MissingNameError if value.nil? raise InvalidNameError unless value.is_a?(String) || value.respond_to?(:to_s) value = value.to_i unless value.is_a?(Integer) @name = value end end ``` ## Contributing * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it * Fork the project * Start a feature/bugfix branch * Commit and push until you are happy with your contribution * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally. * Please try not to mess with the Rakefile, VERSION, or Gemfile. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it. ## Copyright Copyright © 2012 Ryan Scott Lewis <ryan@rynet.us>. The MIT License (MIT) - See LICENSE for further details. [docs]: http://rubydoc.info/gems/trope/frames [gems]: https://rubygems.org/gems/trope [source]: https://github.com/RyanScottLewis/trope
No description provided.
No description provided.
No description provided.
No description provided.
No description provided.
No description provided.
No description provided.
No description provided.