Blog Me think, why waste time say lot word, when few word do trick

Ruby (3.3) on Rails (1.0).

Rails 8.0 has recently branched out on Github, and I found myself curious about the feasibility of running Rails 1.0 on the latest Ruby version. While I was pretty sure it wouldn’t work right off the bat, I wondered: how many modifications would be necessary to at least reach the iconic “Welcome aboard! You’re riding the Rails!” screen?

So, let’s dive in. My starting point was this Gemfile:

source "https://rubygems.org"
gem "rails", "1.0.0"

Since I knew that it would be required to make some changes to Rails gems I install it with bundle install --local. This would allow for easier modifications later on. My first attempt was running bundle exec rails --help:

/activesupport-1.2.5/lib/active_support/inflector.rb:157: syntax error, unexpected ':', expecting `then' or ',' or ';' or '\n' (SyntaxError)
        when 1: "#{number}st"
              ^

Indeed, older Ruby versions allowed the use of a colon in place of then in case/when expressions. This syntax is no longer supported, so I updated it across the codebase to match the current Ruby syntax.

Ok let’s try again with bundle exec rails --help:

cannot load such file -- parsedate (LoadError)

Oh yeah, parsedate lib that was shipped with Ruby 1.8 is not longer there. It was used to parse date strings, like so:

ParseDate.parsedate "Tuesday, July 5th, 2007, 18:35:20 UTC"
# => [2007, 7, 5, 18, 35, 20, "UTC", 2]

Not sure why it was returning an array but ok. Now I can replace it with DateTime.parse that returns DateTime object. So I’ve fixed that and tried to run it again. Ugh, another error:

rails-1.0.0/lib/rails_generator/options.rb:124: syntax error, unexpected '[', expecting '|' (SyntaxError)
...make any changes.') { |options[:pretend]| }

That’s some weird syntax. Turns out you could assign something to a hash right inside the block variable thing:

opt.on('-p', '--pretend', 'Run but do not make any changes.') { |options[:pretend]| }

meaning that whatever you pass as -p option will end up being assigned to options[:pretend]. Basically it equals to

opt.on('-p', '--pretend', 'Run but do not make any changes.') { |o| options[:pretend] = o }

Alrighty then. Rerun bundle exec rails --help:

no implicit conversion of Enumerator into Array

And it’s without any stacktrace. Great. Looks like something catches all the errors and just prints them. After some investigation I’ve found this code:

def cache
  @cache ||= sources.inject([]) { |cache, source| cache + source.map }
end

In Ruby 1.8 [].map would return an array but now it returns Enumerator object and you can concat an Array with Enumerator hence the error:

irb(main):001> [] + [].map
(irb):1:in `+': no implicit conversion of Enumerator into Array (TypeError)

It’s an easy fix though. Let’s just call .to_a on the source:

def cache
  @cache ||= sources.inject([]) { |cache, source| cache + source.to_a }
end

Are we getting there?

bundle exec rails --help

`load': cannot load such file -- config.rb (LoadError)

The code in question is

require 'rbconfig'

DEFAULT_SHEBANG = File.join(Config::CONFIG['bindir'],
                            Config::CONFIG['ruby_install_name'])

Makes sense, in old Ruby RbConfig could be referenced with Config constant and now it’s only RbConfig. Fixed. Does it work now?

bundle exec rails --help

Usage: /vendor/bundle/ruby/3.3.0/bin/rails /path/to/your/app [options]

Great Scott! It works! Let’s try to generate a new app:

bundle exec rails blog

undefined method `exists?' for class File

Dammit, File.exists?/FileTest.exists? were removed in Ruby 1.9. Let’s replace it with File.exist?/FileTest.exist? and try again:

bundle exec rails blog
      create
      create  app/controllers
      create  app/helpers
      create  app/models
      create  app/views/layouts
      create  config/environments
      create  components
      create  db
      create  doc
      create  lib
      create  lib/tasks
      create  log
      create  public/images
      create  public/javascripts
      create  public/stylesheets
      create  script/performance
      create  script/process
      create  test/fixtures
      create  test/functional
      create  test/mocks/development
      create  test/mocks/test
      create  test/unit
      create  vendor
      create  vendor/plugins
      create  Rakefile
      create  README
      create  app/controllers/application.rb
Cannot create Binding object for non-Ruby caller

Success! Is it though? It has generated an app but all the files are empty. And if you have a sharp eye you’ll have noticed this error:

