LendingHome destruct

Code Climate Coverage Gem Version

ES6 style object destructuring in Ruby

Check out the JavaScript ES6 object destructuring documentation for more information.

Why?

This was primarily a learning exercise to understand how this newer ES6 feature could work under the hood. We're not currently using this in production anywhere but it was a pretty fun challenge to solve.


Ruby 2.3+ already has some built-in methods and operators for simple object destructuring:

This gem introduces a couple of new methods to the Object class for more complex destructuring.

  • Object#dig
  • Object#destruct

It's mostly useful for fetching multiple nested values out of objects in a single method call.

Installation

Add this gem to the project Gemfile.

gem "destruct"

Usage

Object#dig

This behaves just like the dig methods in Array, Hash, and Struct allowing ALL objects to be destructured.

The implementation simply uses send to pass valid method calls thru to objects recursively.

class Object
  def dig(method, *paths)
    object = send(method) if respond_to?(method)
    paths.any? ? object&.dig(*paths) : object
  end
end

This method behaves very similar to the safe navigation operator &. but checks if the object responds to the method before attempting to call it. Invalid method calls return nil instead of raising NoMethodError.

"test".dig(:upcase, :reverse) # "TSET"
"test".dig(:invalid, :chain, :of, :methods) # nil

It also delegates to native dig implementations for Array, Hash, or Struct objects whenever possible.

class Blog
  def posts
    [
      { "title" => "Testing" },
      { "title" => "Example" }
    ]
  end
end

Blog.new.dig(:posts, 1, "title") # "Example"

Object#destruct

This method is like a hybrid of all the other native Ruby destructuring methods! Let's define an example object:

object = {
  id: 123,
  title: "Hi",
  translations: [
    {
      locale: "es_MX",
      last_edit: "2014-04-14T08:43:37",
      title: "Hola"
    }
  ],
  url: "/hi-123"
}

It behaves like values_at and looks up values by keys:

id, url = object.destruct(:id, :url)
puts id # 123
puts url # "/hi-123"

It behaves like dig to lookup nested values:

title, locale_title = object.destruct(:title, [:translations, 0, :title])
puts title # "Hi"
puts locale_title # "Hola"

It accepts hashes to dig out nested values as well:

locale, title = object.destruct(translations: { 0 => [:locale, :title] })
puts locale # "es_MX"
puts title # "Hola"

It accepts a mixture of different argument types:

title, last_edit, locale, locale_title = object.destruct(
  :title,
  [:translations, 0, :last_edit],
  translations: { 0 => [:locale, :title] }
)

puts title # "Hi"
puts last_edit # "2014-04-14T08:43:37"
puts locale # "es_MX"
puts locale_title # "Hola"

It accepts a block to lookup nested values with a clear and convenient DSL:

title, last_edit, locale, url = object.destruct do
  title
  translations[0].last_edit
  translations[0][:locale]
  url
end

puts title # "Hi"
puts last_edit # "2014-04-14T08:43:37"
puts locale # "es_MX"
puts url # "/hi-123"

It returns a Destruct::Hash object when the return values are not splatted:

destructured = object.destruct do
  title
  translations[0].last_edit
  translations[0][:locale]
  url
end

puts destructured.title # "Hi"
puts destructured[:title] # "Hi"
puts destructured[0] # "Hi"

puts destructured.last_edit # "2014-04-14T08:43:37"
puts destructured.locale # "es_MX"

puts destructured.url # "/hi-123"
puts destructured[-1] # "/hi-123"

puts destructured[999] # nil
puts destructured[:missing] # nil
puts destructured.missing # NoMethodError

Note that Destruct::Hash values are overwritten if there are multiple with the same keys:

destructured = object.destruct(:title, [:translations, 0, :title])

puts destructured.title # "Hola"

# This is where the index lookups really come in handy
puts destructured[0] # "Hi"
puts destructured[1] # "Hola"

The return value destructuring is done using Destruct::Hash#to_ary for implicit Array conversion!

Examples

Let's compare some of the JavaScript ES6 destructuring examples with their Ruby equivalents.

Note that almost all of these examples simply use native Ruby 2.3+ features!

Array destructuring

Basic variable assignment

var foo = ["one", "two", "three"];
var [one, two, three] = foo;

