DmitryTsepelev/store_model

Passing parameter to Configuration

JoeKaldas opened this issue ยท 13 comments

Is it possible to pass a parameter to the configuration?
So I have a json column I'm validating, but the keys I'm validating depend on an enum
So instead of using multiple configurations for each key in the enum, I want to use just one configuration and pass the enum key to it, since all enums have the same validations logic, just different keys

Hi @JoeKaldas!

I believe you could do it using a plain Ruby: define a module with a method that initializes the attribute and validations by its name and call this method for each attribute:

module StoreModel
  module Model
    def attribute(attribute_name, attribute_type)
      puts "attribute defined: #{attribute_name} (#{attribute_type})"
    end

    def validates(attribute_name, options)
      puts "validation defined: #{attribute_name} (#{options})"
    end
  end
end

module SharedAttributeConfig
  include StoreModel::Model

  def attribute_with_shared_config(attribute_name)
    attribute attribute_name, :string
    validates attribute_name, presence: true
  end
end

class MyModel
  extend SharedAttributeConfig

  attribute_with_shared_config :first
  attribute_with_shared_config :second
end

Hi @DmitryTsepelev

Thank you for your answer. However I think you misunderstood me, let me explain more.

I have a single table database, called Content and this table has an enum column that tells me the record belongs to which "table". So let's say I have 2 enum keys "team" and "project"
And then a json column in content table that has the columns of each table team/project

So let's say team has name/email (again those are dynamic and could change)
And project has title/client

Let me share a bit of the code. So this is the team's configuration file
I have a json schema that I read from where everything is defined.
So Project's configuration would be exactly the same except that I want to set "model" variable to "project" instead
So I don't want to make another configuration that has exactly the same code, just with "model" variable changed

I just want to have one configuration and pass the model variable to it

I hope it's clear

class TeamConfiguration
  model = 'team'
  include StoreModel::Model

  ApplicationController.helpers.exclude_id_from_columns(model).each do |key, value|
      attribute key.to_sym, value[:type].to_sym
  end

  ApplicationController.helpers.get_required_columns_by_validation(model, 'presence').each do |key, value|
      validates key.to_sym, presence: true
  end
end

You cannot do it out of the box using the library DSL, but my initial approach still works, you can generate there configuration classes using inheritance/composition and helper method:

# fake code to make it runnable

module StoreModel
  module Model
    def self.included(base)
      base.extend(Module.new do
        def attribute(attribute_name, attribute_type)
          puts "attribute defined: #{attribute_name} (#{attribute_type})"
        end

        def validates(attribute_name, options)
          puts "validation defined: #{attribute_name} (#{options})"
        end
      end)
    end
  end
end

module ApplicationController
  module_function

  def helpers
    Module.new do
      module_function

      def exclude_id_from_columns(model)
        case model
        when "team" then { name: { type: :string }, email: { type: :string } }
        when "project" then { name: { type: :string }, client: { type: :string } }
        end
      end

      def get_required_columns_by_validation(model, _type)
        { name: { type: :string } }
      end
    end
  end
end

# app code

class ContentConfiguration
  include StoreModel::Model

  def self.configure_as(model)
    ApplicationController.helpers.exclude_id_from_columns(model).each do |key, value|
      attribute key.to_sym, value[:type].to_sym
    end

    ApplicationController.helpers.get_required_columns_by_validation(model, 'presence').each do |key, value|
      validates key.to_sym, presence: true
    end
  end
end

class ProjectConfiguration < ContentConfiguration
  configure_as('project')
end

class TeamConfiguration < ContentConfiguration
  configure_as('team')
end

Hi @DmitryTsepelev

Thanks again.

I did the following

  1. Created a configurations folder under app
  2. Created 3 files under this folder. One for ContentConfiguration, TeamConfiguration, Project Configuration
  3. Here is TeamConfiguration for example
class TeamConfiguration < ContentConfiguration
  configure_as('team')
end

And I have a team model that inherits from content model and has the following

class Team < Content
  attribute :data, TeamConfiguration.to_type
end

But I'm getting following error:

undefined method `type' for TeamConfiguration:Class

Thanks again

Is there a line that calls this #type method in the stacktrace?

Log doesn't say anything. Just this

But the puts for attributes and validations are printed

NoMethodError (undefined method `to_type' for TeamConfiguration:Class):

Looks like I figured it out, the only change is in the ContentConfiguration class (see the comment):

# fake code to make it runnable

module StoreModel
  module Model
    def self.included(base)
      base.include(Module.new do
        def attribute(attribute_name, attribute_type)
          puts "attribute defined: #{attribute_name} (#{attribute_type})"
        end

        def validates(attribute_name, options)
          puts "validation defined: #{attribute_name} (#{options})"
        end
      end)
    end

    def to_type
      puts "StoreModel::Model#to_type"
    end
  end
end

module ApplicationController
  module_function

  def helpers
    Module.new do
      module_function

      def exclude_id_from_columns(model)
        case model
        when "team" then { name: { type: :string }, email: { type: :string } }
        when "project" then { name: { type: :string }, client: { type: :string } }
        end
      end

      def get_required_columns_by_validation(model, _type)
        { name: { type: :string } }
      end
    end
  end
end

# app code

class ContentConfiguration
  def self.configure_as(model)
    self.class.include StoreModel::Model # this line was changed

    ApplicationController.helpers.exclude_id_from_columns(model).each do |key, value|
      attribute key.to_sym, value[:type].to_sym
    end

    ApplicationController.helpers.get_required_columns_by_validation(model, 'presence').each do |key, value|
      validates key.to_sym, presence: true
    end
  end
