<repo_name>krissi/ruby-transparent-lua<file_sep>/README.md
# Description
This library makes it easy to provide a complex API for Lua scripts. It
uses [RLua](https://github.com/whitequark/rlua) internally and therefore it
has the same compatibility and restrictions.

# Example code
Lets say we have these classes:

```ruby
User = Struct.new(:name, :favorite_food, :age)
Food = Struct.new(:name, :cooking_time) do
  def prepare(timer)
    if timer == cooking_time
      "This #{name} is delicious"
    elsif timer > cooking_time
      "This #{name} is awfully burnt"
    else
      "Smells like frozen #{name}..."
    end
  end

  def buy(*)
    'Got one'
  end
end

Pizza   = Food.new('Pizza', 16)
Lasagne = Food.new('Lasagne', 40)

class Sandbox
  def get(username)
    users.detect { |u| u.name == username }
  end

  def users
    [
        User.new('Kyle', Pizza, 43),
        User.new('Lisa', Pizza, 25),
        User.new('Ben', Lasagne, 6),
    ]
  end
end
```
    
To make it available to our Lua script we can use this code:

```ruby
require 'transparent_lua'

tlua = TransparentLua.new(Sandbox.new)
tlua.call(<<-EOF)
      print(get_user('Kyle').name .. " likes " .. get_user('Kyle').favorite_food.name .. ".");
      print(get_user('Kyle').favorite_food.buy());
      print("It needs to cook exactly " .. get_user('Kyle').favorite_food.cooking_time .. " minutes");

      my_cooking_time = 270;
      print(get_user('Kyle').name .. " cooks it for " .. my_cooking_time .. " minutes.");
      print(get_user('Kyle').favorite_food.prepare(my_cooking_time));
EOF

Pizza.cooking_time = 270

tlua.call(<<-EOF)
      print("Lets try it again for " .. my_cooking_time .. " minutes. Maybe it works now...");

      print(get_user('Kyle').name .. " cooks it for " .. my_cooking_time .. " minutes.");
      print(get_user('Kyle').favorite_food.prepare(my_cooking_time));
EOF
```

# Types of methods
 * Methods without arguments are created as index of the Lua table
 * Methods with arguments are created as a callable metatable.
 * To make clear, that an argumentless method is a method (`food.buy()` vs. `food.buy`),
   discard any arguments (`def buy(*)`)

# Type conversion
In addition to RLuas type conversion, this library converts Lua tables to either Hashes or
Arrays (when all keys are Numeric).

<file_sep>/Guardfile
guard :rspec,
      cmd:            'bundle exec rspec --fail-fast --color',
      all_on_start:   true,
      all_after_pass: true do
  watch(%r{^spec/.+_spec\.rb$})
  watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
  watch('spec/spec_helper.rb') { 'spec' }
  notification :file,
               path:   '.rspec_result',
               format: '%s'
end


guard :cucumber,
      all_on_start:   true do
  watch '.rspec_result' do
    'features' if File.read('.rspec_result').strip == 'success'
  end
  watch(%r{^features/.+\.feature$})
  watch(%r{^features/support/.+$}) { 'features' }

  watch(%r{^features/step_definitions/(.+)_steps\.rb$}) do |m|
    Dir[File.join("**/#{m[1]}.feature")][0] || 'features'
  end
end
<file_sep>/lib/transparent_lua.rb
require 'rlua'
require 'logger'

class TransparentLua
  SUPPORTED_SIMPLE_DATATYPES = [
    NilClass,
    TrueClass,
    FalseClass,
    Fixnum,
    Bignum,
    Float,
    Proc,
    String,
  ]

  attr_reader :sandbox, :state, :logger

  # @param [Object] sandbox The object which will be made visible to the lua script
  # @param [Hash] options
  # @option options [Lua::State] :state (Lua::State.new) a lua state to use
  # @option options [Boolean] :leak_globals When true, all locals from the lua scope are set in the sandbox.
  #   The sandbox must store the values itself or an error will be raised.
  #   When false the locals are not reflected in the sandbox
  def initialize(sandbox, options = {})
    @sandbox                 = sandbox
    @state                   = options.fetch(:state) { Lua::State.new }
    @logger                  = options.fetch(:logger) { Logger.new('/dev/null') }
    leak_locals              = options.fetch(:leak_globals) { false }
    setup(leak_locals)
  end

  # @param [String] script a lua script
  # @param [String] script_name the name of the lua script (#see Lua::State.__eval)
  # @return [Object] the return value of the lua script
  def call(script, script_name = nil)
    v = state.__eval(script, script_name)
    lua2rb(v)
  end

  private
  def setup(leak_globals = false)
    state.__load_stdlib :all

    global_metatable               = {
      '__index' => index_table(sandbox)
    }
    global_metatable['__newindex'] = newindex_table(sandbox) if leak_globals

    state._G.__metatable = global_metatable

    state.package.loaders    = Lua::Table.new(state)
    state.package.loaders[1] = ->(modname) do
      return "\n\tno module '#{modname}' available in sandbox" unless can_require_module? modname

      loader = ->(modname) do
        source = require_module(modname)
        state.__eval(source, "=#{modname}")
      end

      state.package.loaded[modname] = loader
      loader
    end
  end

  def can_require_module?(modname)
    return false unless sandbox.respond_to? :can_require_module?

    sandbox.can_require_module? modname
  end

  def require_module(modname)
    fail NoMethodError,
         "#{sandbox} must respond to #require_module because it responds to #can_require_module?" unless sandbox.respond_to? :require_module

    String(sandbox.require_module(modname))
  end

  def getter_table(object)
    return object if is_supported_simple_datatype? object

    metatable = {
      '__index'    => index_table(object),
      '__newindex' => newindex_table(object),
    }
    delegation_table(object, metatable)
  end

  def newindex_table(object)
    ->(t, k, v) do
      getter_table(object.public_send(:"#{k}=", lua2rb(v)))
    end
  end

  def index_table(object)
    ->(t, k, *newindex_args) do
      method = get_method(object, k)
      k = get_ruby_method_name(k)
      logger.debug { "Dispatching method #{method}(#{method.parameters})" }

      case method
      when ->(m) { m.arity == 0 }
        logger.debug { "Creating a getter table for #{method}" }
        getter_table(object.public_send(k.to_sym, *newindex_args))
      else
        logger.debug { "Creating a method table for #{method}" }
        method_table(method)
      end
    end
  end

  def get_method(object, method_name)
    method_name = get_ruby_method_name(method_name)
    object.method(method_name.to_sym)
  rescue NameError
    fail NoMethodError, "#{object}##{method_name.to_s} is not a method (but might be a valid message which is not supported)"
  end

  # @param [Method] method
  def method_table(method)
    delegation_table(
      '__call' => ->(t, *args) do
        converted_args = args.collect do |arg|
          lua2rb(arg)
        end

        getter_table(method.call(*converted_args))
      end
    )
  end

  def delegation_table(object = nil, hash)
    tab                = Lua::Table.new(@state)
    tab.__rb_object_id = -> { object.__id__.to_f } if object
    tab.__metatable    = hash
    tab
  end

  def lua2rb(v)
    case v
    when ->(t) { has_rb_object_id?(t) }
      ObjectSpace._id2ref(Integer(v.__rb_object_id))
    when ->(t) { Lua::Table === t && t.to_hash.keys.all? { |k| k.is_a? Numeric } }
      v.to_hash.values.collect { |v| lua2rb(v) }
    when Lua::Table
      v.to_hash.each_with_object({}) { |(k, v), h| h[lua2rb(k)] = lua2rb(v) }
    when Float
      (Integer(v) == v) ? Integer(v) : v
    else
      v
    end
  end

  def has_rb_object_id?(o)
    o.__rb_object_id
    true
  rescue NoMethodError
    false
  end

  # @param [Symbol] lua_method_name
  # @return [Symbol] ruby method name
  def get_ruby_method_name(lua_method_name)
    lua_method_name = String(lua_method_name)
    case lua_method_name
    when /^is_(.*)$/, /^has_(.*)$/
      return :"#{$1}?"
    else
      return lua_method_name.to_sym
    end
  end

  def is_supported_simple_datatype?(object)
    return true if SUPPORTED_SIMPLE_DATATYPES.include? object.class
    return true if Array === object && object.all? { |i| is_supported_simple_datatype?(i) }
    return true if Hash === object && object.keys.all? { |k| is_supported_simple_datatype?(k) } && object.values.all? { |v| is_supported_simple_datatype?(v) }
    false
  end
end
<file_sep>/transparent-lua.gemspec
# -*- encoding: utf-8 -*-
lib = File.expand_path('../lib/', __FILE__)
$:.unshift lib unless $:.include?(lib)

require 'transparent_lua/version'

Gem::Specification.new do |gem|
  gem.name          = 'transparent-lua'
  gem.version       = TransparentLua::VERSION
  gem.authors       = ['<NAME>']
  gem.email         = ['<EMAIL>']
  gem.description   = %q{A wrapper to pass complex objects between Ruby and Lua}
  gem.summary       = <<-EOD
  This library enables an easy way to pass complex objects between
  Ruby and Lua in a transparent way.
  EOD
  gem.homepage      = 'https://github.com/krissi/ruby-transparent-lua'
  gem.license       = 'MIT'

  gem.files         = `git ls-files`.split($/)
  gem.executables   = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
  gem.test_files    = gem.files.grep(%r{^(test|spec|features)/})
  gem.require_paths = ['lib']

  gem.add_dependency 'rlua', '~> 1.0'

  gem.add_development_dependency  'rake', '~> 10.4'
  gem.add_development_dependency  'cucumber', '~> 2.0'
  gem.add_development_dependency  'rspec', '~> 3.3'
end
<file_sep>/features/step_definitions/validation_steps.rb
Then(/^it should return (.*)$/) do |expected_return_value|
  expected_return_value = eval(expected_return_value)
  expect(@return_value).to eq(expected_return_value)
end


Then(/^the attribute "([^"]*)" of the sandbox is (.*)$/) do |attribute, expected_return_value|
  expected_return_value = eval(expected_return_value)

  expect(@sandbox.public_send(attribute.to_sym)).to eq(expected_return_value)
