Working around the lack of context in Sinatra's route methods
Asked Answered
T

3

7

I have been having an issue with missing instances and nilClass errors when calling my routes. After delving around within the source it seems like the generate_method call basically creates a new method using the block of the initial method.

get "/" do
    @some_local_instance.do_something()
end

So in the above method there could very well be a local variable within that class called some_local_instance, however when the rote is actually evaluated it has no context as to where the method was defined, so it will fail.

The reason I ask is because as part of my script I have external classes which are loaded when Sinatra is loaded which register routes and when those routes are called I need to access some local variables on these classes. An example would be:

class SomeRouteClass
    def initialize(sinatra, calculator)
        @calculator = calculator
        @sinatra = sinatra
    end

    def setup_routes
        @sinatra.get "/add" do
            return @calculator.add(1,1)
        end
    end
end

class Calculator
    def add(a,b)
        return a+b;
    end
end

sinatra = Sinatra.new
calculator = Calculator.new

routing_class = SomeRouteClass.new(sinatra, calculator)
routing_class.setup_routes

sinatra.run!

Forgive any spelling/syntax mistakes this is just a quick example, but as you can see a class registers routes and when that route is hit returns some value generated by an instance of the calculator it took when it was instantiated.

Problem I have is that in this example when I try and run the /add route it tells me that @calculator is a nilClass, and I believe it to be down to the way that Sinatra just takes the block of code without context. This seems fine for any simple template rendering, but if you need to do anything more fancy, or want to keep your code modular by not using statics and singletons you do not seem to have any way around this...

Are my assumptions correct here? and if so is there any way to keep context as it feels like it is forcing me to write bad and hard to maintain code if I have to write everything as statics and singletons to interact from a route.

== Edit ==

Have restructured the question and content to more accurately reflect the actual problem, now that I have a firmer understanding of the library.

Television answered 22/11, 2011 at 11:12 Comment(2)
After looking through the source some more, it seems that it does just copy and paste the method that is used with a route into a new method using define_method. So there is no context outside of that method... which implies that the only way around that would be to make everything static, which just seems wrong... as its harder to change and test this way... I could be completely wrong about what its doing though as Ruby is still a new language to me.Television
Have been trying to use the const object pattern to get around this, but this is still making things REALLY nasty as EVERYTHING has to be static for this to work... is there a reason why they copy the method body into a new method so it loses all context? as I cannot see any benefit to it, just drawbacks... although again I am new to this so may not fully understand some complication which requires this. Only thing I can think of is that the method body object may be cleaned up or the method may be updated or taken away from the instance before it is called, however that doesnt seem likelyTelevision
T
0

I may not accept this answer, but after doing more research it may be that in a dynamic language like Ruby static classes are not such as nightmare from a maintenance point of view.

It seems that most major Ruby libraries work against static instances (or consts) which get setup then used... this does still seem a little odd to me, as in a database provider point of view. It is very easy to just call your database static class and connect to your database and then start querying, however what if you need to connect to 2 separate databases at the same time. You would need to keep swapping servers with the same static class which would be troublesome.

Anyway it seems at the moment like the answer is just make a constant for everything you need to expose to a route, then when you are testing just set that const to a mock. It still seems a bit crazy calling these things consts, when really they are not consts in the true sense of the word as they can be changed at any time... like many things to new ruby developers it just seems confusing for the sake of it (i.e elsif, @@blah, variable case defining if its const or not).

As I said I will not accept this answer if someone else can show me a better pattern of doing this, but for the moment will give it a few more days.

Television answered 24/11, 2011 at 9:33 Comment(1)
It looks like with the use of helper you can put some of the logic within there but still doesn't feel that nice. I am looking at Padrino as that at least provides a controller and per controller helper by the looks of it, encapsulating your data and logic better, but this is all just built on top of sinatra, so there must be the ability to do this within the default Sinatra build.Television
F
0

The block passed to get is evaluated in a different context than the Calculator object. Sinatra is probably calling instance_eval or one of its cousins. However, it should be possible to capture local variables from the surrounding scope, using something like the following (untested, alas) approach:

def setup_routes
    calculator = @calculator
    @sinatra.get "/add" do
        return calculator.add(1,1)
    end
end
Fluoroscopy answered 7/1, 2012 at 22:39 Comment(0)
C
-2
class SomeRouteClass
    def initialize(sinatra, calculator)
        @calculator = calculator
        @sinatra = sinatra
    end

    def calculator
        @calculator
    end

    def setup_routes
        @sinatra.get "/add" do
            return calculator.add(1,1)
        end
    end
end
Carruth answered 22/11, 2011 at 13:52 Comment(1)
Causes an exception: undefined local variable or method `calculator' for #<#<Class:0x2b330e0>:0x29c9a60>, I see what you are trying to do there, but it still seems odd. As surely when you register a route you are saying for PATTERN-X call METHOD-Y, but does Sinatra just pluck out the method and bin the class around it? so it no longer has any context to the scope outside of that method?Television

© 2022 - 2024 — McMap. All rights reserved.