Alex Craft
Alex Craft

Reputation: 15336

from_json doesn't work with record macro?

Why from_json doesn't work for struct created with record macro?

require "json"

record Stock,
  symbol :           String,
  name :             String

p Stock.from_json %({ "symbol": "MSFT", "name": "Some" })

Error

 13 | new parser
      ^--
Error: wrong number of arguments for 'Stock.new' (given 1, expected 2)

Overloads are:
 - Stock.new(symbol : String, name : String)

Question 2:

The stack trace doesn't have the line number, I tried to use --error-trace but it had no effect, how can I use --error-trace?

> crystal api/crystal/play.cr --error-trace
Showing last frame. Use --error-trace for full trace.

In /crystal-1.1.1-1/src/json/from_json.cr:13:3

 13 | new parser
      ^--
Error: wrong number of arguments for 'Stock.new' (given 1, expected 2)

Overloads are:
 - Stock.new(symbol : String, name : String)

P.S.

I found solution, although would be better if it just worked, without the need to include JSON::Serializable

record Stock,
  symbol :           String,
  name :             String,
do
  include JSON::Serializable
end

Upvotes: 1

Views: 83

Answers (1)

Philipp Claßen
Philipp Claßen

Reputation: 43969

You figured it out already. Regarding the question on whether JSON::Serialize should be always included, it implies JSON support is available (require "json"). Otherwise, it would not compile. I assume that is the reason why it does not get baked in by default.

Regarding the original questions, there is a way to debug macros. If you include {% debug %} at the end of the macro, it will print the generated code. I tried it out in your example by copying the source of the record macro (sources are here), only renaming recordby my_record:

macro my_record(name, *properties)
  struct {{name.id}}
    {% for property in properties %}
      {% if property.is_a?(Assign) %}
        getter {{property.target.id}}
      {% elsif property.is_a?(TypeDeclaration) %}
        getter {{property}}
      {% else %}
        getter :{{property.id}}
      {% end %}
    {% end %}

    def initialize({{
                     *properties.map do |field|
                       "@#{field.id}".id
                     end
                   }})
    end

    {{yield}}

    def copy_with({{
                    *properties.map do |property|
                      if property.is_a?(Assign)
                        "#{property.target.id} _#{property.target.id} = @#{property.target.id}".id
                      elsif property.is_a?(TypeDeclaration)
                        "#{property.var.id} _#{property.var.id} = @#{property.var.id}".id
                      else
                        "#{property.id} _#{property.id} = @#{property.id}".id
                      end
                    end
                  }})
      self.class.new({{
                       *properties.map do |property|
                         if property.is_a?(Assign)
                           "_#{property.target.id}".id
                         elsif property.is_a?(TypeDeclaration)
                           "_#{property.var.id}".id
                         else
                           "_#{property.id}".id
                         end
                       end
                     }})
    end

    def clone
      self.class.new({{
                       *properties.map do |property|
                         if property.is_a?(Assign)
                           "@#{property.target.id}.clone".id
                         elsif property.is_a?(TypeDeclaration)
                           "@#{property.var.id}.clone".id
                         else
                           "@#{property.id}.clone".id
                         end
                       end
                     }})
    end
  end
  {% debug %}
end

Note the {% debug %} at the end. Now when running your example ...

my_record Stock,
  symbol :           String,
  name :             String

... it expands to:

struct Stock
  getter symbol : String

  getter name : String

  def initialize(@symbol : String, @name : String)
  end

  def copy_with(symbol _symbol = @symbol, name _name = @name)
    self.class.new(_symbol, _name)
  end

  def clone
    self.class.new(@symbol.clone, @name.clone)
  end
end

Also, the --error-trace argument should work. I have not tried it with crystal play, but you can use crystal run. The option has to go before the file name:

$ crystal run --error-trace example.cr
struct Stock
  getter symbol : String

  getter name : String

  def initialize(@symbol : String, @name : String)
  end

  def copy_with(symbol _symbol = @symbol, name _name = @name)
    self.class.new(_symbol, _name)
  end

  def clone
    self.class.new(@symbol.clone, @name.clone)
  end
end
In example.cr:70:9

 70 | p Stock.from_json %({ "symbol": "MSFT", "name": "Some" })
              ^--------
Error: instantiating 'Stock.class#from_json(String)'


In /usr/lib/crystal/json/from_json.cr:13:3

 13 | new parser
      ^--
Error: wrong number of arguments for 'Stock.new' (given 1, expected 2)

Overloads are:
 - Stock.new(symbol : String, name : String)

Upvotes: 1

Related Questions