Skip to content

Commit

Permalink
Validation#validate implemented. this should become #valid?.
Browse files Browse the repository at this point in the history
  • Loading branch information
apotonick committed May 4, 2014
1 parent 9afb88b commit f6aedd5
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 134 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ Add this line to your Gemfile:
gem 'reform'
```

## Nomenklatura

Reform gives you two things: form objects for UI bla and contracts for API validation, taking the validations from the models into separate classes.


## Defining Forms

You're working at a famous record label and your job is archiving all the songs, albums and artists. You start with a form to populate your `songs` table.
Expand Down
7 changes: 7 additions & 0 deletions lib/reform/form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@

module Reform
class Form < Validation
# self.representer_class.form_class = self
self.representer_class.class_eval do
def self.form_class
Reform::Form
end
end

def aliased_model
# TODO: cache the Expose.from class!
Reform::Expose.from(self.class.representer_class).new(:model => model)
Expand Down
230 changes: 99 additions & 131 deletions lib/reform/form/validate.rb
Original file line number Diff line number Diff line change
@@ -1,174 +1,142 @@
# Mechanics for writing to forms in #validate.
module Reform::Form::Validate
module Writer
def from_hash(*)
nested_forms do |attr|
attr.delete(:prepare)
attr.delete(:extend)

attr.merge!(
:collection => attr[:collection], # TODO: Def#merge! doesn't consider :collection if it's already set in attr YET.
:parse_strategy => :sync, # just use nested objects as they are.
:deserialize => lambda { |object, params, args|

options = args.user_options.dup
options[:prefix] = options[:prefix].dup # TODO: implement Options#dup.
options[:prefix] << args.binding.name # FIXME: should be #as.

# puts "======= user_options: #{args.user_options.inspect}"

object.validate!(params, options)
},
)
end

super
end
end

module Reform
class Form < Validation
module Validate
module Writer
def from_hash(*)
nested_forms do |attr|
attr.delete(:prepare)
attr.delete(:extend)

attr.merge!(
:collection => attr[:collection], # TODO: Def#merge! doesn't consider :collection if it's already set in attr YET.
:parse_strategy => :sync, # just use nested objects as they are.
:deserialize => lambda { |object, params, args|

options = args.user_options.dup
options[:prefix] = options[:prefix].dup # TODO: implement Options#dup.
options[:prefix] << args.binding.name # FIXME: should be #as.

# puts "======= user_options: #{args.user_options.inspect}"

object.validate!(params, options)
},
)
end

module Populator
class PopulateIfEmpty
def initialize(*args)
@form, @fragment, args = args
@index = args.first
@args = args.last
super
end
end

def call
binding = @args.binding
form = binding.get

return if binding.array? and form and form[@index] # TODO: this should be handled by the Binding.
return if !binding.array? and form
# only get here when above form is nil.

if binding[:populate_if_empty].is_a?(Proc)
model = @form.instance_exec(@fragment, @args, &binding[:populate_if_empty]) # call user block.
else
model = binding[:populate_if_empty].new
module Populator
class PopulateIfEmpty
def initialize(*args)
@form, @fragment, args = args
@index = args.first
@args = args.last
end

form = binding[:form].new(model) # free service: wrap model with Form. this usually happens in #setup.

if binding.array?
@form.model.send("#{binding.getter}") << model # FIXME: i don't like this, but we have to add the model to the parent object to make associating work. i have to use #<< to stay compatible with AR's has_many API. DISCUSS: what happens when we get out-of-sync here?
@form.send("#{binding.getter}")[@index] = form
else
@form.model.send("#{binding.setter}", model) # FIXME: i don't like this, but we have to add the model to the parent object to make associating work.
@form.send("#{binding.setter}", form) # :setter is currently overwritten by :parse_strategy.
end
end
end
def call
binding = @args.binding
form = binding.get

return if binding.array? and form and form[@index] # TODO: this should be handled by the Binding.
return if !binding.array? and form
# only get here when above form is nil.

def from_hash(params, *args)
populated_attrs = []
if binding[:populate_if_empty].is_a?(Proc)
model = @form.instance_exec(@fragment, @args, &binding[:populate_if_empty]) # call user block.
else
model = binding[:populate_if_empty].new
end

nested_forms do |attr|
next unless attr[:populate_if_empty]
form = binding[:form].new(model) # free service: wrap model with Form. this usually happens in #setup.

attr.merge!(
# DISCUSS: it would be cool to move the lambda block to PopulateIfEmpty#call.
:populator => lambda do |fragment, *args|
PopulateIfEmpty.new(self, fragment, args).call
if binding.array?
@form.model.send("#{binding.getter}") << model # FIXME: i don't like this, but we have to add the model to the parent object to make associating work. i have to use #<< to stay compatible with AR's has_many API. DISCUSS: what happens when we get out-of-sync here?
@form.send("#{binding.getter}")[@index] = form
else
@form.model.send("#{binding.setter}", model) # FIXME: i don't like this, but we have to add the model to the parent object to make associating work.
@form.send("#{binding.setter}", form) # :setter is currently overwritten by :parse_strategy.
end
)
end
end


nested_forms do |attr|
next unless attr[:populator]
def from_hash(params, *args)
populated_attrs = []

attr.merge!(
:parse_strategy => attr[:populator],
:representable => false
)
populated_attrs << attr.name.to_sym
end
nested_forms do |attr|
next unless attr[:populate_if_empty]

super(params, {:include => populated_attrs})
end
end
attr.merge!(
# DISCUSS: it would be cool to move the lambda block to PopulateIfEmpty#call.
:populator => lambda do |fragment, *args|
PopulateIfEmpty.new(self, fragment, args).call
end
)
end


def errors
@errors ||= Errors.new(self)
end
nested_forms do |attr|
next unless attr[:populator]

def validate(params)
options = {:errors => errs = Errors.new(self), :prefix => []}

validate!(params, options)
attr.merge!(
:parse_strategy => attr[:populator],
:representable => false
)
populated_attrs << attr.name.to_sym
end

self.errors = errs # if the AM valid? API wouldn't use a "global" variable this would be better.
super(params, {:include => populated_attrs})
end
end

errors.valid?
end

def errors
@errors ||= Validation::Errors.new(self)
end

def validate!(params, options)
# puts "validate! in #{self.class.name}: #{params.inspect}"
populate!(params)
def validate(params)
options = {:errors => errs = Validation::Errors.new(self), :prefix => []}

# populate nested properties
# update attributes of forms (from_hash)
# run validate(errors) for all forms (no 1-level limitation anymore)
validate!(params, options)

# here it would be cool to have a validator object containing the validation rules representer-like and then pass it the formed model.
self.errors = errs # if the AM valid? API wouldn't use a "global" variable this would be better.

errors.valid?
end

prefix = options[:prefix]

# sets scalars and recurses #validate.
mapper.new(self).extend(Writer).from_hash(params, options)
def validate!(params, options)
# puts "validate! in #{self.class.name}: #{params.inspect}"
populate!(params)

res = valid? # this validates on <Fields> using AM::Validations, currently.
# populate nested properties
# update attributes of forms (from_hash)
# run validate(errors) for all forms (no 1-level limitation anymore)

options[:errors].merge!(self.errors, prefix)
end
# here it would be cool to have a validator object containing the validation rules representer-like and then pass it the formed model.

private
attr_writer :errors # only used in top form.

def populate!(params)
mapper.new(self).extend(Populator).from_hash(params)
end
prefix = options[:prefix]

# sets scalars and recurses #validate.
mapper.new(self).extend(Writer).from_hash(params, options)

res = valid? # this validates on <Fields> using AM::Validations, currently.

# The Errors class is planned to replace AM::Errors. It provides proper nested error messages.
class Errors < ActiveModel::Errors
def messages
return super unless Reform.rails3_0?
self
options[:errors].merge!(self.errors, prefix)
end

# def each
# messages.each_key do |attribute|
# self[attribute].each { |error| yield attribute, Array.wrap(error) }
# end
# end

def merge!(errors, prefix)
prefixes = prefix.join(".")

# TODO: merge into AM.
errors.messages.each do |field, msgs|
field = (prefix+[field]).join(".").to_sym # TODO: why is that a symbol in Rails?
private

msgs = [msgs] if Reform.rails3_0? # DISCUSS: fix in #each?

msgs.each do |msg|
next if messages[field] and messages[field].include?(msg)
add(field, msg)
end # Forms now contains a plain errors hash. the errors for each item are still available in item.errors.
end
end

def valid? # TODO: test me in unit test.
blank?
def populate!(params)
mapper.new(self).extend(Populator).from_hash(params)
end
end # Errors

end
end
end
3 changes: 2 additions & 1 deletion lib/reform/representer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ def clone_config!
def self.inline_representer(base_module, name, options, &block)
name = name.to_s.singularize.camelize

Class.new(Form) do
puts "self: #{self}"
Class.new(form_class) do
# TODO: this will soon become a generic feature in representable.
include *options[:features].reverse if options[:features]

Expand Down
Loading

0 comments on commit f6aedd5

Please sign in to comment.