console.log(one); // "one"
console.log(two); // "two"
console.log(three); // "three"
foo = ["one", "two", "three"]
one, two, three = foo

puts one # "one"
puts two # "two"
puts three # "three"

Default values

var [a=5, b=7] = [1];

console.log(a); // 1
console.log(b); // 7
a, b = [1]
a ||= 5
b ||= 7

puts a # 1
puts b # 7

Swapping variables

var a = 1;
var b = 3;
[a, b] = [b, a];

console.log(a); // 3
console.log(b); // 1
a = 1
b = 3
a, b = b, a

puts a # 3
puts b # 1

Parsing an array returned from a function

function f() {
  return [1, 2];
}

var [a, b] = f();

console.log(a); // 1
console.log(b); // 2
def f
  [1, 2]
end

a, b = f

puts a # 1
puts b # 2

Ignoring some returned values

function f() {
  return [1, 2, 3];
}

var [a, , b] = f();

console.log(a); // 1
console.log(b); // 3
def f
  [1, 2, 3]
end

a, _, b = f

puts a # 1
puts b # 3

Ignoring remaining values

var [a, b] = [1, 2, 3, 4];

console.log(a); // 1
console.log(b); // 2
a, b = [1, 2, 3, 4]

puts a # 1
puts b # 2

Capture remaining values

var [a, b, ...c] = [1, 2, 3, 4];

console.log(c); // [3, 4]
a, b, *c = [1, 2, 3, 4]

puts c.inspect # [3, 4]

Destructure a nested array

const avengers = [
  "Natasha Romanoff",
  ["Tony Stark", "James Rhodes"],
  ["Steve Rogers", "Sam Wilson"]
];

const [blackWidow, [ironMan, warMachine], [cap, falcon]] = avengers;

console.log(warMachine); // "James Rhodes"
avengers = [
  "Natasha Romanoff",
  ["Tony Stark", "James Rhodes"],
  ["Steve Rogers", "Sam Wilson"]
]

black_widow, iron_man, war_machine, cap, falcon = avengers.flatten

puts war_machine # "James Rhodes"

Pluck a single value from a deeply nested array

const avengers = [
  "Natasha Romanoff",
  [["Tony Stark", "Pepper Potts"], "James Rhodes"],
  ["Steve Rogers", "Sam Wilson"]
];

const [, [[, potts ]]] = avengers;

console.log(potts); // "Pepper Potts"
avengers = [
  "Natasha Romanoff",
  [["Tony Stark", "Pepper Potts"], "James Rhodes"],
  ["Steve Rogers", "Sam Wilson"]
]

potts = avengers.dig(1, 0, 1)

puts potts # "Pepper Potts"

Pulling values from a regular expression match

var url = "https://developer.mozilla.org/en-US/Web/JavaScript";

var parsedURL = /^(\w+)\:\/\/([^\/]+)\/(.*)$/.exec(url);
console.log(parsedURL); // ["https://developer.mozilla.org/en-US/Web/JavaScript", "https", "developer.mozilla.org", "en-US/Web/JavaScript"]

var [, protocol, fullhost, fullpath] = parsedURL;
console.log(protocol); // "https"
url = "https://developer.mozilla.org/en-US/Web/JavaScript"

parsed_url = /^(\w+)\:\/\/([^\/]+)\/(.*)$/.match(url).to_a
puts parsed_url.inspect # ["https://developer.mozilla.org/en-US/Web/JavaScript", "https", "developer.mozilla.org", "en-US/Web/JavaScript"]

_, protocol, fullhost, fullpath = parsed_url.to_a
puts protocol # "https"

Object destructuring

Basic assignment

var o = {p: 42, q: true};
var {p, q} = o;

console.log(p); // 42
console.log(q); // true
o = { p: 42, q: true }
p, q = o.values_at(:p, :q)

puts p # 42
puts q # true

Assigning to new variable names

var o = {p: 42, q: true};
var {p: foo, q: bar} = o;

console.log(foo); // 42
console.log(bar); // true
o = { p: 42, q: true }
foo, bar = o.values_at(:p, :q)

puts foo # 42
puts bar # true

Default values

var {a=10, b=5} = {a: 3};

console.log(a); // 3
console.log(b); // 5
a, b = { a: 3 }.values_at(:a, :b)
a ||= 10
b ||= 5