end

class ProjectConfiguration < ContentConfiguration
  configure_as('project')
end

class TeamConfiguration < ContentConfiguration
  configure_as('team')
end

Hello, now I'm getting the following error and the stacktrace

NameError (undefined local variable or method `attributes' for #<Class:0x00007fb9490c5260>)
Did you mean?  attribute
Traceback (most recent call last):
	35: from bin/rails:6:in `<main>'
	34: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.4/lib/active_support/dependencies.rb:324:in `require'
	33: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.4/lib/active_support/dependencies.rb:291:in `load_dependency'
	32: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.4/lib/active_support/dependencies.rb:324:in `block in require'
	31: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/bootsnap-1.5.1/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:31:in `require'
	30: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/bootsnap-1.5.1/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require_with_bootsnap_lfi'
	29: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/bootsnap-1.5.1/lib/bootsnap/load_path_cache/loaded_features_index.rb:92:in `register'
	28: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/bootsnap-1.5.1/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi'
	27: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/bootsnap-1.5.1/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require'
	26: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/railties-6.0.3.4/lib/rails/commands.rb:18:in `<main>'
	25: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/railties-6.0.3.4/lib/rails/command.rb:46:in `invoke'
	24: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/railties-6.0.3.4/lib/rails/command/base.rb:69:in `perform'
	23: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/thor-1.0.1/lib/thor.rb:392:in `dispatch'
	22: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/thor-1.0.1/lib/thor/invocation.rb:127:in `invoke_command'
	21: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/thor-1.0.1/lib/thor/command.rb:27:in `run'
	20: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/railties-6.0.3.4/lib/rails/commands/console/console_command.rb:102:in `perform'
	19: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/railties-6.0.3.4/lib/rails/commands/console/console_command.rb:19:in `start'
	18: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/railties-6.0.3.4/lib/rails/commands/console/console_command.rb:70:in `start'
	17: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/2.7.0/irb.rb:400:in `start'
	16: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/2.7.0/irb.rb:471:in `run'
	15: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/2.7.0/irb.rb:471:in `catch'
	14: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/2.7.0/irb.rb:472:in `block in run'
	13: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/2.7.0/irb.rb:537:in `eval_input'
	12: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/2.7.0/irb/ruby-lex.rb:150:in `each_top_level_statement'
	11: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/2.7.0/irb/ruby-lex.rb:150:in `catch'
	10: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/2.7.0/irb/ruby-lex.rb:151:in `block in each_top_level_statement'
	 9: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/2.7.0/irb/ruby-lex.rb:151:in `loop'
	 8: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/2.7.0/irb/ruby-lex.rb:154:in `block (2 levels) in each_top_level_statement'
	 7: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/2.7.0/irb/ruby-lex.rb:182:in `lex'
	 6: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/2.7.0/irb.rb:518:in `block in eval_input'
	 5: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/2.7.0/irb.rb:704:in `signal_status'
	 4: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/2.7.0/irb.rb:519:in `block (2 levels) in eval_input'
	 3: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/2.7.0/irb/input-method.rb:290:in `gets'
	 2: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/2.7.0/forwardable.rb:230:in `input='
	 1: from /Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/2.7.0/reline.rb:133:in `input='
/Users/joekaldas/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/store_model-0.8.0/lib/store_model/model.rb:40:in `==': undefined local variable or method `attributes' for #<Class:0x00007fb94d8bf778> (NameError)
Did you mean?  attribute

Hi @DmitryTsepelev
Any help with the previous error?

Sorry, we have a loooong New Year holiday season here in Russia ๐Ÿ™ƒ

So it looks like something is wrong in the order of calls, and in order to debug that I'll have to rebuild the app from scratch (which is kinda time consuming), so let me explain what I tried to do and it will probably help you to fix the problem.

There is a base class (ContentConfiguration) which has the class method configure_as. Each child class calls the method and passes the name of the entity to it. The method does two things:

  1. Configures the class to be a StoreModel (something might be wrong here, you could try to use a regular include in each child model
  2. Configures validations according to the helper methods

It's okay! Appreciate the response :)

Yes I understand the approach you're using. Was just stuck since the error is store model related.
But I'll look into it and let you know if I reach anything.

Thanks a lot again!

@DmitryTsepelev so I figured it out. It was pretty straight forward haha

It's the same approach you suggested. Only difference is I didn't override StoreModel module

I suggest you can also add it to the readme as an inheritance feature.
I'll close the issue now. Thanks again!

module ApplicationController
  module_function

  def helpers
    Module.new do
      module_function

      def exclude_id_from_columns(model)
        case model
        when "team" then { name: { type: :string }, email: { type: :string } }
        when "project" then { name: { type: :string }, client: { type: :string } }
        end
      end

      def get_required_columns_by_validation(model, _type)
        { name: { type: :string } }
      end
    end
  end
end

# app code

class ContentConfiguration
  include StoreModel::Model

  def self.configure_as(model)
    ApplicationController.helpers.exclude_id_from_columns(model).each do |key, value|
      attribute key.to_sym, value[:type].to_sym
    end

    ApplicationController.helpers.get_required_columns_by_validation(model, 'presence').each do |key, value|
      validates key.to_sym, presence: true
    end
  end
end

class ProjectConfiguration < ContentConfiguration
  configure_as('project')
end

class TeamConfiguration < ContentConfiguration
  configure_as('team')
end

Good to know! I'll be very grateful if you could make a documentation PR with your snippet