Create new ruby gem - Moriarty part-3
Tags: ruby, beginners, opensource, tutorial
Published: 2021-04-02 11:27:10

{% github decentralizuj/moriarty no-readme %}

In past series I created `moriarti.rb` (responsible for everything). I love to work with ruby scripts, because of ability to write powerful/useful code in short time. But this only works for "*small projects*". If we want to extend script after some time, It'll become pain in the ass. For this we have ruby-gems, which are easily extendible even for different developer.  
  
 - **Performance advice:**
  
>_Each **require** make ruby slower. Do not create gem with a few lines of code and many **requires**. For that case, make a script file and use it until you want to extend it._  
    
---
  
# Create Ruby Gem  

---

First thing is to create directory where you want your ruby gem. In root directory create `bin` and `lib` folders. That's same for every gem.  
  
  
```ruby
  # Moriarty directory tree

moriarty |
         | - bin/ 
         |
         | - lib/
```  

Lib folder handle our scripts, and bin (binaries) handle executable files. Now let's go back to the root of our gem. We need few more files to add there.  

```ruby
  # Moriarty root files

Gemfile
Rakefile
README.md  # important!
moriarty.gemspec
```  

Let's talk about README file first. It's important to create and to write few lines about gem. Even if you do not plan to opensource it, or share with others, after some time you'll forget what you did 6 month ago in which file. To avoid time and nerve lose, don't be lazy and create it. Same thing with comments in source code - it's not must to go in details, just something to help you remember those details. 

**Pro TIP:**  
>Too many comments make source code unreadable. make comments short, write code to be readable. I wrote all this comments for purpose of learning. If you want to add comments _in right way,_ learn RDoc. I will write another article about generating RDoc from gem's comments.

Gemfile handle our dependencies from `.gemspec` file, so bundler know what to install:

```ruby
# Moriarty Gemfile

source 'https://rubygems.org'

# this line tell bundler where to look
gemspec
```  

Gemspec file contain everything about our gem. Dependencies, name, description, version, site... but also all files that our gem use. Specification also have data about executable files, so when we install our gem locally, we can run it `moriarty USERNAME` instead of `ruby bin/moriarty` from root directory.

>This is a reason why all objects should define path when work with files, because working directory is not always script directory.  

```ruby
# Gemspec file (moriarty.gemspec)

# frozen_string_literal: true
Gem::Specification.new do |s|
  s.name        = 'name'
  s.version     = '0.1.0'
  s.summary     = 'short description'
  s.description = <<~DESC
    # Write long description here.
  DESC
  s.authors  = ['...']
  s.homepage =  '...'
  s.license  =  '...'

  s.metadata['homepage_uri']    = '...'
  s.metadata['source_code_uri'] = '...'
  s.metadata['bug_tracker_uri'] = '...'

  # here you add all files from your gem
  s.files = ['README.md', 'LICENSE']

  # directory with executable
  s.bindir = 'bin'

  # one or more executable files
  s.executables   = ['...']

  # directory with our files
  s.require_paths = ['lib']

  # define gems needed to work
  s.add_runtime_dependency 'colorize', '~> 0.8.1'
  s.add_runtime_dependency 'rest-client', '~> 2.1.0'
  s.add_runtime_dependency 'nokogiri', '~> 1.11.2'

  # define gems needed for development
  s.add_development_dependency 'bundler', '~> 2.2.9'
  s.add_development_dependency 'rake', '~> 13.0.3'
end
```  

With all of this, we're ready to create files that will handle our code. In `lib/` we will create file `moriarty.rb`, and that file will require all other files. This way our executable (or other gem) need to require just one file for everything to work.

```ruby
# lib/moriarty.rb

#!/usr/bin/env ruby
require 'rest-client'
require 'colorize'
require 'nokogiri'

require_relative 'moriarty/cli'
```  

Here I required dependencies from gemspec. I explained in first article why I use rest-client. I also required relative file moriarti/cli, and we need to create it. Don't think about it too much for now.

From our script file I will copy almost all methods. I will not copy `#find!` and code we used to run script. Only code to construct username, url, send request and receive response.

