6

For some context, a lot of my code has the same lines of text throughout it (we are using Calabash to do iOS automation, if that gives you an idea).

For example: "all label marked:'#{name}'" is used 8 times in a particular class.

I would prefer to be able to have a constant that uses that text, but if I throw it at the top of the class, of course the variable "name" has not been set yet. Without defining a method that takes a parameter and returns a string, is there a way to do something essentially like this that can exist at the top of the class, but not be evaluated until it's used?:

class ClassName
  extend Calabash::Cucumber::Operations

  @NAME_CONSTANT = "all label marked:'#{name}'"

  def self.method_name(name)
    query("#{@NAME_CONSTANT} sibling label marked:'anotherLabel' isHidden:0")
  end
end

If you use the syntax I mentioned, you get this error: undefined local variable or method `name' for ClassName

Teresa Peters
  • 229
  • 3
  • 14
  • In what version you use ruby? – Arup Rakshit Nov 04 '13 at 18:35
  • Remember [Module#name](http://www.ruby-doc.org/core-2.0.0/Module.html#method-i-name) exist... So done use parameter name as `name` of your method `method_name`. – Arup Rakshit Nov 04 '13 at 18:37
  • Can you show the full code? I want to see the scope of `@NAME_CONSTANT`. – Arup Rakshit Nov 04 '13 at 18:39
  • We are using 2.0. I can't share the full code, but it would probably be at the top of the class for now. – Teresa Peters Nov 04 '13 at 18:42
  • @iOSTester You don't need to provide production code.. Just a dummy one to recreate the same error.. – Arup Rakshit Nov 04 '13 at 18:44
  • I mean, I put a simplified version above so it's not much different from that. All you have to do is have a class to put it in. Edit: can't format here so I guess I'll edit above. – Teresa Peters Nov 04 '13 at 18:50
  • It is working for me,,,Please check your codebase.. `module M;end class ClassName extend M @NAME_CONSTANT = "all label marked:'#{name}'" def self.method_name(name) "#{@NAME_CONSTANT} sibling label marked:'anotherLabel' isHidden:0" end end ClassName.method_name('a') # => "all label marked:'ClassName' sibling label marked:'anotherLabel' isHidden:0"`.. – Arup Rakshit Nov 04 '13 at 18:55
  • Now full error stack please – Arup Rakshit Nov 04 '13 at 18:55
  • In your example, you would want it to print "all label marked:'a' sibling label marked:'anotherLabel' isHidden:0" – Teresa Peters Nov 04 '13 at 18:59

3 Answers3

4

You could use String#% to insert the string later.

class ClassName
    @NAME_CONSTANT = "all label marked:'%{name}'"

    def self.method_name(insert_name)
        query("#{@NAME_CONSTANT} sibling label marked:'anotherLabel' isHidden:0" % {name: insert_name})
    end

    def self.query(string)
        puts string
    end 
end 

ClassName.method_name('test')
#=> "all label marked:'test' sibling label marked:'anotherLabel' isHidden:0"
Justin Ko
  • 46,526
  • 5
  • 91
  • 101
2

I agree with @Sergio. Don't define a constant string that includes a variable. Just use a method. Including a variable in a constant seems like a bad idea. Constants shouldn't be dynamic, by definition.

If you really want to include a variable in a constant string, you can assign a lambda to a contstant, like so:

class ClassName
  extend Calabash::Cucumber::Operations

  NAME_CONSTANT = ->(name) { "all label marked:'#{name}'" }

  def self.method_name(name)
    query("#{NAME_CONSTANT.call(name)} sibling label marked:'anotherLabel' isHidden:0")
  end
end

I removed the @ before the constant, since including it creates a class-level instance variable, not a constant.

I really wouldn't use the code sample I posted, though. Just use a method. Avdi Grimm has a good post called "Do we need constants?" where he describes some of the benefits of using methods instead of constants.

Michael Stalker
  • 1,357
  • 9
  • 15
  • Yes I suppose I should have called it a macro, but Ruby doesn't really have a concept of macros. Some of the strings in the file do not have variables and some do, and none of the strings should manipulatable by developers in those files so "constant" was the closest thing I could think of. I suppose I should have gone with 'macro' anyway. Because we will have several of these per file and we might move these to a constants file eventually, I did not want to use a method which is why I stated that in my question. Thanks for your feedback, I'll try it out! – Teresa Peters Nov 04 '13 at 21:02
  • Not yet, I haven't tried it yet. I'll mark the one I use as the answer. Thanks! – Teresa Peters Nov 04 '13 at 21:18
1

The fundamental issue you're facing is that string interpolation occurs at the time the literal is interpreted and the scope of any referenced variables is determined by the location of the string in the code.

If you put the interpolated string in a method, then it won't have access to the local definition of any variables used in the string. You'd have to pass in the value of any variables used, as in:

def name_constant(name)
  "all label marked:'#{name}'"
end

Alternatively, you'd need to declare the "constant" as an uninterpreted string as follows:

@name_constant = '"all label marked:''#{name}''"'

and then interpret it when you reference it, as follows:

eval(@name_constant)

BTW, I've ignored the issue of this not really being a "constant" and using instance variables vs. class variables.

Peter Alfvin
  • 28,599
  • 8
  • 68
  • 106
  • Yes I suppose I should have called it a macro, but Ruby doesn't really have a concept of macros. Some of the strings in the file do not have variables and some do, and none of the strings should manipulatable by developers in those files so "constant" was the closest thing I could think of. I suppose I should have gone with 'macro' anyway. Thanks for your feedback, I'll try it out! – Teresa Peters Nov 04 '13 at 21:01