end


Then(/^the result of (.+) should (.*)$/) do |subject, expectation|
  expect(instance_eval(String(subject))).to eval(expectation)
end
<file_sep>/spec/transparent_lua_spec.rb
require 'spec_helper'
require 'transparent_lua'

describe TransparentLua do
end
<file_sep>/features/step_definitions/transparent_lua_steps.rb
When(/^I execute the script$/) do
  @transparent_lua = TransparentLua.new(@sandbox = @sandbox_class.new)

  @return_value = @transparent_lua.call(@script)
end

When(/^I execute the script with locales leaking enabled$/) do
  @transparent_lua = TransparentLua.new(@sandbox = @sandbox_class.new, leak_globals: true)

  @return_value = @transparent_lua.call(@script)
end
<file_sep>/features/step_definitions/preparation_steps.rb
require 'transparent_lua'

Given(/^an empty sandbox$/) do
  @sandbox_class = Class.new do
    def inspect
      '#<Empty Sandbox Class>'
    end

    alias :to_s :inspect
  end
end

Given(/^an? sandbox with an empty member class$/) do
  @member_class = Class.new do
    def inspect
      '#<Sandbox Member Class>'
    end

    alias :to_s :inspect
  end

  @sandbox_class = Class.new do
    def inspect
      '#<Sandbox Class>'
    end

    alias :to_s :inspect
  end

  @sandbox_class.class_exec(@member_class) do |member_class|
    define_method(:member_class) { member_class.new }
  end
end

And(/^the following (?:method definition as part of|code executed in) the sandbox: ?(.*)$/) do |*args|
  inline_code, text_code = *args
  code = String(text_code).empty? ? String(inline_code) : String(text_code)
  @sandbox_class.class_eval(code)
end

And(/^the following method definition as part of the member class: (.*)$/) do |method_definition|
  @member_class.class_eval(method_definition)
end

And(/^the following line as Lua script: (.*)$/) do |script|
  @script = String(script).strip
end

And(/^the following Lua script:$/) do |script|
  @script = String(script).strip
end

