Foundation Gem
The Foundation gem is part of the Bridgetown Ruby API, offering some handy helpers for strings, hashes, class hierarchies, and more. Over time we intend to migrate reusable, utility-like patterns elsewhere in the Bridgetown codebase to this Foundation gem. We hope this can prove useful even on non-Bridgetown projects (you could bundle add bridgetown-foundation
directly).
A number of the features in Foundation are provided via a Ruby feature called refinements. This may not be so well-known to some developers, so we’ll explain how it works.
Table of Contents #
How Refinements Work (and How to Add Your Own) #
A refinement is like a mixin for an existing Ruby class, but instead of monkey-patching that class in a global way, it only patches within a lexical scope. When you activate a refinement via using
, you have access to that refinement only within that scope (which could be a module or class, or an entire Ruby file).
The Foundation refinements are all bundled together within a single module, so to “opt-in” with your code, call using Bridgetown::Refinements
.
Here’s an example:
using Bridgetown::Refinements
"abc".within? %w[xyz abc] # true
The within?
method is only available in a scope where the refinements are activated. Here’s an example of scoping to a single class:
class TryWithin
using Bridgetown::Refinements
def initialize(str) = @str = str
def is_within?(arr) = @str.within? arr
end
TryWithin.new("abc").is_within?(%w[xyz abc]) # true
"abc".within? %w[xyz abc] # raises NoMethodError
There are certain contexts in which it’s not possible use using
, for example in Roda server blocks or Ruby templates like .erb
. In those cases, you can “refine an object” directly with the refine
helper and use refinement methods on that:
refine("abc").within? %w[xyz abc] # true
There’s also a Bridgetown.refine(obj)
method you can call if the helper is not available in a certain execution context. Otherwise, you can write include Bridgetown::Refinements::Helper
—but most likely, if you’re in a position to do that you can simply go with the normal using
way of activating refinements.
Under the hood, a “refined object” is a SimpleDelegator
Ruby class which wraps the original object, so using the refined object is indistinguishable from using the original object (other than refinement methods are available).
Foundation makes it easy to add your own refinements! There’s only a small amount of boilerplate required:
module Adding
refine Numeric do
def add(input)
self + input
end
end
end
Bridgetown.add_refinement(Adding) do
# This is required boilerplate:
using Bridgetown::Refinements
def method_missing(...) = __getobj__.send(...) # rubocop:disable Style
end
and then:
class AddNumbers
using Bridgetown::Refinements
def initialize(num) = @num = num
def together(new_num) = @num.add new_num
end
AddNumbers.new(10).together(15) # 25
You could put this in a config file or in a plugin gem, whatever makes sense for your use case.
Bridgetown::Refinements
also automatically includes the HashWithDotAccess refinement which adds an as_dots
method to Ruby Hash
to convert a standard hash to one with string/symbol/method key access.
Foundation API #
The following methods are accessed via refinements unless otherwise noted.
Object #
within?
This method lets you check if the receiver is “within” the other object. In most cases, this check is accomplished via the include?
method…aka, 10.within? [5, 10]
would return true
as [5, 10].include? 10
is true. String/String comparison are case-insensitive, so "FOO".within?("foobar")
is true.
However, for certain comparison types: Module/Class, Hash, and Set, the lesser-than (<
) operator is used instead. This is so you can check BigDecimal.within? Numeric
, {easy_as: 123}.within?({indeed: "it's true", easy_as: 123})
, and Set[:b, :c].within? Set[:a, :b, :c, :d]
(under the hood Set evaluates using proper_subset?
).
For Array/Array comparisons, a difference (&&
) is checked, so [1,2].within? [3,2,1]
is true, but [1,2].within? [2,3]
is false.
For Range, the cover?
method is used, so (3..4).within?(2..6)
is true, but (1..4).within?(2..6)
is false.
Class #
descendants
Monkey-patch. This class method lets you retrieve a flattened array of all of a class’ descendants (and their descendants if applicable, etc.). There are two stipulations for a class to be included: it has to have a name (so no anonymous classes), and it has to be available in the global namespace (aka accessible via Kernel.const_get
).
String #
indent
/ indent!
This lets you indent a string by a certain number of spaces. "it\n is indented\n\nnow".indent(2)
would result in " it\n is indented\n\n now"
(each line now starts with two spaces). indent!
modifies a string in-place.
starts_with?
/ ends_with?
Monkey-patch. Aliases for Ruby’s native start_with?
and end_with?
methods.
questionable?
This returns a Bridgetown::Foundation::QuestionableString
copy of the string, now with the ability to use a question method. "test".questionable.test?
will return true, whereas "test".questionable.nope?
will return false. This is used by Bridgetown.env
so you can call Bridgetown.env.production?
.
colors (red
, cyan
, etc.)
Monkey-patch. You can use ANSI color methods on strings as part of colorized terminal output, e.g., puts "Error".red
. These colors are provided by Foundation’s Ansi package. If your output somehow gets “stuck” in a color, you can also call reset_ansi
on a string.
Module #
nested_within?
This lets you check if a particular module or class is nested inside of a namespace. For example, Bridgetown::Resource::Base.nested_within? Bridgetown::Resource
is true, but Bridgetown::Resource::Base.nested_within? Bridgetown::Model
is false.
nested_parents
/ nested_parent
The first method will return an array of parent classes/modules within the namespace hierarchy. Bridgetown::Resource::Base.nested_parents
returns [Bridgetown::Resource, Bridgetown]
. And Bridgetown::Resource::Base.nested_parent
returns Bridgetown::Resource
.
nested_name
This returns the string identifier of the class/module name without its nested parents. For example: Bridgetown::Resource::Base.name
would return "Bridgetown::Resource::Base"
, but Bridgetown::Resource::Base.nested_name
returns "Base"
.
Packages #
A few of the features in Foundation are provided in the form of Inclusive packages. This is a “syntactic sugar” way of accessing utility methods from modules without using mixins directly. You can load in a package by adding include Inclusive
in a Ruby class and then defining a method for accessing one or more packages:
packages def some_package = [Some::Available::Package]
def later_on
some_package.method_here(...)
end
Ansi
This is used for providing a way to output strings with color in a terminal. (See the docs above on String
.) View code here.
PidTracker
This is used for managing pid files in a multiprocess setting. View code here.
SafeTranslations
This package is used to manage the display of translations which include HTML (although it’s not inherently HTML-specific), marking them as “safe”. Sample usage:
class TranslateWithHTML
include Inclusive
packages def translate_package = [Bridgetown::Foundation::Packages::SafeTranslations]
def translate(key, **options)
escaper = ->(input) { input.to_s.encode(xml: :attr).gsub(%r{\A"|"\Z}, "") }
translate_package.translate(key, escaper, :html_safe, **options)
end
end
# This will escape the value in the provided hash
TranslateWithHTML.new.translate("key.path.to.entry", { name: "Jared White <script>// XSS</script>" })