Cannot create Binding object for non-Ruby caller

Again, no stacktracks, just some plain error. It took me some time to locate that line of code that was failing with such error but here it is:

file(relative_source, relative_destination, template_options) do |file|
  # Evaluate any assignments in a temporary, throwaway binding.
  vars = template_options[:assigns] || {}
  b = binding
  vars.each { |k,v| eval "#{k} = vars[:#{k}] || vars['#{k}']", b }

  ...
end

Believe me, I really tried to figure you what this error was about given that it’s obviously a Ruby caller but luck wasn’t there for me. Then I tried to replace binding with Kernel.binding and it worked… If you know what’s going on here please let me know! Maybe Rails were redefining binding somewhere?

Alright, let’s proceed:

bundle exec rails blog
      create
      create  app/controllers
      create  app/helpers
      create  app/models
      ...

Finally! The app is generated, files are not empty. We’re close, I can smell it! Let’s try to start it:

 bundle exec ruby script/server -p 3001

`require': cannot load such file -- script/../config/boot (LoadError)

Sure, just some random load error. Turns out in Ruby 1.8 you could require a file with relative to current file path and now you can’t:

# In Ruby 1.8
require File.dirname(__FILE__) + '/../config/boot'

# In Ruby 1.9+
require_relative '../config/boot'

With this one fixed we can try it one more time:

bundle exec ruby script/server -p 3001

=> Booting WEBrick...

activerecord-1.13.2/lib/active_record/base.rb:708: circular argument reference - table_name (SyntaxError)

Ok this one should be trivial. The code in question:

def class_name(table_name = table_name)
  ...
end

I’m a bit surprised that this was working in Ruby 1.8. The error is pretty self-explanatory so I just renamed default argument value and continued with my life:

bundle exec ruby script/server -p 3001

=> Booting WEBrick...

`load': cannot load such file -- big_decimal.rb (LoadError)
Did you mean?  bigdecimal

Right, bigdecimal is not required by default now. I’ll spare you some time and say that there was the same issue with rexml and net-smtp gems (net-smtp is not even part of Ruby anymore and I had to add it to the Gemfile). So I fixed it and tried again:

bundle exec ruby script/server -p 3001

=> Booting WEBrick...
actionmailer-1.1.5/lib/action_mailer/quoting.rb:22: invalid multibyte escape: /[\000-\011\013\014\016-\037\177-\377]/ (SyntaxError)

Oh yeah, Ruby 1.9 did a lot of changes to string encoding (you can read more on this here) and now using raw bytes doesn’t work anymore. So I believe we can convert it to /[\x00-\x11\x13\x14\x16-\x1F\x7F-\xFF]/n and it’s going to work? Well, at least the issue was fixed (yeah, yeah who cares if we’ve just introduced some vulnerability? I don’t):

bundle exec ruby script/server -p 3001

=> Booting WEBrick...
`require': cannot load such file -- soap/rpc/driver (LoadError)

Oh ffs. It comes from action_web_service (god knows what was that back in the days) and lucky us we can remove this Rails component from our stack with this config:

# Skip frameworks you're not going to use
config.frameworks -= [ :action_web_service ]

image

bundle exec ruby script/server -p 3001

=> Booting WEBrick...
`require': cannot load such file -- soap/rpc/driver (LoadError)

rails-1.0.0/lib/rails_info.rb:8: syntax error, unexpected ')' (SyntaxError)
        map {|(name, )| name}

Cool cool, you could do .map { |(param1, )| param1 } in Ruby 1.8 to ommit the second block param. You can actually do it in Ruby 3.3 but you don’t need this extra comma:

{a: 1, b: 2, c: 3}.map { |a, | a } # => [:a, :b, :c]
# or
{a: 1, b: 2, c: 3}.map { |(a)| a } # => [:a, :b, :c]
# without
{a: 1, b: 2, c: 3}.map { |a| a } # =>[[:a, 1], [:b, 2], [:c, 3]]

And one more time…

bundle exec ruby script/server -p 3001

=> Booting WEBrick...
[2024-01-15 21:23:25] INFO  WEBrick 1.8.1
[2024-01-15 21:23:25] INFO  ruby 3.3.0 (2023-12-25) [arm64-darwin23]
[2024-01-15 21:23:25] INFO  WEBrick::HTTPServer#start: pid=98161 port=3001