puts a # 3
puts b # 5

Setting default function parameters

function drawES6Chart({size = "big", cords = { x: 0, y: 0 }, radius = 25} = {}) {
  console.log(size, cords, radius);
  // do some chart drawing
}

drawES6Chart({
  cords: { x: 18, y: 30 },
  radius: 30
});
def draw_es6_chart(size: "big", cords: { x: 0, y: 0 }, radius: 25)
  puts size, cords, radius
  # do some chart drawing
end

draw_es6_chart(
  cords: { x: 18, y: 30 },
  radius: 30
)

Nested object and array destructuring

var metadata = {
  title: "Scratchpad",
  translations: [
    {
      locale: "de",
      localization_tags: [ ],
      last_edit: "2014-04-14T08:43:37",
      url: "/de/docs/Tools/Scratchpad",
      title: "JavaScript-Umgebung"
    }
  ],
  url: "/en-US/docs/Tools/Scratchpad"
};

var { title: englishTitle, translations: [{ title: localeTitle }] } = metadata;

console.log(englishTitle); // "Scratchpad"
console.log(localeTitle);  // "JavaScript-Umgebung"
 = {
  title: "Scratchpad",
  translations: [
    {
      locale: "de",
      localization_tags: [ ],
      last_edit: "2014-04-14T08:43:37",
      url: "/de/docs/Tools/Scratchpad",
      title: "JavaScript-Umgebung"
    }
  ],
  url: "/en-US/docs/Tools/Scratchpad"
}

english_title, locale_title = .destruct do
  title
  translations[0].title
end

puts english_title # "Scratchpad"
puts locale_title # "JavaScript-Umgebung"

For of iteration and destructuring

var people = [
  {
    name: "Mike Smith",
    family: {
      mother: "Jane Smith",
      father: "Harry Smith",
      sister: "Samantha Smith"
    },
    age: 35
  },
  {
    name: "Tom Jones",
    family: {
      mother: "Norah Jones",
      father: "Richard Jones",
      brother: "Howard Jones"
    },
    age: 25
  }
];

for (var {name: n, family: { father: f } } of people) {
  console.log("Name: " + n + ", Father: " + f);
}

// "Name: Mike Smith, Father: Harry Smith"
// "Name: Tom Jones, Father: Richard Jones"
people = [
  {
    name: "Mike Smith",
    family: {
      mother: "Jane Smith",
      father: "Harry Smith",
      sister: "Samantha Smith"
    },
    age: 35
  },
  {
    name: "Tom Jones",
    family: {
      mother: "Norah Jones",
      father: "Richard Jones",
      brother: "Howard Jones"
    },
    age: 25
  }
]

people.each do |person|
  n, f = person.destruct(:name, family: :father)
  puts "Name: #{n}, Father: #{f}"
end

# "Name: Mike Smith, Father: Harry Smith"
# "Name: Tom Jones, Father: Richard Jones"

Pulling fields from objects passed as function parameter

function userId({id}) {
  return id;
}

function whois({displayName: displayName, fullName: {firstName: name}}){
  console.log(displayName + " is " + name);
}

var user = {
  id: 42,
  displayName: "jdoe",
  fullName: {
    firstName: "John",
    lastName: "Doe"
  }
};

console.log("userId: " + userId(user)); // "userId: 42"
whois(user); // "jdoe is John"
def user_id(id:)
  id
end

def whois(display_name:, full_name:)
  puts "#{display_name} is #{full_name[:first_name]}"
end

user = {
  id: 42,
  displayName: "jdoe",
  fullName: {
    firstName: "John",
    lastName: "Doe"
  }
}

puts "userId: #{user_id(user)}" # "userId: 42"
whois(user) # "jdoe is John"

Computed object property names

let key = "z";
let { [key]: foo } = { z: "bar" };

console.log(foo); // "bar"
key = :z
foo = { z: "bar" }[key]

puts foo # "bar"

Testing

bundle exec rspec

Contributing

  • Fork the project.
  • Make your feature addition or bug fix.
  • Add tests for it. This is important so we don't break it in a future version unintentionally.
  • Commit, do not mess with the version or history.
  • Open a pull request. Bonus points for topic branches.

Authors

License

MIT - Copyright © 2016 LendingHome