modelish 0.3.0 → 1.0.0.pre.1

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: ebd460629d89807f4466c496083fd9a0a7be54d5
4
+ data.tar.gz: 667ce55093567f11e2d0c4a5f108828d5c3808fa
5
+ SHA512:
6
+ metadata.gz: 7414d42602a180112990808d9ce895ad9da82ba6dc19dc9dc69d6967286b986260ae8d9ba8bfaa4c5842d508fc12fdfb902c56567e72ae4f8a1019afc81e137d
7
+ data.tar.gz: ac6d7d3831e1854da332e0d6b14e0b2358f17e0a6e4baa0d1b326e995c5afc51ab76e7c257a38389673bb554692ee0600b32cd1610b9abcd48a0ea1dacd9090f
data/.gitignore CHANGED
@@ -1,8 +1,10 @@
1
1
  *.gem
2
2
  .bundle
3
3
  Gemfile.lock
4
+ gemfiles/*.gemfile.lock
4
5
  pkg/*
5
6
  .yardoc/*
6
7
  doc/*
7
8
  *~
8
9
  .DS_Store
10
+ coverage
data/.rspec CHANGED
@@ -1 +1,3 @@
1
- --colour
1
+ --color
2
+ --format Fuubar
3
+ --require spec_helper
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- ruby-1.8.7-p374
1
+ 2.3.4
data/.travis.yml ADDED
@@ -0,0 +1,14 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.2.7
5
+ - 2.3.4
6
+ - 2.4.1
7
+ gemfile:
8
+ - gemfiles/hashie_1.gemfile
9
+ - gemfiles/hashie_2.gemfile
10
+ - gemfiles/hashie_3.gemfile
11
+ before_install:
12
+ - gem install bundler
13
+ script:
14
+ - bundle exec rspec spec
data/Appraisals ADDED
@@ -0,0 +1,11 @@
1
+ appraise 'hashie-1' do
2
+ gem 'hashie', '~> 1.0'
3
+ end
4
+
5
+ appraise 'hashie-2' do
6
+ gem 'hashie', '~> 2.0'
7
+ end
8
+
9
+ appraise 'hashie-3' do
10
+ gem 'hashie', '~> 3.0'
11
+ end
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 1.0.0.pre.1 (2017-06-11)
4
+
5
+ * **Backwards incompatible change**: dropped support for ruby 1.8.7
6
+ * Relax dependency on hashie to >= 1.0
7
+ * Add support for ruby 2.3 and 2.4
8
+ * General cleanup in dev and CI environments
9
+
10
+ ## 0.4.0 (2017-06-08)
11
+
12
+ * **yanked** on 2017-06-11 because it broke compatibility with ruby 1.8.7
13
+
3
14
  ## 0.3.0 (2013-7-8)
4
15
 
5
16
  * Upgrade hashie dependency to v2.0.x
data/Gemfile CHANGED
@@ -1,4 +1,8 @@
1
- source "http://rubygems.org"
1
+ # frozen_string_literal: true
2
+
3
+ source 'http://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in modelish.gemspec
4
6
  gemspec
7
+
8
+ gem 'hashie', '~> 3.0'
data/README.md CHANGED
@@ -3,6 +3,11 @@
3
3
  When a real modeling framework is too heavy, sometimes you want something just
4
4
  a little modelish.
5
5
 
6
+ **IMPORTANT NOTE**: modelish is considered legacy code at this point.
7
+ Maintenance will continue for bug fixes, but active development has ceased.
8
+ For new projects that require lightweight models, try looking at something like
9
+ [virtus](https://github.com/solnic/virtus) instead.
10
+
6
11
  ## Overview ##
7
12
 
8
13
  If a Hash or OpenStruct almost suits your needs, but you need bits of
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "hashie", "~> 1.0"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "hashie", "~> 2.0"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "hashie", "~> 3.0"
6
+
7
+ gemspec path: "../"
data/lib/modelish/base.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'hashie'
2
4
  require 'modelish/property_translations'
3
5
  require 'modelish/property_types'
@@ -5,69 +7,18 @@ require 'modelish/validations'
5
7
  require 'modelish/configuration'
6
8
 
7
9
  module Modelish
10
+ # Base class for all modelish objects
8
11
  class Base < Hashie::Dash
9
12
  include PropertyTranslations
10
13
  include PropertyTypes
11
14
  include Validations
12
15
  extend Configuration
13
16
 
14
- def initialize(options={}, &block)
17
+ def initialize(options = {}, &block)
15
18
  super(&block)
16
19
 
17
20
  attributes = options ? options.dup : {}
18
-
19
- attributes.delete_if do |k,v|
20
- if self.class.translations.keys.include?(k.to_sym)
21
- self[k]=v
22
- true
23
- end
24
- end
25
-
26
- attributes.each_pair do |att, value|
27
- self[att] = value
28
- end
29
- end
30
-
31
- # Creates a new attribute.
32
- #
33
- # @param [Symbol] name the name of the property
34
- # @param [Hash] options configuration for the property
35
- # @option opts [Object] :default the default value for this property
36
- # when the value has not been explicitly
37
- # set (defaults to nil)
38
- # @option opts [#to_s] :from the original key name for this attribute
39
- # (created as write-only)
40
- # @option opts [Class,Proc] :type the type of the property value. For
41
- # a list of accepted types, see
42
- # {Modelish::PropertyTypes}
43
- # @option opts [true,false] :required enables validation for the property
44
- # value's presence; nil or blank values
45
- # will cause validation methods to fail
46
- # @option opts [Integer] :max_length the maximum allowable length for a valid
47
- # property value
48
- # @option opts [true,false] :validate_type enables validation for the property value's
49
- # type based on the :type option
50
- # @option opts [Proc] :validator A block that accepts a value and validates it;
51
- # should return nil if validation passes, or an error
52
- # message or error object if validation fails.
53
- # See {Modelish::Validations}
54
- def self.property(name, options={})
55
-
56
- #Hashie::Dash.property is going to delete :required from the options hash
57
- required = options[:required]
58
-
59
- super
60
-
61
- add_property_type(name, options[:type]) if options[:type]
62
- add_property_translation(options[:from], name) if options[:from]
63
-
64
- if options[:required] || (self.respond_to?(:required?) && required?(name))
65
- add_validator(name) { |val| validate_required(name => val).first }
66
- end
67
-
68
- add_validator(name) { |val| validate_length(name, val, options[:max_length]) } if options[:max_length]
69
- add_validator(name, &options[:validator]) if options[:validator]
70
- add_validator(name) { |val| validate_type(name, val, options[:type]) } if options[:validate_type]
21
+ init_attributes(attributes)
71
22
  end
72
23
 
73
24
  # Convert this Modelish object into a vanilla Hash with stringified keys.
@@ -75,15 +26,7 @@ module Modelish
75
26
  # @return [Hash] the hash of properties
76
27
  def to_hash
77
28
  out = {}
78
- self.class.properties.each do |p|
79
- val = self.send(p)
80
- if val.is_a?(Array)
81
- out[p.to_s]||=[]
82
- out[p.to_s].concat(val.collect{|x|x.respond_to?(:to_hash) ? x.to_hash : x})
83
- else
84
- out[p.to_s] = val.respond_to?(:to_hash) ? val.to_hash : val
85
- end
86
- end
29
+ self.class.properties.each { |p| out[hash_key(p)] = hash_value(p) }
87
30
  out
88
31
  end
89
32
 
@@ -96,27 +39,120 @@ module Modelish
96
39
  end
97
40
 
98
41
  private
42
+
43
+ def init_attributes(attributes)
44
+ attributes.delete_if do |k, v|
45
+ if self.class.translations.keys.include?(k.to_sym)
46
+ self[k] = v
47
+ true
48
+ end
49
+ end
50
+
51
+ attributes.each_pair do |att, value|
52
+ self[att] = value
53
+ end
54
+ end
55
+
99
56
  def property_exists?(property)
100
57
  if self.class.property?(property.to_sym)
101
58
  true
102
- elsif self.class.ignore_unknown_properties ||
103
- (self.class.ignore_unknown_properties.nil? && Modelish.ignore_unknown_properties)
59
+ elsif self.class.ignore_unknown_properties ||
60
+ (self.class.ignore_unknown_properties.nil? &&
61
+ Modelish.ignore_unknown_properties)
104
62
  false
105
63
  else
106
- raise NoMethodError, "The property '#{property}' is not defined for this Modelish object."
64
+ raise NoMethodError, "The property '#{property}' is not defined for " \
65
+ 'this Modelish object.'
107
66
  end
108
67
  end
109
68
 
110
- def assert_required_properties_set!
111
- nil
112
- end
69
+ # Disable the various ways that hashie tries to assert required properties
70
+ # are set on initialization (modelish defers this until validation)
71
+ def assert_required_properties_set!; end
113
72
 
114
- def assert_property_required!(property,value)
115
- nil
116
- end
73
+ def assert_property_required!(_property, _value); end
74
+
75
+ def assert_required_attributes_set!; end
117
76
 
118
77
  def assert_property_exists!(property)
119
78
  property_exists?(property)
120
79
  end
80
+
81
+ def hash_key(property)
82
+ property.to_s
83
+ end
84
+
85
+ def hash_value(property)
86
+ val = send(property)
87
+
88
+ if val.is_a?(Array)
89
+ val.map { |x| x.respond_to?(:to_hash) ? x.to_hash : x }
90
+ else
91
+ val.respond_to?(:to_hash) ? val.to_hash : val
92
+ end
93
+ end
94
+
95
+ class << self
96
+ # Creates a new attribute.
97
+ #
98
+ # @param [Symbol] name the name of the property
99
+ # @param [Hash] options configuration for the property
100
+ # @option opts [Object] :default the default value for this property
101
+ # when the value has not been explicitly
102
+ # set (defaults to nil)
103
+ # @option opts [#to_s] :from the original key name for this attribute
104
+ # (created as write-only)
105
+ # @option opts [Class,Proc] :type the type of the property value. For
106
+ # a list of accepted types, see
107
+ # {Modelish::PropertyTypes}
108
+ # @option opts [true,false] :required enables validation for the property
109
+ # value's presence; nil or blank
110
+ # will cause validation to fail
111
+ # @option opts [Integer] :max_length the maximum allowable length for a
112
+ # valid property value
113
+ # @option opts [true,false] :validate_type enables validation for the
114
+ # property value's type based on
115
+ # the :type option
116
+ # @option opts [Proc] :validator A block that validates a value;
117
+ # returns nil if validation passes, or an
118
+ # error message or error object if
119
+ # validation fails.
120
+ # See {Modelish::Validations}
121
+ def property(name, options = {})
122
+ # Hashie::Dash.property deletes the :required key from the options
123
+ required = options[:required]
124
+ super
125
+
126
+ add_property_type(name, options[:type]) if options[:type]
127
+ add_property_translation(options[:from], name) if options[:from]
128
+
129
+ process_required(name) if required
130
+ process_max_length(name, options)
131
+ process_validate_type(name, options)
132
+
133
+ add_validator(name, &options[:validator]) if options[:validator]
134
+ end
135
+
136
+ private
137
+
138
+ def process_required(name)
139
+ return unless respond_to?(:required?) && required?(name)
140
+ add_validator(name) { |val| validate_required(name => val).first }
141
+ end
142
+
143
+ def process_max_length(name, options)
144
+ return unless options[:max_length]
145
+ add_validator(name) do |val|
146
+ validate_length(name, val, options[:max_length])
147
+ end
148
+ end
149
+
150
+ def process_validate_type(name, options)
151
+ return unless options[:validate_type]
152
+ add_validator(name) do |val|
153
+ validate_type(name, val, options[:type])
154
+ end
155
+ end
156
+ end
121
157
  end
122
158
  end
@@ -1,11 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Modelish
4
+ # Configure modelish's behavior on a global level
2
5
  module Configuration
3
6
  # If true, ignore unknown property names when initializing new models;
4
7
  # otherwise, raise an error when an unknown property is encountered.
5
8
  # Defaults to false.
6
9
  attr_accessor :ignore_unknown_properties
7
10
 
8
- # When set, unknown property names will be ignored during modelish initialization
11
+ # When set, unknown property names will be ignored during initialization
9
12
  # and property setting.
10
13
  #
11
14
  # @see {raise_errors_on_unknown_properties!}
@@ -13,15 +16,16 @@ module Modelish
13
16
  self.ignore_unknown_properties = true
14
17
  end
15
18
 
16
- # When set, unknown property names will cause errors to be raised when encountered
17
- # during modelish initialization and property setting. This is the default behavior.
19
+ # When set, raise errors during initializeation and property setting when
20
+ # unknown property names are encountered. This is the default.
18
21
  #
19
22
  # @see {ignore_unknown_properties!}
20
23
  def raise_errors_on_unknown_properties!
21
24
  self.ignore_unknown_properties = false
22
25
  end
23
26
 
24
- # When this module is extended, set all configuration options to their default values
27
+ # When this module is extended, set all configuration options to their
28
+ # default values
25
29
  def self.extended(base)
26
30
  base.reset
27
31
  end
@@ -35,10 +39,7 @@ module Modelish
35
39
  # config.ignore_unknown_properties = true
36
40
  # end
37
41
  def configure
38
- if block_given?
39
- yield self
40
- end
41
-
42
+ yield self if block_given?
42
43
  self
43
44
  end
44
45
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Modelish
2
4
  # Mixin behavior for mapping one property name to another
3
5
  module PropertyTranslations
@@ -5,6 +7,7 @@ module Modelish
5
7
  base.extend(ClassMethods)
6
8
  end
7
9
 
10
+ # Methods for managing a dictionary of property translations
8
11
  module ClassMethods
9
12
  # Adds a property translation to the model.
10
13
  # This maps a mutator name to an existing property,
@@ -58,24 +61,33 @@ module Modelish
58
61
  # @param [Symbol,String] from_name the name of the source property
59
62
  # @param [Symbol,String] to_name the name of the destination property
60
63
  def add_property_translation(from_name, to_name)
61
- self.translations[from_name.to_sym] ||= []
62
- self.translations[from_name.to_sym] << to_name.to_sym
64
+ source = from_name.to_sym
65
+ target = to_name.to_sym
63
66
 
64
- class_eval do
65
- define_method("#{from_name}=") do |value|
66
- self.class.translations[from_name.to_sym].each do |prop|
67
- self.send("#{prop}=", value)
68
- end
69
- end
70
- end
67
+ translations[source] ||= []
68
+ translations[source] << target
69
+ define_writer_with_translations(source)
71
70
  end
72
71
 
73
- # A map of the translations that have already been configured, keyed on from_name.
72
+ # A map of the configured property translations, keyed on from_name
74
73
  #
75
- # @return [Hash<Symbol,Array>] the key is the from_name, the value is an array to_names
74
+ # @return [Hash<Symbol,Array>] key is from_name, value is list of to_names
76
75
  def translations
77
76
  @translations ||= {}
78
77
  end
78
+
79
+ private
80
+
81
+ def define_writer_with_translations(source)
82
+ class_eval do
83
+ remove_method("#{source}=") if method_defined?("#{source}=")
84
+ define_method("#{source}=") do |value|
85
+ self.class.translations[source].each do |target|
86
+ send("#{target}=", value)
87
+ end
88
+ end
89
+ end
90
+ end
79
91
  end
80
92
  end
81
93
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'date'
2
4
 
3
5
  module Modelish
@@ -7,21 +9,24 @@ module Modelish
7
9
  base.extend(ClassMethods)
8
10
  end
9
11
 
12
+ # Class methods for managing typed properties
10
13
  module ClassMethods
11
14
  # Adds a typed property to the model.
12
15
  # This dynamically generates accessor/mutator methods that perform
13
16
  # the appropriate type conversions on the property's value.
14
- #
17
+ #
15
18
  # Generated methods:
16
- # * +<property_name>=(new_value)+ -- sets the property value.
17
- # * +<property_name>+ -- returns the property value, converted to the configured
18
- # type. If the value cannot be converted, no error will be
19
- # raised, and the raw unconverted value will be returned.
20
- # * +<property_name>!+ -- returns the property value, converted to the configured
21
- # type. If the value cannot be converted, a TypeError will
22
- # be raised.
23
- # * +raw_<property_name> -- the original property value, without any type conversions.
24
- #
19
+ # * +<property_name>=(new_value)+ -- sets the property value.
20
+ # * +<property_name>+ -- returns the property value, converted to the
21
+ # configured type. If the value cannot be
22
+ # converted, no error will be raised, and the raw
23
+ # unconverted value will be returned.
24
+ # * +<property_name>!+ -- returns the property value, converted to the
25
+ # configured type. If the value cannot be
26
+ # converted, a TypeError will be raised.
27
+ # * +raw_<property_name> -- the original property value, without any type
28
+ # conversions.
29
+ #
25
30
  # @param [Symbol] property_name the name of the property.
26
31
  # @param [Class, Proc] property_type the type of the property's value.
27
32
  # Valid types include:
@@ -34,49 +39,57 @@ module Modelish
34
39
  # * +String+
35
40
  # * any arbitrary +Class+ -- will attempt conversion by passing the raw
36
41
  # value into the class's initializer
37
- # * an instance of +Proc+ -- will convert the value by executing the proc,
38
- # passing in the raw value as an argument
39
- def add_property_type(property_name, property_type=String)
42
+ # * an instance of +Proc+ -- will convert the value by executing the
43
+ # proc, passing in the raw value
44
+ def add_property_type(property_name, property_type = String)
40
45
  accessor = property_name.to_sym
46
+ return if property_types[accessor] == property_type
41
47
 
42
- # TODO: Refactor. This method is getting unwieldy as more
48
+ # TODO: Refactor. This method is getting unwieldy as more
43
49
  # corner cases are discovered. A few well-placed design
44
50
  # refinements should take care of it (now we just need to figure
45
51
  # out what those are.)
46
- unless property_types[accessor] == property_type
47
- property_types[accessor] = property_type
48
-
49
- raw_accessor = define_raw_accessor(accessor)
50
- bang_accessor = define_bang_accessor(accessor)
52
+ property_types[accessor] = property_type
51
53
 
52
- typed_accessor = "_typed_#{accessor}".to_sym
53
- typed_mutator = "#{typed_accessor}=".to_sym
54
- to_safe = "_to_safe_#{accessor}".to_sym
54
+ raw_accessor = define_raw_accessor(accessor)
55
+ bang_accessor = define_bang_accessor(accessor)
55
56
 
56
- class_eval do
57
- attr_accessor typed_accessor
58
- private typed_accessor, typed_mutator
57
+ typed_accessor = "_typed_#{accessor}".to_sym
58
+ typed_mutator = "#{typed_accessor}=".to_sym
59
+ to_safe = "_to_safe_#{accessor}".to_sym
59
60
 
60
- define_method(to_safe) do
61
- self.send(bang_accessor) rescue self.send(raw_accessor)
61
+ class_eval do
62
+ remove_method(typed_accessor) if method_defined?(typed_accessor)
63
+ remove_method(typed_mutator) if method_defined?(typed_mutator)
64
+ attr_accessor typed_accessor
65
+ protected typed_accessor, typed_mutator
66
+
67
+ remove_method(to_safe) if method_defined?(to_safe)
68
+ define_method(to_safe) do
69
+ begin
70
+ send(bang_accessor)
71
+ rescue
72
+ send(raw_accessor)
62
73
  end
63
- private to_safe
64
-
65
- define_method(accessor) do
66
- val = self.send(typed_accessor)
74
+ end
75
+ protected to_safe
67
76
 
68
- unless val || self.send(raw_accessor).nil?
69
- val = self.send(to_safe)
70
- self.send(typed_mutator, val)
71
- end
77
+ remove_method(accessor) if method_defined?(accessor)
78
+ define_method(accessor) do
79
+ val = send(typed_accessor)
72
80
 
73
- val
81
+ unless val || send(raw_accessor).nil?
82
+ val = send(to_safe)
83
+ send(typed_mutator, val)
74
84
  end
75
85
 
76
- define_method("#{accessor}=") do |val|
77
- self.send("#{raw_accessor}=", val)
78
- self.send(typed_mutator, self.send(to_safe))
79
- end
86
+ val
87
+ end
88
+
89
+ remove_method("#{accessor}=") if method_defined?("#{accessor}=")
90
+ define_method("#{accessor}=") do |val|
91
+ send("#{raw_accessor}=", val)
92
+ send(typed_mutator, send(to_safe))
80
93
  end
81
94
  end
82
95
  end
@@ -86,6 +99,7 @@ module Modelish
86
99
  end
87
100
 
88
101
  private
102
+
89
103
  def define_raw_accessor(name)
90
104
  accessor = name.to_sym
91
105
  raw_accessor = "raw_#{name}".to_sym
@@ -112,9 +126,10 @@ module Modelish
112
126
  converter = value_converter(property_types[property_name.to_sym])
113
127
 
114
128
  class_eval do
129
+ remove_method(bang_accessor) if method_defined?(bang_accessor)
115
130
  define_method(bang_accessor) do
116
- value = self.send("raw_#{property_name}")
117
- (converter && value) ? converter.call(value) : value
131
+ value = send("raw_#{property_name}")
132
+ converter && value ? converter.call(value) : value
118
133
  end
119
134
  end
120
135
 
@@ -123,19 +138,21 @@ module Modelish
123
138
 
124
139
  def value_converter(property_type)
125
140
  if [Date, DateTime].include?(property_type)
126
- lambda { |val| property_type.parse(val.to_s) }
141
+ ->(val) { property_type.parse(val.to_s) }
127
142
  elsif property_type == Symbol
128
- lambda { |val| val.to_s.strip.gsub(/([A-Z]+)([A-Z][a-z])/, '\\1_\\2').
129
- gsub(/([a-z\d])([A-Z])/, '\\1_\\2').
130
- gsub(/\s+|-/,'_').downcase.to_sym }
143
+ lambda do |val|
144
+ val.to_s.strip.gsub(/([A-Z]+)([A-Z][a-z])/, '\\1_\\2')
145
+ .gsub(/([a-z\d])([A-Z])/, '\\1_\\2')
146
+ .gsub(/\s+|-/, '_').downcase.to_sym
147
+ end
131
148
  elsif property_type == String
132
- lambda { |val| val.to_s.strip }
133
- elsif Kernel.respond_to?(property_type.to_s)
134
- lambda { |val| Kernel.send(property_type.to_s, val) }
149
+ ->(val) { val.to_s.strip }
150
+ elsif Kernel.respond_to?(property_type.to_s)
151
+ ->(val) { Kernel.send(property_type.to_s, val) }
135
152
  elsif property_type.respond_to?(:call)
136
153
  property_type
137
154
  else
138
- lambda { |val| property_type.new(val) }
155
+ ->(val) { property_type.new(val) }
139
156
  end
140
157
  end
141
158
  end