Ruby encapsulate/package existing code into a namespace - ruby

I've been looking all around and didn't found any kind of answer to the problem i'm facing in Ruby. I'm writing an app that use core modules sets that are available in different versions. If I'm sourcing a core set version after an another one both code version will be sourced at the same and will clash with each other. That is quite normal and I'm ok with this.
One approach could be to unload the previous version to load the new one but I'd like to keep all the ones loaded into specific namespaces (to avoid time consuming to unload/reload code all the time). 2 possible solutions to me (or maybe other)
Either source the code then move it into a version namespace (some clues to do it see below but doesn't work yet)
Or source the code directly into a version namespace (don't know how to do it exactly, maybe with module_eval but need to recode the require process with dependencies). Does any solution seems possible ?
Here is a very simple poc of what I'm trying to achieve
file : coreset_1.0.0.rb
module CoreA
def self.who_am_i?; self.to_s; end
def self.get_coreb; CoreB end
end
module CoreB
def self.who_am_i?; self.to_s; end
end
file : coreset_2.0.0.rb (got some changes)
module CoreA
def self.my_name; self.to_s; end
def self.get_coreb; CoreB end
end
module CoreB
def self.my_name; self.to_s; end
end
file : coreManager.rb
module CoreManager
def self.load_version(arg_version)
#Create a module set for the selected version
core_set_name = CoreSet + '_' + arg_version.gsub('.', '_')
core_set = eval("Module #{core_set_name}; end; #{core_set_name}"
#Load the requested code
require "coreset_#{arg_version}.rb"
#Move loaded code into it core set module
core_set.const_set(:CoreA, Object.send(:remove_const, :CoreA))
core_set.const_set(:CoreB, Object.send(:remove_const,:CoreB))
#Return the created core set
core_set
end
end
If running the code :
require 'coreManager.rb'
core_set = CoreManager.load_version("1.0.0")
puts core_set::CoreA.who_am_i?
puts core_set::CoreA.get_coreB
it returns :
CoreA #not CoreSet_1_0_0::CoreA
uninitialized constant CoreA::CoreB (NameError)
If running something statically defined, it works
module CoreSet
module CoreA
def self.who_am_i?; self.to_s; end
def self.get_coreb; CoreB end
end
module CoreB
def self.who_am_i?; self.to_s; end
end
end
CoreSet::CoreA.get_coreb
It returns as expected :
CoreSet::CoreB
Depite of what is usually said :"a module is a constant", it seems to be more than that. What are the differencies and how to make the dynamic version working ?
Any other ideas ?
Thanks for your help folks :)

There are several things broken in your code (which is ok for POC, I guess), but the main one is that require loads constants and globals into the global namespace.
So, your Core<X> modules are not namespaced this way, as you might expect. There is Kernel#load method that allows "wrapped" execution of the loaded file, but it's wrapped into an anonymous module, so you can prevent the global namespace from being polluted, but you cannot "target" the constants to be defined into a particular namespace this way.
What you could try is to load the code as text and then eval it within the dynamically created module, matching your version. For example, look at this quick and very dirty sketch (coreset_...rb files are expected to sit into coresets directory):
module CoreSet; end
class CoreManager
class << self
def load_version(ver)
raise "Vesion #{ver} unknown" unless exists?(ver)
file = filename(ver)
code = File.read(file)
versioned_wrapper = Module.new do
class_eval code
end
CoreSet.const_set("V_#{ver.gsub('.', '_')}", versioned_wrapper)
end
private
def exists?(ver)
File.exists? filename(ver)
end
def filename(ver)
"coresets/coreset_#{ver.gsub('.', '_')}.rb"
end
end
end
CoreManager.load_version("1.0.0")
CoreManager.load_version("2.0.0")
p CoreSet::V_1_0_0::CoreA.who_am_i? # => "CoreSet::V_1_0_0::CoreA"
p CoreSet::V_1_0_0::CoreA.get_coreb # => CoreSet::V_1_0_0::CoreB
p CoreSet::V_2_0_0::CoreA.my_name # => "CoreSet::V_2_0_0::CoreA"
p CoreSet::V_2_0_0::CoreA.get_coreb # => CoreSet::V_2_0_0::CoreB
But, DON'T do this at home, please :) At least, I would think twice.
If all you need is to have all the versions loaded at once (you need namespaces exactly for this, right?) what stops you from defining them statically, like CoreSet::V1::Core<X> etc and use the idiomatic and safe ways to (auto)load them? :) Playing with dynamic nested constants definition (and, especially, their removing) is one of the easiest ways to shoot your own foot...

Ok I finally came to a solution that may help others or that can be discussed.
Getting the error uninitialized constant CoreA::CoreB (NameError) leads me to take the problem under a new angle. If I'm not able to access to CoreB module from CoreA (because the module nesting has been broken when redefining module constants into the CoreSet module) then why not referencing in each core module the other ones in the set ? And finaly it works without any dirty hack, I'm just creating pointers and the Ruby Core find it natively ;)
module CoreManager
def self.load_version(arg_version)
#Create a module set for the selected version
core_set_name = CoreSet + '_' + arg_version.gsub('.', '_')
core_set = eval("Module #{core_set_name}; end; #{core_set_name}"
#Load the requested code
toplevel_consts = Object.constants
require "coreset_#{arg_version}.rb"
core_modules = Object.constants - toplevel_consts
#Move the core modules to the set namespace
core_modules.collect! do |core_module|
core_module_sym = core_module.to_s.to_sym
core_set.const_set(core_module_sym, Object.send(:remove_const, core_module_sym))
eval("#{core_set}::#{core_module}")
end
#Create connexion between set cores to skirt broken module nesting
core_modules.each do |current_core|
core_modules.each do |other_core|
current_core.const_set(other_core.to_s.to_sym, other_core) unless current_core == other_core
end
end
#Return the created core set
core_set
end
end

Related

Why the program pass all tests if I use regular if statement in the method but says `stack level too deep` when using a ternary operator instead?

I was working on coding challenge called Robot name. I also had tests for that. The program passed all the tests. The code is below..
class Robot
attr_accessor :name
##robots = []
def initialize
#name = self.random_name
##robots << self.name
end
def random_name
name = ''
2.times do
name << ('a'..'z').to_a.sample
end
3.times do
name << (1..9).to_a.sample.to_s
end
no_duplicate(name.upcase)
end
def reset
#name = self.random_name
end
def no_duplicate(name)
if ##robots.include? name
reset
else
name
end
end
end
If you need to see the tests file you can look it up here robot_name_tests.
Then I started to refactor and one of the first things was to refactor no_duplicate method. So after refactoring the code looked like this
class Robot
...
# the rest of code stayed the same
def no_duplicate(name)
##robots.include? name ? reset : name
end
end
With this version all tests showed SystemStackError: stack level too deep. Why does it give this error and what is going on behind the scenes in both cases considering the code provided? Thanks!
I like your poetry mode code but it has led you into trouble here.
One way to kinda keep it in poetry mode but fix your operator priority issue is to do this:
def no_duplicate(name)
(##robots.include? name) ? reset : name
end
Update: if you work in Big Corporation With Coding Standards you will need to make it a bit more boring. I thought this was obvious but the gallery is correctly noting the usual solution:
##robots.include?(name) ? reset : name

How can I mimic Node.js's require function in Ruby?

In node.js you can write:
var lib = require('lib');
but in Ruby the require function simply runs the code in the file and true is returned.
Currently, I'm using a very dirty solution:
main.rb:
$stuff = []
require './file1.rb'
require './file2.rb'
# and so on
file1.rb:
$stuff << something
and so on.
How can I eliminate the use of a global variable?
eg:
main.rb:
$stuff = []
$stuff << cool_require './file1.rb'
# etc
file1.rb:
exports.what = something
One of the biggest errors when working with a language, is trying to make the language working like a different one.
Ruby is not NodeJs, there are features built-in into each language that are unique to the language and cannot be reproduced easily.
In other words, there is no way to implement the NodeJS require behavior in Ruby because in Ruby there is no notion of export. When you require a file, every method/class included in the required file are made available to the scope.
In Ruby there are objects and method visibility. The way you have to make a method visible or not is to declare it as public or private/protected.
Well, first consider that Ruby is not Node.js. As Simone Carletti said, there are some features that are unique to each language. Sometimes it's good to take from other language but sometimes it's bad.
There are few things that you must keep in mind:
meth is method invocation, to pass method you use method method: method(:meth) or package it into module/class
you can use class/module by assigning it to some 2nd variable:
class A;
def self.aa; puts 'aa'; end;
end;
New_a = A;
New_a.aa # aa;
eval is dangerous method(you can evaluate unknown code)
Method:
Here is one way you can do. It is not idiot-proof tough. It is not 100% safe(eval). :
file1.rb:
Module.new do
def self.meth1
42
end
def self.meth2
'meth2'
end
end
This file contain module with 2 methods.
I am using Module.new because it returns object that you want. You can assign it later into variable/constant.
I am using self.meth* so you don't have to include but run instantly it like this: module_name.meth1()
req.rb:
def cool_require name
eval(File.read name)
end
Some_variable = cool_require('req.rb')
puts Some_variable.meth1 # 42
puts Some_variable.meth2 # meth2
cool_require reads filename(argument name) and evaluate it(it is just like you would type it in irb/pry)
Some_variable is constant. It won't disappear that easily.
2 last line is how it works. As fair I remember, that's how node.js' require works.

read json in Ruby and set variables for use in another class

The need here is to read a json file and to make the variables which is done from one class and use them with in another class. What I have so far is
helper.rb
class MAGEINSTALLER_Helper
#note nonrelated items removed
require 'fileutils'
#REFACTOR THIS LATER
def load_settings()
require 'json'
file = File.open("scripts/installer_settings.json", "rb")
contents = file.read
file.close
#note this should be changed for a better content check.. ie:valid json
#so it's a hack for now
if contents.length > 5
begin
parsed = JSON.parse(contents)
rescue SystemCallError
puts "must redo the settings file"
else
puts parsed['bs_mode']
parsed.each do |key, value|
puts "#{key}=>#{value}"
instance_variable_set("#" + key, value) #better way?
end
end
else
puts "must redo the settings file"
end
end
#a method to provide feedback simply
def download(from,to)
puts "completed download for #{from}\n"
end
end
Which is called in a file of Pre_start.rb
class Pre_start
#note nonrelated items removed
def initialize(params=nil)
puts 'World'
mi_h = MAGEINSTALLER_Helper.new
mi_h.load_settings()
bs_MAGEversion=instance_variable_get("#bs_MAGEversion") #doesn't seem to work
file="www/depo/newfile-#{bs_MAGEversion}.tar.gz"
if !File.exist?(file)
mi_h.download("http://www.dom.com/#{bs_MAGEversion}/file-#{bs_MAGEversion}.tar.gz",file)
else
puts "mage package exists"
end
end
end
the josn file is valid json and is a simple object (note there is more just showing the relevant)
{
"bs_mode":"lite",
"bs_MAGEversion":"1.8.0.0"
}
The reason I need to have a json settings file is that I will need to pull settings from a bash script and later a php script. This file is the common thread that is used to pass settings each share and need to match.
Right now I end up with an empty string for the value.
The instance_variable_setis creating the variable inside MAGEINSTALLER_Helper class. That's the reason why you can't access these variables.
You can refactor it into a module, like this:
require 'fileutils'
require 'json'
module MAGEINSTALLER_Helper
#note nonrelated items removed
#REFACTOR THIS LATER
def load_settings()
content = begin
JSON.load_file('scripts/installer_settings.json')
rescue
puts 'must redo the settings file'
{} # return an empty Hash object
end
parsed.each {|key, value| instance_variable_set("##{key}", value)}
end
#a method to provide feedback simply
def download(from,to)
puts "completed download for #{from}\n"
end
end
class PreStart
include MAGEINSTALLER_Helper
#note nonrelated items removed
def initialize(params=nil)
puts 'World'
load_settings # The method is available inside the class
file="www/depo/newfile-#{#bs_MAGEversion}.tar.gz"
if !File.exist?(file)
download("http://www.dom.com/#{#bs_MAGEversion}/file-#{#bs_MAGEversion}.tar.gz",file)
else
puts "mage package exists"
end
end
end
I refactored a little bit to more Rubish style.
On this line:
bs_MAGEversion=instance_variable_get("#bs_MAGEversion") #doesn't seem to work
instance_variable_get isn't retrieving from the mi_h Object, which is where your value is stored. The way you've used it, that line is equivalent to:
bs_MAGEversion=#bs_MAGEversion
Changing it to mi_h.instance_variable_get would work. It would also be painfully ugly ruby. But I sense that's not quite what you're after. If I read you correctly, you want this line:
mi_h.load_settings()
to populate #bs_MAGEversion and #bs_mode in your Pre_start object. Ruby doesn't quite work that way. The closest thing to what you're looking for here would probably be a mixin, as described here:
http://www.ruby-doc.org/docs/ProgrammingRuby/html/tut_modules.html
We do something similar to this all the time in code at work. The problem, and solution, is proper use of variables and scoping in the main level of your code. We use YAML, you're using JSON, but the idea is the same.
Typically we define a constant, like CONFIG, which we load the YAML into, in our main code, and which is then available in all the code we require. For you, using JSON instead:
require 'json'
require_relative 'helper'
CONFIG = JSON.load_file('path/to/json')
At this point CONFIG would be available to the top-level code and in "helper.rb" code.
As an alternate way of doing it, just load your JSON in either file. The load-time is negligible and it'll still be the same data.
Since the JSON data should be static for the run-time of the program, it's OK to use it in a CONSTANT. Storing it in an instance variable only makes sense if the data would vary from instance to instance of the code, which makes no sense when you're loading data from a JSON or YAML-type file.
Also, notice that I'm using a method from the JSON class. Don't go through the rigamarole you're using to try to copy the JSON into the instance variable.
Stripping your code down as an example:
require 'fileutils'
require 'json'
CONTENTS = JSON.load_file('scripts/installer_settings.json')
class MAGEINSTALLER_Helper
def download(from,to)
puts "completed download for #{from}\n"
end
end
class Pre_start
def initialize(params=nil)
file = "www/depo/newfile-#{ CONFIG['bs_MAGEversion'] }.tar.gz"
if !File.exist?(file)
mi_h.download("http://www.dom.com/#{ CONFIG['bs_MAGEversion'] }/file-#{ CONFIG['bs_MAGEversion'] }.tar.gz", file)
else
puts "mage package exists"
end
end
end
CONFIG can be initialized/loaded in either file, just do it from the top-level before you need to access the contents.
Remember, Ruby starts executing it at the top of the first file and reads downward. Code that is outside of def, class and module blocks gets executed as it's encountered, so the CONFIG initialization will happen as soon as Ruby sees that code. If that happens before you start calling your methods and creating instances of classes then your code will be happy.

Two versions of each blog post in Jekyll

I need two versions of each of my posts in a very simple Jekyll setup: The public facing version and a barebones version with branding specifically for embedding.
I have one layout for each type:
post.html
post_embed.html
I could accomplish this just fine by making duplicates of each post file with different layouts in the front matter, but that's obviously a terrible way to do it. There must be a simpler solution, either at the level of the command line or in the front matter?
Update:
This SO question covers creating JSON files for each post. I really just need a generator to loop through each post, alter one value in the YAML front matter (embed_page=True) and feed it back to the same template. So each post is rendered twice, once with embed_page true and one with it false. Still don't have a full grasp of generators.
Here's my Jekyll plugin to accomplish this. It's probably absurdly inefficient, but I've been writing in Ruby for all of two days.
module Jekyll
# override write and destination functions to taking optional argument for pagename
class Post
def destination(dest, pagename)
# The url needs to be unescaped in order to preserve the correct filename
path = File.join(dest, CGI.unescape(self.url))
path = File.join(path, pagename) if template[/\.html$/].nil?
path
end
def write(dest, pagename="index.html")
path = destination(dest, pagename)
puts path
FileUtils.mkdir_p(File.dirname(path))
File.open(path, 'w') do |f|
f.write(self.output)
end
end
end
# the cleanup function was erasing our work
class Site
def cleanup
end
end
class EmbedPostGenerator < Generator
safe true
priority :low
def generate(site)
site.posts.each do |post|
if post.data["embeddable"]
post.data["is_embed"] = true
post.render(site.layouts, site.site_payload)
post.write(site.dest, "embed.html")
post.data["is_embed"] = false
end
end
end
end
end

Monkey patch class methods

I'm trying to do some monkey patching in ActiveShipping UPS class .
I need to add a class level method (starting with .self), so here it's what I'm trying to do:
module ActiveMerchant
module Shipping
class UPS < Carrier
def self.process_request(receiver, sender, packages, options = {})
# some code
end
def regular_method
"foobar"
end
end
end
end
Unfortunately when I'm trying to use it:
ActiveMerchant::Shipping::UPS.process_request(receiver etc)
I get an error:
NoMethodError: undefined method `process_request' for ActiveMerchant::Shipping::UPS:Class
from (irb):6
from C:/Ruby19/bin/irb.bat:19:in `<main>'
There is no class method named process_request in original class.
In original UPS class provided in gem there is one static method defined self.retry_safe = true
and I can use it without errors.
I can also use regular_method after creating instance of UPS class.
More details provided:
I'm working with Rails 2.3 ( :-( ) and Ruby 1.9.2. I have no influce on environment.
Monkey patched code is under plugins/my_plugin/lib/active_shipping/ext/carriers/ups.rb
In /active_shipping I have file named extensions.rb in which i have:
require 'active_shipping'
require_relative 'ext/carriers'
require_relative 'ext/carriers/ups'
It deals with loading everything properly (I suppose basing on regular_method beheaviour from first chunk of code in my question).
I try to invoke process_request in one of my Controllers. This part is little tricky, beacuse i'm using sth like this:
MyModel.courier_service.process_request(parameters)
where courier_service, in this case holds the ActiveMerchant::Shipping::UPS class.
I'm still a newbie in Ruby and don't know what sort of details i should provide.
Maybe you want to do it in another way
File patch_classes.rb:
module ActiveMerchantExpand
module Shipping
module ClassMethods
def self.process_request(receiver, sender, packages, options = {})
# some code
end
end
module InstanceMethods
def regular_method
"foobar"
end
end
def self.included(receiver)
receiver.extend ClassMethods
receiver.send :include, InstanceMethods
end
end
end
Then you have to load your class "ActiveMerchant::Shipping::UPS"
and after that you can attach your methods to your class via
Rails.configuration.to_prepare do
require_dependency [[file for ActiveMerchant::Shipping::UPS]]
require 'patch_classes' )
ActiveMerchant::Shipping::UPS.send(:include, ::ActiveMerchantExpand::Shipping)
end
This is from rails plugin writing, i hope this helps.
regards tingel2k
Do you explicitly require file with your monkey patch? If you just put it under your app or lib path without requiring, it wouldn't load because constant ActiveMerchant::Shipping::UPS is defined in gem and it doesn't trigger dependency resolution mechanism.

Resources