Oh my God, we did it!

Screenshot 2024-01-15 at 21 24 54

If for some reason you want to check the code here it is: https://github.com/nashby/rails-from-the-past

I’m pretty sure there are way more issues to fix to make it work properly but I’m not going to do it. I’m just happy enough that I’ve got to the point where I can see this greeting screen! Fin.

And may Ruby be with you!

Ruby 3.0 changes how methods of subsclassed core classes work.

First of all you most likely shouldn’t inherit from Ruby’s core classes. But if you still want to do it, it’s better to know what was changed in Ruby 3.0.

Before Ruby 3.0 return values of core classes methods like e.g String#upcase or Array#rotate were inconsistent. Here what I mean:

class Foo < Array
end

foo = Foo.new
foo.rotate.class # => Array
class Bar < String
end

bar = Bar.new
bar.upcase.class # => Bar

As you can see in the first example Foo#rotate returns the instance of Array class and Bar#upcase returns the instance of Bar class. Personally, I’d prefer if Ruby would always return an instance of subclass but if you check this discussion it becomes pretty clear that it’s hard to make it work properly for all cases and it’s better to always return an instance of the original class.

So that’s what was done in Ruby 3.0! You can check these Pull Requests here and here with the changes. Now all String and Array methods always return instances of the original class, not the subclass.

The way I learned about this change was failing Enumerize build. Enumerize is a gem for enumerated attributes with I18n and ActiveRecord/Mongoid/MongoMapper/Sequel support. And a class that powers enumerated value is a subclass of String core class. So before you could just write something like

user.role.upcase # => 'ADMIN'

where the role is an enumerated field, you would get upcased value of Enumerize::Value instance but with Ruby 3.0 changes it becomes String instance. It means you can’t use any Enumerize methods on that value anymore. It’s not a big deal in terms of Enumerize usage since in most cases you don’t apply any String’s methods on it but would be good to make it not depend on the fact that Enumerize::Value is a subclass of Ruby’s String.

PostgreSQL’s IS DISTINCT FROM.

Sometimes you need to find records which field is not equal to given value or is NULL. And instead of writing something like:

User.where('email != ? OR email IS NULL', 'test@example.com')

you can use pretty handy PostgreSQL’s IS DISTINCT FROM comparison operator:

User.where('email IS DISTINCT FROM ?', 'test@example.com')

Rails API and Facebook login (featuring Doorkeeper).

When it comes to add Facebook login to Rails application there’re tons of guides and examples how to do it using Devise and Omniauth gems. That works well when you need to make it work with “classic” Rails application. But if you have Rails API application that powers your iOS/Android application it’s a bit different story.

Note: This post is only about backend part and it assumes that you know how to obtain Facebook access token (it’s described in this doc).

So that’s what we have:

  • Rails API application with protected endpoints that are accessible only by authenticated users (authenticated through Facebook).
  • iOS/Android client that does Facebook authentication and provides us users’s Facebook access token.

For user authentication we will use OAuth 2 protocol. We gonna use Doorkeeper gem to add OAuth 2 authorization support for our Rails API. By default Doorkeeper supports all common grants like Authorization code, Client credentials, Implicit grant, etc. But it doesn’t work for us. All that we going to get from mobile clients is users’s access token from Facebook. Here comes the Assertion grant. Since it’s not supported by Doorkeeper by default we need to use this doorkeeper-grants_assertion extension. That’s how the flow works:

 Relying
 Party                     Client                   Token Service
   |                          |                         |
   |                          |  1) Request Assertion   |
   |                          |------------------------>|
   |                          |                         |
   |                          |  2) Assertion           |
   |                          |<------------------------|
   |    3) Assertion          |                         |
   |<-------------------------|                         |
   |                          |                         |
   |    4) OK or Failure      |                         |
   |------------------------->|                         |
   |                          |                         |
   |                          |                         |

where Relying Party is our Rails API, Client is iOS/Android app, Token Service is Facebook.

Let’s configure Doorkeeper to support that flow. That’s how your config/initializers/doorkeeper.rb might look:

Doorkeeper.configure do
  # Getting resource owner (User) from the Facebook's access token.
  resource_owner_from_assertion do
    Authentication::Facebook.new(params[:assertion]).user!
  end

  # Allows only assertion flow.
  grant_flows %w(assertion)
end