```ruby
#
# Main class to get data and execute request
#
# Methods: [ #new, #go, #success?, #make_url, #url=, #user= ]
#
# Attributes:
#  :user     = :moriarty     => 'moriarty'
#  :url      = 'example.com' => 'https://example.com/'
#  :response => [.code, .headers, .body]  -> restclient#get
#  :html     => scrapped HTML if success? -> nokogiri#html
#

class Moriarty

  attr_reader :url, :user, :response, :html

  # Set username and site for search request
  # exclude 'https', #make_url will add it for you
  # To use different protocol, set it as third parameter
  #
  # @jim = Moriarty.new( 'moriarty', 'example.com', :http )
  #  => @jim.user == 'moriarty'
  #  => @jim.url  == 'http://example.com/'

  def initialize( name = '', site = 'github.com', type = :https )
    @user = name.to_s
    @url  = make_url site, type
  end

  # execute request (args are optional)
  # @jim.go site: 'github.com', user: 'mynickname'
  #  => true/false
  #  -> @jim.response (.code, .headers, .body)
  #  -> @jim.html (page HTML if request success)

  def go( opt = {} )
    opt[:user] ||= @user
    url = opt[:site].nil? ? @url : make_url(opt[:site])
    uri = url + opt[:user]
    @response = RestClient.get uri
    @html     = Nokogiri::HTML @response
    return @success = true
    rescue
    return @success = false
  end

  alias search go

  # create URL from site name, add 'https' if needed
  # @jim.make_url 'instagram.com'
  #  => 'https://instagram.com/'

  def make_url( link, prot = :https )
    prot = nil if link.to_s.start_with? prot.to_s
    url  = prot.nil? ? link.to_s : prot.to_s + '://' + link.to_s
    url += '/' unless url.end_with?('/')
    return url
  end

  # Set URL from site name and protocol(optional)
  # @jim.url = 'github.com'
  #  => @jim.url == 'https://github.com'

  def url=( link, start = :https )
    @url = make_url link, start
  end

  # Set username from string or :symbol
  # @jim.user = :moriarty
  #  => @jim.user == 'moriarty'

  def user=( name )
    @user = name.to_s
  end

  # Check does request succeeded or not
  # @jim.success?
  #  => true/false

  def success?
    @success == true
  end
end
```  

Now it's time to create `lib/moriarty` directory, and `cli.rb` inside. That file I will use for methods we need in CLI (in future versions, that will be removed).

```ruby
class Moriarty

 class << self

  # Moriarty.find! 'smartuser'      
  #  => [FOUND!] if username 'smartuser' is free on github
  #
  # Moriarty.find! :stupiduser, 'facebook.com', :hunt
  #  => [FREE!] if user 'stupiduser' is registered on facebook

  def find!( username, web = 'github.com', type = :search )

    @jim = Moriarty.new username.to_s, web.to_s
    @jim.go

    name = print_name(username).to_s
    site = print_url(web).to_s

    case    
    when type.to_sym == :hunt && @jim.success?
      p1('+')
      p1('FOUND!')
      p2(" #{name}", :cyan, :bold)
      p2(" found on >> ") 
      puts p2(site, :cyan, :bold)
    when type.to_sym == :hunt && !@jim.success?
      p1('-', :red, :red)
      p2(" #{name} fail on ", :red)
      puts p2(site, :red)
   when @jim.success?
      p1('-', :red, :red)
      p2(" #{name} is taken on ", :red)
      puts p2(site, :red)
    else
      p1('+')
      p1('FREE!')
      p2(" #{name}", :cyan, :bold)
      p2(" is free on >> ")
      puts p2(site, :cyan, :bold)
    end
  end

  # #hunt! is alias for #find! with :hunt option
  # Check is user 'stupiduser' registered on instagram
  #  -> Moriarty.hunt! 'stupiduser', 'instagram.com'

  def hunt!( name, site = 'github.com', type = :hunt)
    find! name, site, type
  end

  # Remove extensions from domain name
  # Moriarty.print_url('https://www.github.com')
  #  => 'github'

  def print_url( site )
    site, name = site.to_s, ''
    if site.start_with?('http')
      site.gsub!("https://", '') or site.gsub!("http://", '')
    end
    site.gsub!("www.", '') if site.start_with?('www.')
    ext = site.to_s.split('.').last
    name = site.gsub(".#{ext}", '')
    name = name.split('/').first if ext.size < 5
    return name.capitalize
  end

  # Remove extensions from username
  # Moriarty.print_name('@moriarty')
  #  => 'moriarty'

  def print_name( name )
    name.gsub!('@','') if name.to_s.start_with?('@')
    name.gsub!('#','') if name.to_s.start_with?('#')
    name.gsub!('/u/','') if name.to_s.start_with?('/u/')
    return name
  end

  def p1( title, color1 = :'light_green', color2 = :cyan, type = :bold )
    str = ' ['.colorize(color2) + title.to_s.colorize(color1) + ']'.colorize(color2)
    str = str.bold if type == :bold
    print str
  end

  def p2( title, color = :cyan, type = :regular)
    str = title.colorize(color)
    str = str.bold if type == :bold
    print str
  end
 end
end
```  

