Help with creating an API
-
For my next major plugin, I am planning to create an API to go along with it. However, I am not very familiar with good programming style, etc.
I was wondering if some of the Ruby gurus (you know who you are ) could offer some suggestions and 'rules of thumb' for creating an API. Maybe some things have already been covered in the 'Ruby do's and don'ts' thread. Here are some questions I have already:
- Is it bad to add methods to existing SketchUp classes?
- Should I always define new classes within a module?
Thanks!
-
- yes - high risk to redefine other scripts bad behaviour
- yes - this way you can have your own namespace and dont clash with other classes
API creation tips:
- documentation + examples - think that every SU Ruby developer started with box.rb
- 80/20 rule - provide a small number of methods that do most of the job
- check and raise errors on bad input values
- verify everything - if it can go wrong, it will
- have another pair of eyes check the code - if you stay too much on something you will overlook things
good luck and let us know if you need any help
-
I agree with Coen. In fact at 3dbasecamp I felt like I was visiting family. it was so nice to be with a group of people that speak the same language (SUbonics)this forum is like a home away from home and it's great to see such collaboration.
-
@unknownuser said:
- check and raise errors on bad input values
What is the proper way to do this? For example, let's say I define a method that takes a SketchUp::Face as an argument. Should I be checking at the start of the method if the argument is the correct class and is valid? Should I be using Ruby Exception classes for situations like these or is there a simpler way to do it? Can you please give an example of proper error handling style for an API? Thanks
-
Here's an example of a method that requires a face and a calling method.
Todd
def whaats_method(face) if (!(face.is_a? Sketchup;;Face)) then raise ArgumentError, "You must pass a face to this method", caller ; end ; puts "a face was passed" end def outer_call am = Sketchup.active_model face = nil ; face = am.selection[0] if am.selection.count==1 whaats_method(face) end
-
Also, check out progressbar.rb on Smustard for a real life example that is used a ton.
Todd
-
@whaat said:
@unknownuser said:
- check and raise errors on bad input values
What is the proper way to do this? For example, let's say I define a method that takes a SketchUp::Face as an argument. Should I be checking at the start of the method if the argument is the correct class and is valid? Should I be using Ruby Exception classes for situations like these or is there a simpler way to do it? Can you please give an example of proper error handling style for an API? Thanks
I would not use exceptions but simply well documented return codes or behaviors in case of errors. The reason is that your API will be used by programmers, who know what to do in case of error. Exceptions are powerful, but you need to track and intercept them along the whole calling chain, without knowing if this is an error that can be addressed in context, or one that stop the whole application.
Look at the Sketchup API, where there is no exception raised externally. Personally, I only use exception handling within my code, where I can decide what to do with it.
In all cases, it is recommended to check the arguments (class, value), and very often you can take benefit of this check to adapt. For instance, you can easily manage a method taking a face or a list of faces by checking the class of the arguments.
Finally, it is a good idea to use inline proc (i.e. block and yield) to allow some extensions of the API, provide enumeration capabilities, handle unexpected errors, etc....
Say for instance, you want to assign a material while you create faces, based on criterias that only the calling program knows. You could simply have a method to be invoked like this:process_selection(...) do |face| if face.is_triangle? "blue" else "red" end end
Your method would then simply use the returned color to paint the newly created faces.
Of course, the above is my personal feeling with Sketchup Ruby. In other contexts, exceptions are useful and a good practice.
-
@unknownuser said:
Look at the Sketchup API, where there is no exception raised externally.
This is not correct. sketchup.rb, which provides helper methods, will raise an exception on an inputbox if a numeric type is called for and the user does not enter a numeric type.
In my opinion, an API is the best place to raise an exception, especially when validating input parms.
Did you ever see this Ruby Challenge? (Sorry, apparently color tags are not recognized inside code tags.)
@unknownuser said:
This challenge is one that I've already solved, but it took some "Ruby Thinking" for this old procedural programmer to figure out. Once I did though, it was like peeling back another layer of that Object Oriented Onion skin for me.
Anyway, here we go.
With this challenge, you get to write a script (short, preferably) that presents a dialog box to the user within SketchUp. There are two values to get from the user: the user's age and height. You will output a popup that tells the user how many units they have grown, on average, per year.
Here are the constraints:
-
You have to use the .inputbox() method. This is the one that is defined in sketchup.rb. You cannot call UI.inputbox from your script.
-
You cannot use global variables
-
You have to operate in the user's preference for units, accepting and outputting imperial or metric measurements.
-
You have to allow the user to enter a space between the number and the unit, as in "95 mm".
-
When initializing the default height variable (4' if imperial or 100cm if metric) you have to use the clause:
height = "100cm".to_l
or
height = "4'".to_l- When initializing the default age (25), you have to use the clause:
age = 25
- You have to accept any input from the user. If any invalid data is entered, you must redrive the dialog, telling the user what the error was, if there was an error, but without putting up a messagebox or allowing any other popups.
A bit more involved than last week, but perhaps you will gain something from it.
Cut-off time is Friday at 10PM, GMT -6 hours.
Todd
And the solution with commentary:
@unknownuser said:
Well, I feel a little bit like the Lone Ranger this week, but that's OK. Maybe more can participate next time.
Here are the high points of the solution I came up with. Comments/Critiques/Questions are certainly welcome.
Since one of the requirements for the clallenge was to use the inputbox() method defined in the sketchup.rb ruby script, one has to make sure it is loaded. The require statement does this.
**
require 'sketchup.rb'
**
One of the pitfalls of this challenge is the .to_i method to convert a string to an Integer. It does not raise an exception if you pass invalid data to it. If you pass non-numeric data to it, it simply returns a zero. (Yuk, spit, gross, BAD RUBY! BAD!).
Therefore, I chose to extend the String class within Ruby to add a new method to verify a string was only composed of numbers. If the new method integer? returns true, I can safely call the .to_i method. If it returns false, the user entered bad data. (Note I would not by habit extend the String class in production code.)
**
> class String > def integer? # my method name > self.each_byte {|c| > return false if !c.chr.to_s.between?("0","9") > } > true ; > end ; > end # class String
**
In the initialize method below, you can see in blue where I incorporated the local variable assignments for age and height required by this week's Challenge.
**
class UserStats > > attr_accessor ;age, ;height ; > > def initialize([color=blue]age=25,height="4'".to_l[/color]) ; > @age = age ; > @height = height ; > end ; > > def height_per_year ; > (@height/@age).to_l > end ; > > def getdata() ; > p1 = "Enter your Age" > p2 = "Enter your Height" ; > [color=red]title = "Enter Your Statistics" ;[/color] > [color=green]begin[/color] > values = [@age.to_s,@height.to_s] ; > results = inputbox([p1,p2],values,title) ; > [color=green]raise "Exit"[/color] if !results ; > [color=green]begin[/color] > @age = results[0] ; > @height = results[1] ; > > # Since .to_i fails to raise an exception for bad input, we have > # to validate the value ourself. If bad, raise an excpetion. > > [color=green]raise "Data Value Missing"[/color] if (@age.empty? || @height.empty?) > [color=green]raise "Invalid Age"[/color] if (@age.to_i==0 || !@age.integer?) > > @age = @age.to_i ; > @height = @height.tr(' ','').to_l ; > > [color=green]rescue => error [/color] > [color=red]title = "Please Enter Valid Statistics" ;[/color] > [color=green]raise[/color] > [color=green]end ;[/color] > [color=green]rescue => error [/color] > [color=green]raise "User Cancel"[/color] if [color=green]error.message[/color] == "Exit" ; > [color=red]Sketchup.set_status_text([color=green]error.exception[/color],SB_PROMPT) ;[/color] > [color=green]retry[/color] > [color=green]end ; [/color] > Sketchup.set_status_text("",SB_PROMPT) ; # clear any error messages > end ; # def getdata > > end ; # class Userstats
**
Above, you can see the code is riddled with begin/rescue/end clauses, with an occasional retry or raise thrown in. These are the key clauses to use for this week's challenge. The rescue clause inside a begin/end block will trap any exceptions raised, either on purpose, or accidentally.
One of the keys to getting past this week's challenge is this line of code:
values = [@age.to_s,@height.to_s] ;
This is because the inputbox method in sketchup.rb has it's own wrapper for exception handling. How it works is that inside SketchUp, the class of the values are saved on entry, and the data supplied by the user is then checked by SketchUp before it is passed back to sketchup.rb. If you define a field as a length, via the .to_l method, you are hosed in regards to this weeks challenge, since SketchUP will raise an Arguement exception and then the sketchup.rb inputbox method displays the popup. The way to get around this datatype (class) checking is to make sure you only pass a String to the inputbox method! Tricky, eh?! hehehe!
Finally, the driver code. No big deal here. An instance of class UserStats is created and kept in variable person. By default, person.age = 25 and person.height is 4 feet tall.
**
> person = UserStats.new ; > > [color=green]begin[/color] > person.getdata ; > UI.messagebox("You have grown #{person.height_per_year} for #{person.age} years.") ; > [color=green]rescue[/color] > [color=green]end ;[/color] >
**
The final rescue allows the script to end gracefully. If the user presses ESC or CANCEL in the dialog, an exception called "User Cancel" is raised, and that gets the code out of the getdata method and back into this short mainline portion of the code (or, the user finally entering valid data gets us back too!).
In red above are the different means to tell the user that bad data was entered.
A lot of thinking this week. I've attached the source if you want to run it.
Todd
(Edit:
The clause:
@height = @height.tr(' ','').to_l ;
addresses another part of the challenge; to be able to accept a non-standard length from the user, like "95 mm" with an embedded blank. The .tr translate method does this by translating all blanks to nil, AKA, removes them.
/Edit)
-
-
Thanks Todd and Fredo. very useful info
-
Todd,
Sorry for misunderstanding. I was just saying that, while you can or should use exceptions in your own code, you should try to provide 'error-free' API to external programmers, by trapping all problems and documenting the rules and error conventions of your interface.
In your example Challenge #2, I just notice that this is what you do, since any call to your GetData() method, will never raise an exception!
Fredo
PS: I have two small remarks about your code sample, for the Integer?() method:
-
Not a good habit to extend Ruby built-in class. Just imagine I do the same (knowing that the name "integer?" is quite natural) and override your code by mine. Better have a neutral method like Fredo6.Integer?(), so that at least you and me control ownership.
-
I would rather use the following code to transform a string into an integer:
def string_to_integer(s) (s && s.strip =~ /\A(\+|\-|\s*)(\s*)(\d*)\Z/) ? ($1.strip + $3).to_i ; nil end
This returns the integer number if the string is valid, otherwise nil
- A more powerful variant allows the user entering mathematical expressions that would normally evaluate to an integer
def string_to_integer2(s) return nil if s == nil || s =~ /[^\s\+\-\*\/\%(\)\d]/ begin eval "(#{s}) + 0" rescue Exception => detail nil end end
For instance, string_to_integer2("(2 + 3) * 4") will return 20. The only issue with eval() is with security, as eval() could be used to delete files on your disk and other nasty things by accident. Hence, the first line to check that you only have digits with operation signs and parentheses.
Fredo
-
Advertisement