Open DanRathbun opened 2 years ago
Dealing with Class Identifiers in the type_convert()
method:
EDIT (10-01-2021): The snippet below is not the final version of this method. There are input value conversion exceptions also to deal with. For more, see:
Example:
module HtmlUI
class InputBox
def type_convert(options, values)
values.each_with_index.map { |value, index|
default = options[:inputs][index][:default]
# Check for a class identifier:
if default.is_a?(Class)
if default == Length
value.to_l
elsif default == Float
value.to_f
elsif default == Integer
value.to_i
elsif default == TrueClass || default == FalseClass
value
else # String assumed
value.to_s
end
else # it's not, so "sniff" the given default type:
case default
when Length
value.to_l
when Float
value.to_f
when Integer
value.to_i
when TrueClass, FalseClass
# We're getting true/false values from Vue, no need to post-process.
value
else
value.to_s
end
end
}
end
end # class Inputbox
# ... the compatibility method defintion ...
end # module HtmlUI
Hmmm... I think I can reduce the above to ... (but I think the above would eval faster and be more understandable) ...
EDIT (10-01-2021): Note that I did not go with the paradigm below as it is a confusing read and the quirky nature of the ===
methods might easily lead to breakage in the future if someone makes changes. I went with the pattern in the post above.
module HtmlUI
class InputBox
def type_convert(options, values)
values.each_with_index.map { |value, index|
default = options[:inputs][index][:default]
# Allow class identifier || literal values as defaults:
if Length === default || (default.is_a?(Class) && default == Length)
value.to_l
elsif Float === default || default == Float
value.to_f
elsif Integer === default || default == Integer
value.to_i
elsif TrueClass === default || default == TrueClass ||
FalseClass === default || default == FalseClass
value
else # String assumed
value.to_s
end
}
end
end # class Inputbox
# ... the compatibility method defintion ...
end # module HtmlUI
Length
requires special handling because of the non-standard implementation of Length#==
.
It raises an ArgumentError
if a class object is passed in (weirdly not a TypeError
.) ie ...
"Error: #<ArgumentError: comparison of Length with Class failed>"
Normally all class #==
methods accept a class object and return false
for instance receiver objects. Ie, in the case of a Float
:
0.0 == Float
#=> false
... and ...
Float === 0.0
#=> true
Also TODO:
get_options_args()
in class ArgumentParser
needs some work to accept class identifiers in the following conditional:
Beginning line 97 ... after line 93 has initialized input[:value]
to an empty String ...
# Default
if default = arguments[:defaults][index]
input[:default] = default
input[:value] = default
end
This is currently testing for a nil
value (which happens if the coder does not pass a default value or explicitly passes nil
.)
So if it is nil
(falsie), input[:value]
will be left as an empty String, and input[:default]
will not be set at all which will return nil
if the options
hash is asked for it.
We will want class identifiers as the value for the input[:default]
key (so type_convert()
can test for them,) ...
but in that case we do not want to set input[:value]
. We want it to remain blank, (ie no default but a return type.)
UPDATED (10-01-2021): In the snippets below, I've broken out the arguments
hash out into local variables.
So, defaults
was arguments[:defaults]
and lists
was arguments[:list]
.
# Default and Value:
default = defaults[index]
if default.nil?
input[:default] = '' # Leave :value empty
else
# Set input[:default] to a literal value or a class identifier:
input[:default] = default
# Only set input[:value] if it's not a class identifier:
input[:value] = default unless default.is_a?(Class)
end
And also in the next section setting the input[:default]
and input[:value]
for dropdown controls ...
# Options
list = lists[index]
if list && !list.empty?
input[:type] = 'dropdown'
if input[:default] != '' && !default.is_a?(Class)
# Check if we should leave render control as unchosen:
if !input[:value].empty? && !list.include?(default)
# NOTE: The non-matching warning is generated above.
input[:value] = ''
end
end
input[:options] = list
end
I'm still developing this idea as time permits. (Finalized per posts below)
... thinking out loud ...
The dialog JS side does nothing with the defaults. The Ruby-side sets the value to the default before the json is passed over. And then the VueJS is just returning the values as existing upon Accept.
So I think, we don't need to serialize @default
in the JSON (HtmlUI::Input#as_json
) for the dialog, as it is not used.
But get_options_hash()
uses HtmlUI::Input#as_json()
to map each of the :inputs
arrays into a hash of input control properties. So we still need @default
included in those hashes as the :default
property.
However, it does look like JSON serializes a Ruby class identifier as it's string name. This is because in the absence of a ::json_create
class method for an object, the object's #to_s
method is called. For a class or module object this defaults to #name
, so we get the class names.
hash = {"a" => Float, "b" => Integer, "c" => Length}
json = hash.to_json
json.inspect
#=> "{\"a\":\"Float\",\"b\":\"Integer\",\"c\":\"Length\"}"
But the JSON library does not convert them back into Ruby Class
objects. ...
new_hash = JSON.parse(json)
#=> {"a"=>"Float", "b"=>"Integer", "c"=>"Length"}
I was worried that since SketchUp API classes are not yet JSON compatible, that either we'd not get through JSON serialization or that JavaScript would see an undefined Length
object identifier and give us an error.
So, the takeaway is that a Length
class identifier as a default should not upset the dialog's JS because it is passed over as a string name. True also for any other acceptable class identifier.
OK. Update. I finalized this last evening. (which was 30-SEP-2021)
For the compatibility signature in ArgumentParser#get_options_args()
, I updated the setting of :default
and :value
per the snippets shown (and updated 10-01-2021) two posts above.
Also, I was getting the class identifier strings coming through into the input values and displaying in the dialog when using the advanced hash arguments. So I needed to prevent this and the best place was the as_json()
method of the Input
superclass:
require 'json'
module Example::HtmlInputBox
module HtmlUI
# The superclass of all HtmlUI input control classes.
class Input
# Called by the class constructor after instantiating the new
# instance object to set instance variables, etc.
def initialize(label: '', default: nil, options: [])
@label = label
@default = default
@options = options
end
# Returns a hash, that can be turned into a JSON String object
# representing this object's properties.
#
# Any class identifier in `@default` are converted to class name
# strings by the JSON library. These class names must not be copied
# into the `:value` of the output hash.
#
# @return [Hash]
def as_json(*)
default = @default.nil? ? '' : @default
value = default.is_a?(Class) ? '' : default
{
label: @label,
default: default,
value: value,
options: @options,
type: self.class.name.downcase.split('::').last
}
end
# Returns a JSON String representing this instance object's properties.
#
# @return [String] The object properties in JavaScript Object Notation.
def to_json(*args)
as_json.to_json(*args)
end
end # class
end # module
end # module
No Current Way to Specify Return Type Without a Default Value.
As discussed in the last PR comments, when a coder passes
nil
or otherwise does not specify a default value (theTextbox
class constructor usesnil
as a default,) and the result returned is aString
object by default. This is the same behavior had the coder purposefully passed""
(an emptyString
) as the argument for the input's default. But in this latter case (the purposeful emptyString
) is an explicit directive that the coder wants aString
object returned bytype_convert()
.But a coder may not want a
String
result but also may wish for the input to be blank, so they cannot pass a default value.In those previous comments, I had entertained the idea that specific subclasses of
Textbox
such asTextboxLength
,TextboxFloat
,TextboxInteger
,TextboxTime
, etc. But this would be clunky, and the easiest solution came to me whilst sitting on the "thinking throne". ;)1 - There needs to be a documentation note that informs coders that no default or
nil
for a default will produce aString
(just as if they had passed""
.)2 - The answer for result typing without a default value is simple. (It's a "Doh!" forehead smacker.)
We make changes that allow coders to pass class identifiers in place of a default value. Ie ...
Advanced inputs ...
If the end user leaves these "typed" fields blank, then the value will be
0
(forInteger
),0.0
(forFloat
) and0.000"
(forLength
).The coder is then responsible for dealing with the zero values.