Skip to content

Latest commit

 

History

History
294 lines (221 loc) · 7.86 KB

ruby-basics.md

File metadata and controls

294 lines (221 loc) · 7.86 KB

Ruby Basics

The openHAB JRuby scripting automation is based on the JRuby implementation of the Ruby language. This page offers a quick overview of Ruby to help you get started writing rules. However, it is by no means comprehensive. A wealth of information can be found on Ruby's web site.

Data Types

In Ruby, everything is an object, even primitive types such as numbers and strings. For example, 1 as a number is an object and has all the methods for the Integer class.

It is useful to get to know the basic data types that we will often encounter:

  • Integer - e.g. 1, -3, etc.
  • Floating Point - e.g. 3.5
  • String - String literals in Ruby can be enclosed with double quotes, or single quotes. Strings enclosed by double quotes can contain variables and expressions that are enclosed with #{}. For example: "Hi my name is #{name_variable}". The String class offers a plethora of useful methods to operate on and manipulate strings.
  • Array - example: [1, 2, 'foo', AnotherObject]
  • Hash - example: { 'key1' => 'value', 'key2' => 'value' }
  • Symbol - example: :iamasymbol
  • Range - example: 1..5

Variables

  • In Ruby, variables start with a lower case and by convention use snake_case.
  • Uppercase identifiers are constants, e.g. NAMES
  • Variable whose names start with $ are global variables, e.g. $i_am_global.
  • Variable whose names start with @ are instance variables, e.g. @instance_variable. Instance variables are similar to member variables or fields in other languages.
  • Local variables are just plain names that starts with a lower case, e.g. local_var.

Examples

Instance variables are created as soon as they are referenced. They are persisted on whatever self is. In most simple file-based rules, and in UI based rules, self is simply a top level Object named main, and instance variables will be persisted between multiple executions of the same rule:

rule "light turned on" do
  changed Light_Switch, to: ON
  run do
    @turned_on_count ||= 0
    @turned_on_count += 1
    logger.info("The light has been turned on #{@turned_on_count} times")
  end
end

String Interpolation

String interpolation is done by enclosing a variable or an expression with #{ and } inside a double quoted string. The variable doesn't have to be a string. Ruby will automatically call #to_s for you.

Although string concatenation with + is possible, using string interpolation is preferred.

An example of string interpolation is included above.

Control Expressions

Ruby supports various control expressions such as if/else, ternary operator, case, etc.

Example:

if a
  # do something here
elsif b
  # something else
else 
  # something here
end

# modifier if form
a = b if c == 5

# ternary operator
a = b == 5 ? 'five' : 'other'

# case/when similar to the switch() { case... } in c / java.
rule 'x' do
  received_command DimmerItem1
  run do |event|
    case event.command
    when OFF
      Light1.off
      Light2.off
    when 0...50
      Light1.on
      Light2.off
    when 50..100, ON
      Light1.on
      Light2.on
    end
  end
end

Loops

While Ruby supports the traditional for and while loop, they are rarely used. Ruby objects such as Array, Hash, Set, etc. provide a plethora of methods to achieve the same thing in a more "Ruby" way.

Examples

array = [1, 2, 3]
array.each do |elem|
  logger.info("Element: #{elem}")
end

array.each_with_index do |elem, index|
  logger.info("Element #{index}: #{elem}")
end

SWITCH_TO_LIGHT_HASH = { Switch1 => Light1, Switch2 => Light2 }

SWITCH_TO_LIGHT_HASH.each do |switch, light|
  logger.info "#{switch.name} => #{light.name}"
end

rule 'turn light on' do
  changed Switches.members
  triggered do |item|
    SWITCH_TO_LIGHT_HASH[item]&.command item.state
  end
end

Note: next is similar to continue in C/Java. break in Ruby is the same as in C/Java.

Methods

Methods are defined like this:

def one_plus_one # It can also be defined as def one_plus_one()
  1 + 1
end

With parameters:

def one_plus(arg1) 
  1 + arg1
end

With a keyword argument:

def one_minus(another:)
  1 - another
end

Note the last value in a method execution becomes its return value, so a return keyword is optional.

To call a method:

# Calling a method without passing any arguments, no parentheses needed.
one_plus_one
# This works too:
one_plus_one()

# Calling a method with an argument:
one_plus(2)
# The parentheses can also be omitted here:
one_plus 2

# Calling a method with a keyword argument:
one_minus(another: 1)
# Guess what, the parentheses can also be omitted here:
one_minus another: 1

Blocks

Multi-line blocks in Ruby are enclosed in a do .. end pair and single line blocks are enclosed with braces { .. }. You have encountered blocks in the examples above. Rules are implemented in a block:

rule 'rulename' do
  ...
end

The execution part is also in a block for the run method, nested inside the rule block:

rule 'rulename' do
  changed Item1
  run do 
    ...
  end
end

Block arguments

Blocks can receive arguments which are passed by its caller. We will often encounter this in {OpenHAB::DSL::Rules::BuilderDSL#run run} and {OpenHAB::DSL::Rules::BuilderDSL#triggered triggered} blocks.

rule 'name' do
  changed Switches.members
  run do |event|
    # do something based on the event argument
  end
end

Ruby's Safe Navigation Operator

Ruby has a safe navigation operator &. which is similar to ?. in C#, Groovy, Kotlin, etc.

# Instead of:
if items['My_Item']
  items['My_Item'].on
end

# We can write it as:
items['My_Item']&.on

Some Gotchas

Exiting early

To exit early from a block, use next instead of return.

rule 'rule name' do
  changed Item1
  run do 
    next if Item1.off? # exit early

    Item2.on # Turn on Item2 if Item1 turned on
    # Do other things
  end
end

Note: To exit early from a UI rule, use return.

Parentheses

In Ruby, parentheses are optional when calling a method. However, when calling a method with arguments and a single-line block, the parentheses must be used. Example:

after(5.seconds) {  }

after(5.seconds) do
  # ...
end

after 5.seconds do
  # parentheses aren't a must before a do..end block
end

# the following example will cause an error
after 5.seconds { }

Zero is "truthy" in Ruby

if 0
  logger.info "This will always be executed"
else
  logger.info "This will never be executed"
end

Source Code Formatting

The ruby style guide offers the generally accepted standards for Ruby source code formatting.

When working with file based rules in a source code editor (e.g. VSCode), it is highly recommended to integrate Rubocop (or rubocop-daemon) as the source code formatter and linter for Ruby.

Happy coding!