In `cli.rb` we again define `class Moriarty`, but this time accept only `self` methods. Note that you can create as many files as you want, just require them in main `lib/moriarty.rb`, and add in `.gemspec`.

Now we have our classes, but we need a way to execute it in terminal. For that we will create `bin/moriarty`:

```ruby
#!/usr/bin/env ruby

# require 'lib/moriarty', which will require 'moriarti/cli'

require_relative '../lib/moriarty'

  # Simple way to try first version
  # accept '--hunt' as argument

  hunt = ARGV.include?('--hunt') ? :hunt : :search
  message = hunt == :hunt ? "Starting hunt on user -> " : "Starting search for free username -> "

  # If no args, or include '-h' or '--help', print banner
  # If use '--hunt', do not search for '--hunt' username

  if ARGV.empty? or ARGV.include? '-h' or ARGV.include? '--help'

    puts "\nSearch social networks for free username".cyan
    puts " $ ".white + "ruby bin/moriarty sherlock watson".green
    puts "\nRun with --hunt to search for registered users".cyan
    puts " $ ".white + "ruby bin/moriarty sherlock watson --hunt\n".green
    exit(1)

  else

    system 'clear' or system 'cls'
    start_time = Time.now.to_i

    ARGV.each do |user|
      next if user == '--hunt'

      puts
      Moriarty.p2(message, :cyan, :bold)
      Moriarty.p1(user)
      puts; puts

      Moriarty.find! user,         'github.com',            hunt
      Moriarty.find! user,         'rubygems.org/profiles', hunt
      Moriarty.find! "@#{user}",   'dev.to',                hunt
      Moriarty.find! "@#{user}",   'medium.com',            hunt
      Moriarty.find! "/u/#{user}", 'reddit.com',            hunt
      Moriarty.find! user,         'youtube.com',           hunt
      Moriarty.find! user,         'facebook.com',          hunt
      Moriarty.find! user,         'instagram.com',         hunt
      Moriarty.find! "@#{user}",   'tiktok.com',            hunt
    end

    end_time = Time.now.to_i
    counter  = end_time - start_time

    sec = ' second'
    sec += 's' unless counter.to_s.end_with?('1')

    puts
    Moriarty.p2('Finished in ->', :cyan, :bold)
    Moriarty.p1(counter)
    Moriarty.p2(sec, :cyan, :bold)
    puts; puts
  end
```  

Don't forget to give this file executable permissions:  
```bash
sudo chmod +x bin/moriarty
```  

Almost done! We need to add all those files in `.gemspec`:

```ruby
  s.files = ['bin/moriarty', 'lib/moriarty.rb', 'lib/moriarty/cli.rb', 'moriarty.gemspec',
             'README.md', 'LICENSE']
  s.bindir = 'bin'
  s.executables = ['moriarty']
```  

I added license file here (MIT in my case), that's all up to you. The easiest way is to create repository on GitHub and choose license.  

To be able to use `rake` tasks, open `Rakefile` and add:

```ruby
require 'bundler/gem_tasks'
```  

Now you can use your gem from terminal:

```ruby
ruby bin/moriarti sherlock --hunt
```

Or compile into gem:

```ruby
bundle exec rake build
```

To push into rubygems:

```ruby
# register on rubygems

gem push pkg/moriarti-0.1.0.gem
```

Or upload to github and use from git:

```ruby
# Gemfile of some app

require 'moriarty', '~> 0.1.0', git: 'https:github.com/....'
```  

---

Next article will be about refactoring gem and terminal use (options, colors, output...)