nform 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 71b2f87653d2d3d13a8bbad3e3c312b7bbf7d22a
4
+ data.tar.gz: 5ce2e00185bb43437f9572183a228082799b7d97
5
+ SHA512:
6
+ metadata.gz: 76f4261d63c26976981e7eaf961e5f0237ba0c737bc26e867a06aad2403f71d36f856078a60573e17e2c164b687f03e2e9238e6ae43b25ad9905a12c234d45dc
7
+ data.tar.gz: abb4f49d0931d307ba9f88b163f654c6556635c77d7cbec482ceecb6579a7b229c9471fc1bb3db30632152fc30b7e3e396f2527aef03f964c65b2f927d039cf2
@@ -0,0 +1,105 @@
1
+ require 'active_support/core_ext/hash'
2
+ require 'nform/coercions'
3
+
4
+ module NForm
5
+ module Attributes
6
+ def attribute(name,coerce:nil,required:false,default:nil)
7
+ attribute_set[name.to_sym] = {coerce: coerce, required: required, default: default}
8
+ end
9
+
10
+ def attribute_set
11
+ @attribute_set ||= {}
12
+ end
13
+
14
+ def undefined_attributes(option)
15
+ unless %i|raise ignore|.include?(option)
16
+ raise ArgumentError, "Unknown option `#{option}` for undefined attributes. Options are :raise or :ignore"
17
+ end
18
+ @undef_attr = option
19
+ end
20
+
21
+ def __undef_attr
22
+ @undef_attr ||= :raise
23
+ end
24
+
25
+ def define_attributes
26
+ attribute_set.each do |name,options|
27
+ define_method(name) do
28
+ instance_variable_get("@#{name}")
29
+ end
30
+
31
+ # TODO: must use coercion set
32
+ c = get_coercion(options[:coerce])
33
+ define_method("#{name}=") do |input|
34
+ instance_variable_set("@#{name}", c.call(input,self))
35
+ end
36
+ end
37
+ end
38
+
39
+ def get_coercion(coerce_option)
40
+ case
41
+ when coerce_option.nil?
42
+ proc{|n| n }
43
+ when coerce_option.is_a?(Symbol)
44
+ NForm::Coercions.fetch(coerce_option)
45
+ when coerce_option.respond_to?(:call)
46
+ coerce_option
47
+ when coerce_option.is_a?(Enumerable)
48
+ chain = coerce_option.map{|o| get_coercion(o) }
49
+ proc do |input,scope|
50
+ chain.reduce(input){|i,c| c.call(i,scope) }
51
+ end
52
+ else
53
+ raise Error, "Invalid coerce option given"
54
+ end
55
+ end
56
+
57
+ def self.extended(base)
58
+ base.include(InstanceMethods)
59
+ end
60
+
61
+ module InstanceMethods
62
+ def initialize(input={})
63
+ i = input.symbolize_keys
64
+ require_attributes!(i)
65
+ self.class.define_attributes
66
+ set_attributes!(i)
67
+ set_missing_defaults
68
+ end
69
+
70
+ def to_hash
71
+ self.class.attribute_set.each.with_object({}) do |(k,v),memo|
72
+ memo[k] = send(k)
73
+ end
74
+ end
75
+
76
+ private
77
+ def require_attributes!(input_hash)
78
+ required = self.class.attribute_set.map{|name,options| name if options[:required]}.compact
79
+ missing = (required - input_hash.keys)
80
+ if missing.any?
81
+ raise ArgumentError, "Missing required attributes: #{missing.inspect}"
82
+ end
83
+ end
84
+
85
+ def set_attributes!(input_hash)
86
+ input_hash.each do |k,v|
87
+ if self.class.__undef_attr == :raise
88
+ raise ArgumentError, "Undefined attribute: #{k}" unless respond_to?("#{k}=")
89
+ end
90
+ send "#{k}=", v if respond_to?("#{k}=")
91
+ end
92
+ end
93
+
94
+ def set_missing_defaults
95
+ self.class.attribute_set.each do |name, options|
96
+ default = options[:default]
97
+ unless default.nil?
98
+ send("#{name}=",default) if send(name).nil?
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+
@@ -0,0 +1,219 @@
1
+ require 'nform/core_ext'
2
+ require 'active_support/core_ext/string'
3
+
4
+ module NForm
5
+ class Builder
6
+ include HTML
7
+
8
+ attr_reader :object, :form_class
9
+ def initialize(object, id: nil, form_class: nil, action: nil, method: nil)
10
+ @object = object
11
+ @form_id = id
12
+ @form_class = form_class
13
+ @action = action
14
+ @http_method = method
15
+ end
16
+
17
+ def form_id
18
+ @form_id || object_name.dasherize
19
+ end
20
+
21
+ def action
22
+ @action or
23
+ "/#{collection_name.dasherize}" + (new_object? ? "" : "/#{object.id}")
24
+ end
25
+
26
+ def http_method
27
+ @http_method || (new_object? ? "POST" : "PATCH")
28
+ end
29
+
30
+ def new_object?
31
+ object.respond_to?(:new?) ? object.new? : true
32
+ end
33
+
34
+ def object_name
35
+ @object_name || detect_object_name(object).underscore
36
+ end
37
+
38
+ def collection_name
39
+ object_name.pluralize
40
+ end
41
+
42
+ def method_tag
43
+ tag(:input, type:"hidden", name:"_method", value:http_method) if http_method != "POST"
44
+ end
45
+
46
+ def errors
47
+ @errors = if object.respond_to?(:errors)
48
+ object.errors
49
+ else
50
+ {}
51
+ end
52
+ end
53
+
54
+ def base_errors
55
+ if e = errors[:base]
56
+ tag(:ul, class: 'base errors') do
57
+ if e.respond_to?(:map)
58
+ zjoin e.map{|m| base_error(m) }
59
+ else
60
+ base_error(e)
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ def base_error(e)
67
+ tag(:li){ e }
68
+ end
69
+
70
+ def render
71
+ tag(:form, id: form_id, class: form_class, action:action, method:"POST") do
72
+ body = yield(self) if block_given?
73
+ zjoin(method_tag,base_errors,body)
74
+ end
75
+ end
76
+
77
+ def title
78
+ sjoin (new_object? ? "Create" : "Edit"), object_name.titleize
79
+ end
80
+
81
+ def param(*args)
82
+ object_name + args.map{|a|"[#{a}]"}.join
83
+ end
84
+
85
+ # allow "label: false" to prevent label being generated so that function that call label_for
86
+ # can all consistently omit the label when label value is given as false
87
+ def label_for(k, label: nil)
88
+ tag(:label, for: k.to_s.dasherize){ label || k.to_s.titleize } unless label == false
89
+ end
90
+
91
+ def input_for(k, type: "text", default: nil, **args)
92
+ val = object.send(k) || default
93
+ opts = {type:type, id:k.to_s.dasherize, name:param(k), value:val}.merge(args)
94
+ tag(:input,opts)
95
+ end
96
+
97
+ def error_for(k)
98
+ tag(:span, class: 'error'){ errors[k] } if errors[k]
99
+ end
100
+
101
+ def text_field(k, label: nil, default: nil, **args)
102
+ zjoin label_for(k, label:label), input_for(k,default:default,**args), error_for(k)
103
+ end
104
+
105
+ def number_field(k, label: nil, default: nil, **args)
106
+ opts = {type:'number', pattern: '\d*'}.merge(args)
107
+ zjoin label_for(k, label:label), input_for(k,type:'number',default:default,**opts), error_for(k)
108
+ end
109
+
110
+ def password_field(k, label: nil, **args)
111
+ zjoin label_for(k, label:label), input_for(k,type:"password",**args), error_for(k)
112
+ end
113
+
114
+ def hidden_field(k,**args)
115
+ input_for(k, type: "hidden",**args)
116
+ end
117
+
118
+ def text_area(k, label: nil, default: nil,**args)
119
+ val = object.send(k) || default
120
+ zjoin(
121
+ label_for(k, label:label),
122
+ tag(:textarea, id:k.to_s.dasherize, name:param(k),**args){ "#{val}" if val },
123
+ error_for(k)
124
+ )
125
+ end
126
+
127
+ def bool_field(k, label: nil,**args)
128
+ checked = ( !object.send(k) || object.send(k) == "false" ) ? false : true
129
+ zjoin label_for(k, label: label),
130
+ tag(:input, type:'hidden',name:param(k), value:"false"),
131
+ tag(:input, type:'checkbox', id: k.to_s.dasherize, name:param(k), value:"true", checked:checked,**args),
132
+ error_for(k)
133
+ end
134
+
135
+ def select(k, options:, label: nil, blank: true,**args)
136
+ opts = options.map{|value,text| option_for(k,value,text) }
137
+ opts.unshift option_for(k,nil,nil) if blank
138
+ zjoin(
139
+ label_for(k, label: label),
140
+ tag(:select, id:k.to_s.dasherize, name:param(k), **args){
141
+ zjoin opts
142
+ },
143
+ error_for(k)
144
+ )
145
+ end
146
+
147
+ def option_for(k,value,text)
148
+ opts = {value: value}
149
+ opts[:selected] = true if object.send(k) == value
150
+ tag(:option, opts){text ? text : value}
151
+ end
152
+
153
+ def association_select(association,key_method: :id, label_method: :name, label: nil, **args)
154
+ aname = detect_object_name(association)
155
+ key = (aname.underscore + "_id").to_sym
156
+ options = Hash[ association.map{|i| [i.send(key_method), i.send(label_method)]}]
157
+
158
+ unless label == false
159
+ label ||= aname.underscore.titleize
160
+ end
161
+
162
+ select(key,options: options, label: label, **args)
163
+ end
164
+
165
+ def date_input(k, label: nil, start_year: nil, end_year: nil, default:{})
166
+ start_year ||= Date.today.year
167
+ end_year ||= start_year+20
168
+ val = get_value(object,k)
169
+ tag :div, class: "date-input" do
170
+ zjoin label_for(nil,label:(label||k.to_s.titleize)),
171
+ tag(:input, date_attrs(k,:month,"MM",01,12,get_value(val,:month, default[:month]))),
172
+ tag(:input, date_attrs(k,:day,"DD",01,31,get_value(val,:day, default[:day]))),
173
+ tag(:input, date_attrs(k,:year,"YYYY",start_year,end_year,get_value(val,:year, default[:year]))),
174
+ error_for(k)
175
+ end
176
+ end
177
+
178
+ def date_attrs(k,time,ph,min,max,val)
179
+ { class: "date-#{time}", type: "number", name: param(k,time),
180
+ placeholder: ph, min: min, max: max, step: 1, value: val }
181
+ end
182
+
183
+ def submit_button(**args)
184
+ text = args.delete(:text)
185
+ tag(:button,**args){ text || (new_object? ? "Create" : "Save") }
186
+ end
187
+
188
+ private
189
+ def detect_object_name(o)
190
+ if o.is_a?(Symbol)
191
+ o.to_s
192
+ elsif o.is_a?(Class)
193
+ o.name
194
+ elsif o.respond_to?(:object_name)
195
+ o.object_name
196
+ elsif o.respond_to?(:model)
197
+ o.model
198
+ elsif o.is_a?(Enumerable)
199
+ detect_object_name(o.first)
200
+ else
201
+ o.class
202
+ end.to_s.demodulize
203
+ end
204
+
205
+ def get_value(object,key,default=nil)
206
+ if object.nil?
207
+ nil
208
+ elsif object.respond_to?(key)
209
+ object.send(key)
210
+ elsif object.respond_to? :fetch
211
+ object.fetch(key,nil)
212
+ else
213
+ raise BuilderError, "Undefined object method: #{key}"
214
+ end or default
215
+ end
216
+ end
217
+
218
+ BuilderError = Class.new(Error)
219
+ end
@@ -0,0 +1,55 @@
1
+ require 'nform/core_ext'
2
+ require 'active_support/core_ext/hash'
3
+ require 'active_support/core_ext/object'
4
+
5
+ module NForm
6
+ class CoercionSet
7
+ def [](key)
8
+ set[key.to_sym]
9
+ end
10
+
11
+ def []=(key,val)
12
+ set[key.to_sym] = val
13
+ end
14
+
15
+ def fetch(key)
16
+ if v = set[key]
17
+ v
18
+ else
19
+ raise Error, "Undefined coercion: #{key}"
20
+ end
21
+ end
22
+
23
+ def respond_to_missing?(name,*)
24
+ set.has_key?(name.to_sym)
25
+ end
26
+
27
+ def method_missing(name,*args,&block)
28
+ if set[name.to_sym]
29
+ set[name.to_sym].call(*args,&block)
30
+ else
31
+ super
32
+ end
33
+ end
34
+
35
+ private
36
+ def set
37
+ @set ||= {}
38
+ end
39
+ end
40
+
41
+ Coercions = CoercionSet.new
42
+ Coercions[:to_presence] = proc {|v| v.presence }
43
+ Coercions[:to_bool] = proc { |v| !(v.nil? || v == false || v == 'false') }
44
+ Coercions[:to_float] = proc { |v| v.to_f }
45
+ Coercions[:to_integer] = proc { |v| v.to_i }
46
+ Coercions[:to_string] = proc {|v| v.to_s.strip }
47
+ Coercions[:to_symbol] = proc {|v| v.to_sym unless v.nil? }
48
+ Coercions[:to_number] = proc do |v|
49
+ if v && v.is_a?(String)
50
+ v.gsub(/,/,'').to_f
51
+ else
52
+ v
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,12 @@
1
+ require 'active_support'
2
+
3
+ module NForm
4
+ module Hashable
5
+ # A convenience method for making a hash with the
6
+ # given methods on self as the keys and return for
7
+ # the given methods as the values
8
+ def hash_of(*keys)
9
+ keys.each.with_object({}){|k,h| h[k] = send(k) }
10
+ end
11
+ end
12
+ end
data/lib/nform/form.rb ADDED
@@ -0,0 +1,44 @@
1
+ require 'nform/core_ext'
2
+
3
+ module NForm
4
+ class Form
5
+ extend NForm::Attributes
6
+ include NForm::Validations
7
+
8
+ def valid?
9
+ errors.clear
10
+ validate!
11
+ true
12
+ rescue ValidationError
13
+ false
14
+ end
15
+
16
+ def validate!
17
+ yield if block_given?
18
+ validation_error! if errors.any?
19
+ end
20
+
21
+ private
22
+ def validation_error!(hash={})
23
+ errors.merge(hash)
24
+ raise ValidationError.new(errors)
25
+ end
26
+ end
27
+
28
+ class ValidationError < Error
29
+ attr_reader :errors
30
+ def initialize(errors={})
31
+ @errors = errors
32
+ end
33
+
34
+ def message
35
+ "\nPlease correct the following errors:\n#{error_messages}"
36
+ end
37
+
38
+ def error_messages
39
+ if errors.any?
40
+ errors.map{|k,v| "#{k.to_s.humanize}: #{v}"}.join("\n")
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,7 @@
1
+ module NForm
2
+ module Helpers
3
+ def form_view(*args,&block)
4
+ NForm::Builder.new(*args).render &block
5
+ end
6
+ end
7
+ end
data/lib/nform/html.rb ADDED
@@ -0,0 +1,50 @@
1
+ module NForm
2
+ # Helper Methods for generating HTML markup
3
+ module HTML
4
+ VOID_ELEMENTS = %i|area base br col embed hr img input keygen link meta param source track wbr|
5
+ BOOL_ATTRIBUTES = %i|allowfullscreen async autofocus autoplay checked compact controls declare default defaultchecked defaultmuted defaultselected defer disabled draggable enabled formnovalidate hidden indeterminate inert ismap itemscope loop multiple muted nohref noresize noshade novalidate nowrap open pauseonexit readonly required reversed scoped seamless selected sortable spellcheck translate truespeed typemustmatch visible|
6
+
7
+ # Generate an HTML Tag
8
+ def tag(name, attributes={}, &block)
9
+ open = sjoin name, attrs(attributes)
10
+ body = block.call if block_given?
11
+ if VOID_ELEMENTS.include?(name.to_sym)
12
+ raise BuilderError, "Void elements cannot have content" if body
13
+ "<#{open}>"
14
+ else
15
+ "<#{open}>#{body}</#{name}>"
16
+ end
17
+ end
18
+
19
+ def attrs(hash={})
20
+ hash.delete_if{|k,v| v.nil? || v == "" }
21
+ .map{|k,v| attr_string(k,v) }
22
+ .compact
23
+ .join(" ")
24
+ end
25
+
26
+ def attr_string(k,v)
27
+ if BOOL_ATTRIBUTES.include?(k)
28
+ attr_key(k) if v
29
+ else
30
+ %Q|#{attr_key(k)}="#{v}"|
31
+ end
32
+ end
33
+
34
+ def attr_key(k)
35
+ k.is_a?(Symbol) ? k.to_s.gsub("_","-") : k
36
+ end
37
+
38
+ def zjoin(*args)
39
+ args.delete_if{|a| a.nil? || a == ""}.join('')
40
+ end
41
+
42
+ def sjoin(*args)
43
+ args.delete_if{|a| a.nil? || a == ""}.join(' ')
44
+ end
45
+
46
+ def njoin(*args)
47
+ args.delete_if{|a| a.nil? || a == ""}.join("\n")
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,47 @@
1
+ require 'nform/core_ext'
2
+
3
+ module NForm
4
+ # Services expect valid input
5
+ # A service performs a single action
6
+ # A service should not accept unfiltered user input,
7
+ # but accept a form object when user input is required
8
+ class Service
9
+ include Hashable
10
+
11
+ attr_reader :form
12
+ def initialize(input)
13
+ @form = get_form(input)
14
+ end
15
+
16
+ def call
17
+ raise "Must be defined in subclass"
18
+ end
19
+
20
+ def self.call(*args)
21
+ new(*args).call
22
+ end
23
+
24
+ def self.form_class(klass=nil)
25
+ @@form_class = klass || const_get(:Form)
26
+ end
27
+
28
+ private
29
+ def get_form(input)
30
+ input.is_a?(@@form_class) ? input : @@form_class.new(input)
31
+ end
32
+
33
+ def error!(message)
34
+ raise ServiceError.new(message)
35
+ end
36
+
37
+ def validate!
38
+ end
39
+ end
40
+
41
+ class ServiceError < Error
42
+ attr_reader :message
43
+ def initialize(message="Unknown Error")
44
+ @message = message
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,62 @@
1
+ module NForm
2
+ # A Module of simple validation methods.
3
+ # Including objects are required to implement a simple API:
4
+ # 1. Keys passed in to a validation must match instance method names
5
+ # 2. Objects must provide an errors object which responds to []
6
+ #
7
+ # When called, validators will return true/false indicating whether
8
+ # the validation passed or failed
9
+ module Validations
10
+ def errors
11
+ @errors ||= {}
12
+ end
13
+
14
+ def validate_presence_of(*keys)
15
+ pass = true
16
+ keys.each do |key|
17
+ unless respond_to?(key) && send(key) && send(key) != ""
18
+ errors[key] = "#{key.to_s.humanize} is required"
19
+ pass = false
20
+ end
21
+ end
22
+ pass
23
+ end
24
+
25
+ def validate_numericality_of(*keys)
26
+ pass = true
27
+ keys.each do |key|
28
+ val = send(key)
29
+ return true if val.is_a?(Numeric)
30
+ unless (val.respond_to?(:to_i) || val.respond_to?(:to_f)) &&
31
+ (val == val.to_i.to_s || val == val.to_f.to_s)
32
+ errors[key] = "#{key.to_s.humanize} must be a number"
33
+ pass = false
34
+ end
35
+ end
36
+ pass
37
+ end
38
+
39
+ def validate_length_of(key,length)
40
+ val = send(key)
41
+ if val && val.respond_to?(:length) && val.length >= length
42
+ true
43
+ else
44
+ errors[key] = "#{key.to_s.humanize} must be at least #{length} characters long"
45
+ false
46
+ end
47
+ end
48
+
49
+ def validate_confirmation_of(attribute)
50
+ confirm_key = attribute.to_s.concat("_confirmation").to_sym
51
+ if !respond_to?(confirm_key)
52
+ errors[attribute] = "#{attribute.to_s.humanize} requires confirmation"
53
+ false
54
+ elsif send(attribute) != send(confirm_key)
55
+ errors[confirm_key] = "#{attribute.to_s.humanize} confirmation does not match"
56
+ false
57
+ else
58
+ true
59
+ end
60
+ end
61
+ end
62
+ end
data/lib/nform.rb ADDED
@@ -0,0 +1,20 @@
1
+ module NForm
2
+ # A base error class for any NForm exceptions to extend
3
+ Error = Class.new(StandardError)
4
+ end
5
+
6
+ # Food for thought:
7
+ # Not all this code is really required,
8
+ # the library could be configured to not require all
9
+ # and let the user just require the bits they want instead...
10
+ # In that case, this list should be used as the basis for
11
+ # `require nform/all`
12
+ # For now, continuing to load all.
13
+ require 'nform/helpers'
14
+ require 'nform/html'
15
+ require 'nform/builder'
16
+ require 'nform/attributes'
17
+ require 'nform/validations'
18
+ require 'nform/coercions'
19
+ require 'nform/form'
20
+ require 'nform/service'
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nform
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Andrew Burleson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-01-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.0'
27
+ description: A nifty form builder and such.
28
+ email: burlesona@gmail.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - lib/nform.rb
34
+ - lib/nform/attributes.rb
35
+ - lib/nform/builder.rb
36
+ - lib/nform/coercions.rb
37
+ - lib/nform/core_ext.rb
38
+ - lib/nform/form.rb
39
+ - lib/nform/helpers.rb
40
+ - lib/nform/html.rb
41
+ - lib/nform/service.rb
42
+ - lib/nform/validations.rb
43
+ homepage: http://github.com/burlesona/nform
44
+ licenses:
45
+ - MIT
46
+ metadata: {}
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubyforge_project:
63
+ rubygems_version: 2.4.5.1
64
+ signing_key:
65
+ specification_version: 4
66
+ summary: A form library
67
+ test_files: []