Yeah, that simple. Authentication::Facebook class is just a simple service that fetches the user information using Facebook’s access token and tries to create a new user or return existing one based on user’s Facebook id. It might look like this:

require 'net/http'

module Authentication
  class Facebook
    FACEBOOK_URL = 'https://graph.facebook.com/v3.0/'
    FACEBOOK_USER_FIELDS = 'id,name,first_name,last_name'

    def initialize(access_token)
      @access_token = access_token
    end

    def user!
      return if user_data.blank? || facebook_id.blank?

      User.find_by(facebook_id: facebook_id) || create_user!
    end

    private

    def create_user!
      User.create!(
        facebook_id: facebook_id,
        first_name: first_name,
        last_name: last_name
      )
    end

    def user_data
      @user_data ||= begin
        response = Net::HTTP.get_response(request_uri)
        JSON.parse(response.body)
      end
    end

    def facebook_id
      user_data['id']
    end

    def first_name
      user_data['first_name']
    end

    def last_name
      user_data['last_name']
    end

    def request_uri
      URI.parse("#{FACEBOOK_URL}me?access_token=#{@access_token}&fields=#{FACEBOOK_USER_FIELDS}")
    end
  end
end

That’s basically it. Finally, you can test that everything works by making a request to our Rails API:

curl -X POST \
  http://localhost:3000/oauth/token \
  -d 'client_id=OAUTH_CLIENT_ID&grant_type=assertion&assertion=YOUR_FACEBOOK_ACCESS_TOKEN'

where OAUTH_CLIENT_ID is your id of Doorkeeper’s OAuth Application (you can read more about it here), YOUR_FACEBOOK_ACCESS_TOKEN is your Facebook access token (just to test, you can generate it in Facebook’s Graph API Explorer). After running that request you’ll get something like:

{"access_token":"e457964f1a006c75b3eeee168460547d6b6ffd5983dc284571832fed3a5b4ec9","token_type":"bearer","expires_in":86400,"refresh_token":"ad91f7b6c0a85f55c1d33c187a3ad0f4e79565a8d2074e38ba9f298357e459b2","created_at":1528458832}

It works! Here’s the access_token you can use to do authorized requests to your endpoints!

Fixing Kernel#singleton_method bug in Ruby.

Few days ago when I was browsing through StackOverflow I came across this question about weird behaviour of Kernel#singleton_method such as if you call Kernel#singleton_methods method on ActiveSupport::Deprecation class you would get you list of singleton methods (no surprise here) but when you try to get that singleton method with Kernel#singleton_method Ruby would throw NameError error:

ActiveSupport::Deprecation.singleton_methods(false) # => [:debug, :initialize, ...]
ActiveSupport::Deprecation.singleton_method(:debug) # => NameError (undefined singleton method `debug' for `ActiveSupport::Deprecation')

At first, I thought it’s ActiveSupport doing something fishy. And while I was looking through AS codebase this answer appeared. @michaelj discovered that it’s possible to reproduce this bug without ActiveSupport at all. All you needed to do is to prepend any module to a singleton class:

module Empty; end

class Foo
  singleton_class.prepend(Empty)

  def self.foo; end
end

Foo.singleton_methods(:false) # => [:foo]
Foo.singleton_method(:foo) # => NameError (undefined singleton method `foo' for `Foo')

So it was something wrong with Module#prepend.

I started looking for some bugs with Module#prepend on Ruby’s bugtracker. All I could find related was this bug with Object#methods and Module#prepend. So what I needed to do is to check how they fixed it and do something similar with Kernel#singleton_method.

DISCLAIMER: I’m not that good with Ruby internals so next part of the post might have some incorrect statements.

Main thing I learned from Object#methods fix was RCLASS_ORIGIN macro. This macro is used to get origin class of the passed class/module. And as I discovered Module#prepend makes a copy of a target class internally so if you need to access original one you can use that macro. Honestly, I don’t really understand why you can’t access singleton method but I got the idea.

That’s how rb_obj_singleton_method looked before the fix:

rb_obj_singleton_method(VALUE obj, VALUE vid)
{
  ...
  if (!id) {
    if (!NIL_P(klass = rb_singleton_class_get(obj))
  ...

as you can see the klass was retrieved by rb_singleton_class_get function which returns class’s copy if it was already prepended with some module. That means all I had to do is to apply RCLASS_ORIGIN on that class. And it did the trick. You can find the whole patch in this issue.

And may Ruby be with you.