How to implement a dsl module?

I'm quite a newbie in ruby who started to learn metaprogramming techniques in this language. Now I'm trying to write a DSL module to be able to annotate classes like in RoR. Unfortunately, I still don't get some things on this topic which causes some troubles in realisation.

Here's a code example of a tag annotation:

module Annotations
  def self.included(host_class)
    host_class.extend(ClassMethods)
  end

  def tags(*values)
    if values.empty?
      if defined? @tags
        @tags
      else
        self.class.tags(values)
      end
    else
      @tags = []
      values.map { |value| validate_tag(value) }.each { |tag| @tags << tag}
    end
  end

  module ClassMethods

    def tags(*values)
      unless defined? @tags
        superclass_tags = self.superclass.tags if self.superclass.respond_to?(:tags)
        @tags = superclass_tags&.any? ? superclass_tags : []
      end

      if values.empty?
        @tags
      else
        if self.superclass.respond_to?(:tags)
          values.map { |value| validate_tag(value) }.each { |tag| @tags << tag}
        else
          @tags = values.map { |value| validate_tag(value) }
        end

        @tags
      end
    end

    alias_method :tag, :tags

    def validate_tag(tag)
      raise 'Tag should be less than 15 chars.' if tag.to_s.length > 15
    end
  end
end

class Foo
  include Annotations
  tags :x1, :x2
end

When I try to execute it, then it will be always the same wrong output.

Foo.tags
=> [nil, nil]

Foo.tag 'c'
Foo.tags
=> [nil]

foo2 = Foo.new

foo2.tags :c5, :c9
foo2.tags
=> [nil, nil]
# etc...

Could you help me to improve code/make me understand where I've made a mistake?


Solution 1:

The only problem is in your validate_tag method. The guard is checking if the length of the tag as a string is greater than 15 and raising an exception if that happens, but there's no value explicitly added for when that doesn't happen. And in that case Ruby returns nil.

You could try updating it to return the tag if tag.to_s.length > 15 returns false:

def validate_tag(tag)
  raise 'Tag should be less than 15 chars.' if tag.to_s.length > 15

  tag
end

Solution 2:

Your validate_tag method does not return tag, but you are mapping over your values argument in tags and calling validate_tag(value), converting an array like [:foo, :bar] into [nil, nil].