mongomodel 0.2.6 → 0.2.7
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +5 -6
- data/Rakefile +3 -4
- data/lib/mongomodel/concerns/properties.rb +15 -9
- data/lib/mongomodel/document/optimistic_locking.rb +1 -1
- data/lib/mongomodel/document/persistence.rb +2 -2
- data/lib/mongomodel/document/validations/uniqueness.rb +65 -36
- data/lib/mongomodel/support/map.rb +4 -0
- data/lib/mongomodel/support/mongo_options.rb +1 -1
- data/lib/mongomodel/support/scope/query_methods.rb +6 -0
- data/lib/mongomodel/support/types/time.rb +2 -3
- data/lib/mongomodel/version.rb +1 -1
- data/mongomodel.gemspec +11 -14
- data/spec/mongomodel/attributes/store_spec.rb +5 -0
- data/spec/mongomodel/concerns/properties_spec.rb +13 -0
- data/spec/mongomodel/support/map_spec.rb +16 -4
- data/spec/mongomodel/support/scope_spec.rb +4 -0
- data/spec/support/models.rb +16 -0
- metadata +16 -32
data/Gemfile
CHANGED
@@ -1,12 +1,11 @@
|
|
1
1
|
source "http://rubygems.org"
|
2
|
-
git "git://github.com/rails/rails.git"
|
3
2
|
|
4
|
-
gem "activemodel", "
|
5
|
-
gem "activesupport", "
|
3
|
+
gem "activemodel", "~> 3.0.0"
|
4
|
+
gem "activesupport", "~> 3.0.0"
|
6
5
|
gem "tzinfo"
|
7
6
|
|
8
|
-
gem "mongo", '
|
9
|
-
gem "bson", '
|
10
|
-
gem "bson_ext", '
|
7
|
+
gem "mongo", '= 1.0.7'
|
8
|
+
gem "bson", '= 1.0.7'
|
9
|
+
gem "bson_ext", '= 1.0.7'
|
11
10
|
|
12
11
|
gem "rspec"
|
data/Rakefile
CHANGED
@@ -46,10 +46,9 @@ begin
|
|
46
46
|
gem.authors = ["Sam Pohlenz"]
|
47
47
|
gem.version = MongoModel::VERSION
|
48
48
|
|
49
|
-
gem.add_dependency('activesupport', '
|
50
|
-
gem.add_dependency('activemodel', '
|
51
|
-
gem.add_dependency('mongo', '
|
52
|
-
gem.add_dependency('bson', '>= 1.0')
|
49
|
+
gem.add_dependency('activesupport', '~> 3.0.0')
|
50
|
+
gem.add_dependency('activemodel', '~> 3.0.0')
|
51
|
+
gem.add_dependency('mongo', '~> 1.0.7')
|
53
52
|
gem.add_development_dependency('rspec', '>= 1.3.0')
|
54
53
|
end
|
55
54
|
|
@@ -14,7 +14,9 @@ module MongoModel
|
|
14
14
|
|
15
15
|
module ClassMethods
|
16
16
|
def property(name, type, options={})
|
17
|
-
properties[name.to_sym] = Property.new(name, type, options)
|
17
|
+
properties[name.to_sym] = Property.new(name, type, options).tap do |property|
|
18
|
+
include type.mongomodel_accessors(property) if type.respond_to?(:mongomodel_accessors)
|
19
|
+
end
|
18
20
|
end
|
19
21
|
|
20
22
|
def model_properties
|
@@ -40,15 +42,19 @@ module MongoModel
|
|
40
42
|
end
|
41
43
|
|
42
44
|
def default(instance)
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
45
|
+
if options.key?(:default)
|
46
|
+
default = options[:default]
|
47
|
+
|
48
|
+
if default.respond_to?(:call)
|
49
|
+
case default.arity
|
50
|
+
when 0 then default.call
|
51
|
+
else default.call(instance)
|
52
|
+
end
|
53
|
+
else
|
54
|
+
default.duplicable? ? default.dup : default
|
49
55
|
end
|
50
|
-
|
51
|
-
|
56
|
+
elsif type.respond_to?(:mongomodel_default)
|
57
|
+
type.mongomodel_default(instance)
|
52
58
|
end
|
53
59
|
end
|
54
60
|
|
@@ -31,7 +31,7 @@ module MongoModel
|
|
31
31
|
if locking_enabled? && _lock_version > 1
|
32
32
|
begin
|
33
33
|
collection.update({ '_id' => id, '_lock_version' => _lock_version-1 }, to_mongo)
|
34
|
-
success = database.
|
34
|
+
success = database.get_last_error['updatedExisting']
|
35
35
|
|
36
36
|
self._lock_version -= 1 unless success
|
37
37
|
|
@@ -68,10 +68,10 @@ module MongoModel
|
|
68
68
|
self.class.database
|
69
69
|
end
|
70
70
|
|
71
|
-
# Generate a new BSON::
|
71
|
+
# Generate a new BSON::ObjectId for the record.
|
72
72
|
# Override in subclasses for custom ID generation.
|
73
73
|
def generate_id
|
74
|
-
::BSON::
|
74
|
+
::BSON::ObjectId.new.to_s
|
75
75
|
end
|
76
76
|
|
77
77
|
module ClassMethods
|
@@ -1,6 +1,70 @@
|
|
1
|
+
require 'active_support/core_ext/array/wrap'
|
2
|
+
|
1
3
|
module MongoModel
|
2
4
|
module DocumentExtensions
|
3
5
|
module Validations
|
6
|
+
class UniquenessValidator < ActiveModel::EachValidator
|
7
|
+
def initialize(options)
|
8
|
+
super(options.reverse_merge(:case_sensitive => true))
|
9
|
+
end
|
10
|
+
|
11
|
+
def setup(klass)
|
12
|
+
@klass = klass
|
13
|
+
|
14
|
+
# Enable safety checks on save
|
15
|
+
klass.save_safely = true
|
16
|
+
|
17
|
+
# Create unique indexes to deal with race condition
|
18
|
+
attributes.each do |attr_name|
|
19
|
+
if options[:case_sensitive]
|
20
|
+
klass.index *[attr_name] + Array.wrap(options[:scope]) << { :unique => true }
|
21
|
+
else
|
22
|
+
lowercase_key = "_lowercase_#{attr_name}"
|
23
|
+
klass.before_save { attributes[lowercase_key] = send(attr_name).downcase }
|
24
|
+
klass.index *[lowercase_key] + Array.wrap(options[:scope]) << { :unique => true }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def validate_each(record, attribute, value)
|
30
|
+
finder_class = find_finder_class_for(record)
|
31
|
+
unique_scope = finder_class.scoped
|
32
|
+
|
33
|
+
if options[:case_sensitive] || !value.is_a?(String)
|
34
|
+
unique_scope = unique_scope.where(attribute => value)
|
35
|
+
else
|
36
|
+
unique_scope = unique_scope.where("_lowercase_#{attribute}" => value.downcase)
|
37
|
+
end
|
38
|
+
|
39
|
+
Array.wrap(options[:scope]).each do |scope|
|
40
|
+
unique_scope = unique_scope.where(scope => record.send(scope))
|
41
|
+
end
|
42
|
+
|
43
|
+
unique_scope = unique_scope.where(:id.ne => record.id) unless record.new_record?
|
44
|
+
|
45
|
+
if unique_scope.any?
|
46
|
+
record.errors.add(attribute, :taken, :message => options[:message], :value => value)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# The check for an existing value should be run from a class that
|
53
|
+
# isn't abstract. This means working down from the current class
|
54
|
+
# (self), to the first non-abstract class. Since classes don't know
|
55
|
+
# their subclasses, we have to build the hierarchy between self and
|
56
|
+
# the record's class.
|
57
|
+
def find_finder_class_for(record) #:nodoc:
|
58
|
+
class_hierarchy = [record.class]
|
59
|
+
|
60
|
+
while class_hierarchy.first != @klass
|
61
|
+
class_hierarchy.insert(0, class_hierarchy.first.superclass)
|
62
|
+
end
|
63
|
+
|
64
|
+
class_hierarchy.detect { |klass| !klass.abstract_class? }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
4
68
|
module ClassMethods
|
5
69
|
# Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user
|
6
70
|
# can be named "davidhh".
|
@@ -37,42 +101,7 @@ module MongoModel
|
|
37
101
|
# Note that this validation method does not have the same race condition suffered by ActiveRecord and other ORMs.
|
38
102
|
# A unique index is added to the collection to ensure that the collection never ends up in an invalid state.
|
39
103
|
def validates_uniqueness_of(*attr_names)
|
40
|
-
|
41
|
-
configuration.update(attr_names.extract_options!)
|
42
|
-
|
43
|
-
# Enable safety checks on save
|
44
|
-
self.save_safely = true
|
45
|
-
|
46
|
-
# Create unique indexes to deal with race condition
|
47
|
-
attr_names.each do |attr_name|
|
48
|
-
if configuration[:case_sensitive]
|
49
|
-
index *[attr_name] + Array(configuration[:scope]) << { :unique => true }
|
50
|
-
else
|
51
|
-
lowercase_key = "_lowercase_#{attr_name}"
|
52
|
-
before_save { attributes[lowercase_key] = send(attr_name).downcase }
|
53
|
-
index *[lowercase_key] + Array(configuration[:scope]) << { :unique => true }
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
validates_each(attr_names, configuration) do |record, attr_name, value|
|
58
|
-
unique_scope = scoped
|
59
|
-
|
60
|
-
if configuration[:case_sensitive] || !value.is_a?(String)
|
61
|
-
unique_scope = unique_scope.where(attr_name => value)
|
62
|
-
else
|
63
|
-
unique_scope = unique_scope.where("_lowercase_#{attr_name}" => value.downcase)
|
64
|
-
end
|
65
|
-
|
66
|
-
Array(configuration[:scope]).each do |scope|
|
67
|
-
unique_scope = unique_scope.where(scope => record.send(scope))
|
68
|
-
end
|
69
|
-
|
70
|
-
unique_scope = unique_scope.where(:id.ne => record.id) unless record.new_record?
|
71
|
-
|
72
|
-
if unique_scope.any?
|
73
|
-
record.errors.add(attr_name, :taken, :default => configuration[:message], :value => value)
|
74
|
-
end
|
75
|
-
end
|
104
|
+
validates_with UniquenessValidator, _merge_attributes(attr_names)
|
76
105
|
end
|
77
106
|
end
|
78
107
|
end
|
@@ -71,7 +71,7 @@ module MongoModel
|
|
71
71
|
|
72
72
|
def add_type_to_selector
|
73
73
|
unless selector['_type'] || @model.superclass.abstract_class?
|
74
|
-
selector['_type'] = { '$in' => [@model.to_s] + @model.subclasses }
|
74
|
+
selector['_type'] = { '$in' => [@model.to_s] + @model.subclasses.map { |m| m.to_s } }
|
75
75
|
end
|
76
76
|
end
|
77
77
|
end
|
@@ -31,6 +31,12 @@ module MongoModel
|
|
31
31
|
CEVAL
|
32
32
|
end
|
33
33
|
|
34
|
+
def from(value, &block)
|
35
|
+
new_scope = clone
|
36
|
+
new_scope.from_value = value.is_a?(String) ? klass.database.collection(value) : value
|
37
|
+
new_scope
|
38
|
+
end
|
39
|
+
|
34
40
|
def reverse_order
|
35
41
|
if order_values.empty?
|
36
42
|
order(:id.desc)
|
@@ -5,9 +5,8 @@ module MongoModel
|
|
5
5
|
module Types
|
6
6
|
class Time < Object
|
7
7
|
def cast(value)
|
8
|
-
time = value.to_time
|
9
|
-
|
10
|
-
::Time.at((time.to_f * 1000).floor / 1000.0)
|
8
|
+
time = value.to_time
|
9
|
+
time.change(:usec => (time.usec / 1000.0).floor * 1000)
|
11
10
|
rescue
|
12
11
|
nil
|
13
12
|
end
|
data/lib/mongomodel/version.rb
CHANGED
data/mongomodel.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{mongomodel}
|
8
|
-
s.version = "0.2.
|
8
|
+
s.version = "0.2.7"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Sam Pohlenz"]
|
12
|
-
s.date = %q{2010-
|
12
|
+
s.date = %q{2010-09-05}
|
13
13
|
s.default_executable = %q{console}
|
14
14
|
s.description = %q{MongoModel is a MongoDB ORM for Ruby/Rails similar to ActiveRecord and DataMapper.}
|
15
15
|
s.email = %q{sam@sampohlenz.com}
|
@@ -217,23 +217,20 @@ Gem::Specification.new do |s|
|
|
217
217
|
s.specification_version = 3
|
218
218
|
|
219
219
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
220
|
-
s.add_runtime_dependency(%q<activesupport>, ["
|
221
|
-
s.add_runtime_dependency(%q<activemodel>, ["
|
222
|
-
s.add_runtime_dependency(%q<mongo>, ["
|
223
|
-
s.add_runtime_dependency(%q<bson>, [">= 1.0"])
|
220
|
+
s.add_runtime_dependency(%q<activesupport>, ["~> 3.0.0"])
|
221
|
+
s.add_runtime_dependency(%q<activemodel>, ["~> 3.0.0"])
|
222
|
+
s.add_runtime_dependency(%q<mongo>, ["~> 1.0.7"])
|
224
223
|
s.add_development_dependency(%q<rspec>, [">= 1.3.0"])
|
225
224
|
else
|
226
|
-
s.add_dependency(%q<activesupport>, ["
|
227
|
-
s.add_dependency(%q<activemodel>, ["
|
228
|
-
s.add_dependency(%q<mongo>, ["
|
229
|
-
s.add_dependency(%q<bson>, [">= 1.0"])
|
225
|
+
s.add_dependency(%q<activesupport>, ["~> 3.0.0"])
|
226
|
+
s.add_dependency(%q<activemodel>, ["~> 3.0.0"])
|
227
|
+
s.add_dependency(%q<mongo>, ["~> 1.0.7"])
|
230
228
|
s.add_dependency(%q<rspec>, [">= 1.3.0"])
|
231
229
|
end
|
232
230
|
else
|
233
|
-
s.add_dependency(%q<activesupport>, ["
|
234
|
-
s.add_dependency(%q<activemodel>, ["
|
235
|
-
s.add_dependency(%q<mongo>, ["
|
236
|
-
s.add_dependency(%q<bson>, [">= 1.0"])
|
231
|
+
s.add_dependency(%q<activesupport>, ["~> 3.0.0"])
|
232
|
+
s.add_dependency(%q<activemodel>, ["~> 3.0.0"])
|
233
|
+
s.add_dependency(%q<mongo>, ["~> 1.0.7"])
|
237
234
|
s.add_dependency(%q<rspec>, [">= 1.3.0"])
|
238
235
|
end
|
239
236
|
end
|
@@ -15,6 +15,7 @@ module MongoModel
|
|
15
15
|
properties[:date] = MongoModel::Properties::Property.new(:date, Date)
|
16
16
|
properties[:time] = MongoModel::Properties::Property.new(:time, Time)
|
17
17
|
properties[:custom] = MongoModel::Properties::Property.new(:custom, CustomClass)
|
18
|
+
properties[:custom_default] = MongoModel::Properties::Property.new(:custom_default, CustomClassWithDefault)
|
18
19
|
properties[:default] = MongoModel::Properties::Property.new(:default, String, :default => 'Default')
|
19
20
|
properties[:as] = MongoModel::Properties::Property.new(:as, String, :as => '_custom_as')
|
20
21
|
properties
|
@@ -28,6 +29,10 @@ module MongoModel
|
|
28
29
|
subject[:default].should == 'Default'
|
29
30
|
end
|
30
31
|
|
32
|
+
it "should set default property value using mongomodel_default if defined by class" do
|
33
|
+
subject[:custom_default].should == CustomClassWithDefault.new("Custom class default")
|
34
|
+
end
|
35
|
+
|
31
36
|
describe "setting to nil" do
|
32
37
|
specify "all property types should be nullable" do
|
33
38
|
properties.keys.each do |property|
|
@@ -52,5 +52,18 @@ module MongoModel
|
|
52
52
|
factory.manager.should be_nil
|
53
53
|
end
|
54
54
|
end
|
55
|
+
|
56
|
+
describe "when using a property type that defines #mongomodel_accessors" do
|
57
|
+
define_class(:ParentClass, described_class) do
|
58
|
+
property :custom, CustomClassWithAccessors
|
59
|
+
end
|
60
|
+
|
61
|
+
subject { ParentClass.new }
|
62
|
+
|
63
|
+
it "should include methods from the module" do
|
64
|
+
subject.should respond_to(:custom_accessor)
|
65
|
+
subject.custom_accessor.should == "Custom accessor method"
|
66
|
+
end
|
67
|
+
end
|
55
68
|
end
|
56
69
|
end
|
@@ -101,8 +101,14 @@ module MongoModel
|
|
101
101
|
subject.value?(456).should be_true
|
102
102
|
end
|
103
103
|
|
104
|
-
|
105
|
-
|
104
|
+
if Hash.method_defined?(:key)
|
105
|
+
it "should cast values on #key" do
|
106
|
+
subject.key(456).should == "123"
|
107
|
+
end
|
108
|
+
else
|
109
|
+
it "should cast values on #index" do
|
110
|
+
subject.index(456).should == "123"
|
111
|
+
end
|
106
112
|
end
|
107
113
|
|
108
114
|
it "should cast key/values on #replace" do
|
@@ -177,8 +183,14 @@ module MongoModel
|
|
177
183
|
subject.value?("Another").should be_true
|
178
184
|
end
|
179
185
|
|
180
|
-
|
181
|
-
|
186
|
+
if Hash.method_defined?(:key)
|
187
|
+
it "should cast values on #key" do
|
188
|
+
subject.key("First").should == :abc
|
189
|
+
end
|
190
|
+
else
|
191
|
+
it "should cast values on #index" do
|
192
|
+
subject.index("First").should == :abc
|
193
|
+
end
|
182
194
|
end
|
183
195
|
|
184
196
|
it "should cast key/values on #replace" do
|
@@ -309,6 +309,10 @@ module MongoModel
|
|
309
309
|
it "should override collection" do
|
310
310
|
subject.from(NotAPost.collection).collection.should == NotAPost.collection
|
311
311
|
end
|
312
|
+
|
313
|
+
it "should allow collection to be set using string" do
|
314
|
+
subject.from(NotAPost.collection.name).collection.name.should == NotAPost.collection.name
|
315
|
+
end
|
312
316
|
end
|
313
317
|
|
314
318
|
describe "#first" do
|
data/spec/support/models.rb
CHANGED
@@ -21,3 +21,19 @@ class CustomClass
|
|
21
21
|
new(value.to_s)
|
22
22
|
end
|
23
23
|
end
|
24
|
+
|
25
|
+
class CustomClassWithDefault < CustomClass
|
26
|
+
def self.mongomodel_default(doc)
|
27
|
+
new("Custom class default")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class CustomClassWithAccessors < CustomClass
|
32
|
+
def self.mongomodel_accessors(property)
|
33
|
+
Module.new do
|
34
|
+
define_method(:custom_accessor) do
|
35
|
+
"Custom accessor method"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mongomodel
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 25
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 2
|
9
|
-
-
|
10
|
-
version: 0.2.
|
9
|
+
- 7
|
10
|
+
version: 0.2.7
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Sam Pohlenz
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-
|
18
|
+
date: 2010-09-05 00:00:00 +09:30
|
19
19
|
default_executable: console
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -24,15 +24,14 @@ dependencies:
|
|
24
24
|
requirement: &id001 !ruby/object:Gem::Requirement
|
25
25
|
none: false
|
26
26
|
requirements:
|
27
|
-
- -
|
27
|
+
- - ~>
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
hash:
|
29
|
+
hash: 7
|
30
30
|
segments:
|
31
31
|
- 3
|
32
32
|
- 0
|
33
33
|
- 0
|
34
|
-
|
35
|
-
version: 3.0.0.beta4
|
34
|
+
version: 3.0.0
|
36
35
|
type: :runtime
|
37
36
|
version_requirements: *id001
|
38
37
|
- !ruby/object:Gem::Dependency
|
@@ -41,15 +40,14 @@ dependencies:
|
|
41
40
|
requirement: &id002 !ruby/object:Gem::Requirement
|
42
41
|
none: false
|
43
42
|
requirements:
|
44
|
-
- -
|
43
|
+
- - ~>
|
45
44
|
- !ruby/object:Gem::Version
|
46
|
-
hash:
|
45
|
+
hash: 7
|
47
46
|
segments:
|
48
47
|
- 3
|
49
48
|
- 0
|
50
49
|
- 0
|
51
|
-
|
52
|
-
version: 3.0.0.beta4
|
50
|
+
version: 3.0.0
|
53
51
|
type: :runtime
|
54
52
|
version_requirements: *id002
|
55
53
|
- !ruby/object:Gem::Dependency
|
@@ -58,34 +56,20 @@ dependencies:
|
|
58
56
|
requirement: &id003 !ruby/object:Gem::Requirement
|
59
57
|
none: false
|
60
58
|
requirements:
|
61
|
-
- -
|
59
|
+
- - ~>
|
62
60
|
- !ruby/object:Gem::Version
|
63
|
-
hash:
|
61
|
+
hash: 25
|
64
62
|
segments:
|
65
63
|
- 1
|
66
64
|
- 0
|
67
|
-
|
65
|
+
- 7
|
66
|
+
version: 1.0.7
|
68
67
|
type: :runtime
|
69
68
|
version_requirements: *id003
|
70
|
-
- !ruby/object:Gem::Dependency
|
71
|
-
name: bson
|
72
|
-
prerelease: false
|
73
|
-
requirement: &id004 !ruby/object:Gem::Requirement
|
74
|
-
none: false
|
75
|
-
requirements:
|
76
|
-
- - ">="
|
77
|
-
- !ruby/object:Gem::Version
|
78
|
-
hash: 15
|
79
|
-
segments:
|
80
|
-
- 1
|
81
|
-
- 0
|
82
|
-
version: "1.0"
|
83
|
-
type: :runtime
|
84
|
-
version_requirements: *id004
|
85
69
|
- !ruby/object:Gem::Dependency
|
86
70
|
name: rspec
|
87
71
|
prerelease: false
|
88
|
-
requirement: &
|
72
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
89
73
|
none: false
|
90
74
|
requirements:
|
91
75
|
- - ">="
|
@@ -97,7 +81,7 @@ dependencies:
|
|
97
81
|
- 0
|
98
82
|
version: 1.3.0
|
99
83
|
type: :development
|
100
|
-
version_requirements: *
|
84
|
+
version_requirements: *id004
|
101
85
|
description: MongoModel is a MongoDB ORM for Ruby/Rails similar to ActiveRecord and DataMapper.
|
102
86
|
email: sam@sampohlenz.com
|
103
87
|
executables:
|