Unions and macro methods

I’m trying to create a macro that converts Type definitions in to a JSON Schema for some client side input validation

So far I have this which is working well: crystal play

macro introspect(klass)
  {% klass = klass.resolve %}
  {% klass_name = klass.name(generic_args: false) %}

   # see the play link above
end

However when I do something like: introspect(Bool | String)
I get an error undefined macro method 'Call#resolve'

But I would expect the Bool | String to be a union
and it does work if I do something like this

alias SomeAlias = Bool | String
introspect(SomeAlias)

so wondering if there is a way to resolve that Call macro type into a Union type

There’s no way for the parser to know that the argument to introspect is supposed to be evaluated in the type grammar. It’s just parsed as a call to method Bool.| with argument String.

As a workaround, you can use Union(String, Int32) to express that it’s a type. Even Union(String | Int32) works. You could even use that to wrap the macro argument (Union({{ klass }})) and thus allow introspect Bool | String.

1 Like

Following straight-shoota recommendation, you can modify the macro to cater for Union use-case as in crystal play

2 Likes

Thanks for the explanation and workaround! That’s super useful to know

So one final issue that is eluding me, I’m trying to parse JSON::Serializable objects

        {% properties = {} of Nil => Nil %}
        {% for ivar in klass.instance_vars %}
          {% ann = ivar.annotation(::JSON::Field) %}
          {% unless ann && (ann[:ignore] || ann[:ignore_deserialize]) %}
            {% properties[((ann && ann[:key]) || ivar).id] = ivar.type %}
          {% end %}
        {% end %}

        {% if properties.empty? %}
          { type: "object" }
        {% else %}
          {type: "object",  properties: {
            {% for key, ivar in properties %}
              {{key}}: introspect({{ivar}}),
            {% end %}
          }, required: [
            {% for key, ivar in properties %}
              {% if !ivar.type.nilable? %}
                {{key.stringify}},
              {% end %}
            {% end %}
          ] of String}
        {% end %}

however instance_vars is always empty… crystal play example

instance_vars can only be used inside methods

Try doing the instrospect call from inside a method that you call

1 Like

Maybe the macro interpreter should error if instance_vars is not called as @type.instance_vars?

1 Like

But it works fine when inside a method, so I don’t think it’s correct to do that

I tried this, with the same results


module JSON::Serializable
    macro included
      macro finished
        def self.__generate_json_schema__
          {% begin %}
            {% puts "\n\nprocessing JSON::Serializable for #{@type.name}" %}
            {% puts "Class: #{@type.instance_vars.map(&.name)}" %}
            {% puts "Instance: #{@type.instance.instance_vars.map(&.name)}\n" %}

i assume it needs to be an instance method?

solved it!


  module ::Introspect
    def __generate_json_schema__
      {% begin %}
        {% puts "\n\nprocessing JSON::Serializable for #{@type.name}" %}
        {% puts "Class: #{@type.instance_vars.map(&.name.stringify)}" %}
        {% puts "Instance: #{@type.instance.instance_vars.map(&.name)}\n" %}
      {% end %}
    end
  end

  module JSON::Serializable
    macro included
      extend Introspect
    end
  end

1 Like

Oh, I somehow thought it would only work in a method owned by the receiver. But it works in any method. Never mind.

We could still make instance_vars an error when called outside of method scope.

2 Likes

Definitely! I think we can error if it’s used before the instance vars are all set up.

4 Likes
2 Likes

The final code for anyone interested in using it their projects
Thanks for the help!

3 Likes