saviour 0.2.3 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -1
- data/.travis.yml +4 -13
- data/DECOMPOSE.md +66 -0
- data/Gemfile +1 -0
- data/README.md +39 -8
- data/lib/saviour/attribute_name_calculator.rb +15 -0
- data/lib/saviour/base_integrator.rb +53 -0
- data/lib/saviour/basic_model.rb +7 -0
- data/lib/saviour/config.rb +0 -1
- data/lib/saviour/file.rb +13 -34
- data/lib/saviour/life_cycle.rb +57 -0
- data/lib/saviour/source_filename_extractor.rb +21 -0
- data/lib/saviour/url_source.rb +1 -1
- data/lib/saviour/utils/class_attribute.rb +26 -0
- data/lib/saviour/version.rb +1 -1
- data/lib/saviour.rb +7 -155
- data/saviour.gemspec +1 -5
- data/spec/feature/access_to_model_and_mounted_as_spec.rb +13 -5
- data/spec/feature/versions_spec.rb +72 -49
- data/spec/models/attribute_name_calculator_spec.rb +11 -0
- data/spec/models/basic_model_spec.rb +51 -0
- data/spec/models/file_spec.rb +32 -55
- data/spec/models/url_source_spec.rb +5 -5
- data/spec/spec_helper.rb +2 -30
- data/spec/support/models.rb +7 -2
- metadata +12 -72
- data/Appraisals +0 -19
- data/gemfiles/4.0.gemfile +0 -9
- data/gemfiles/4.1.gemfile +0 -9
- data/gemfiles/4.2.gemfile +0 -9
- data/gemfiles/5.0.gemfile +0 -9
- data/lib/saviour/processors/digest.rb +0 -16
- data/spec/feature/crud_workflows_spec.rb +0 -143
- data/spec/feature/persisted_path_spec.rb +0 -34
- data/spec/feature/reload_model_spec.rb +0 -24
- data/spec/feature/validations_spec.rb +0 -171
- data/spec/models/processors/digest_spec.rb +0 -22
- data/spec/models/saviour_spec.rb +0 -80
- data/spec/support/schema.rb +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8673c1ddde4f8e16189bfd2d54ea7ad239155679
|
4
|
+
data.tar.gz: 8a97558346780d9105a5b2145fcdc3a038ae3bc6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 541052bdedf19b045140c57fa154d9f40007f56bd4fd4cd63895d99a2f577d24e4e10e39e876c597c1bc38fbd462797dcc59173d3b5f36d89aaa06e91c2cf52c
|
7
|
+
data.tar.gz: beea334842a6e686c7df91b933ed55a70298fb00818a950381c2d015bc138ff90ff7deff9b7d74f0a4c150a8069600800f5afc5ff002d77b92e3e1cb0fde2380
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -1,23 +1,14 @@
|
|
1
1
|
language: ruby
|
2
|
+
sudo: false
|
3
|
+
cache: bundler
|
2
4
|
rvm:
|
3
|
-
- 2.0.0
|
4
5
|
- 2.1.8
|
5
6
|
- 2.2.4
|
6
7
|
- 2.3.0
|
7
8
|
|
8
|
-
gemfile:
|
9
|
-
- gemfiles/4.0.gemfile
|
10
|
-
- gemfiles/4.1.gemfile
|
11
|
-
- gemfiles/4.2.gemfile
|
12
|
-
- gemfiles/5.0.gemfile
|
13
|
-
|
14
9
|
addons:
|
15
10
|
code_climate:
|
16
11
|
repo_token: abb288da5fac3efc45be30ffb37085314b9189ddccedf2cc68282777477e21c5
|
17
12
|
|
18
|
-
|
19
|
-
|
20
|
-
- rvm: 2.0.0
|
21
|
-
gemfile: gemfiles/5.0.gemfile
|
22
|
-
- rvm: 2.1.8
|
23
|
-
gemfile: gemfiles/5.0.gemfile
|
13
|
+
after_success:
|
14
|
+
- bundle exec codeclimate-test-reporter
|
data/DECOMPOSE.md
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
- New saviour-ar gem will provide what's currently in saviour gem
|
2
|
+
- saviour gem will be data-storage independent. It will provide a way to manage files with processors, but without a
|
3
|
+
lifecycle attached to a database-model. It will provide a more low level api.
|
4
|
+
- saviour-ar will provide the specific integration with active record.
|
5
|
+
|
6
|
+
|
7
|
+
# Saviour agnostic gem API
|
8
|
+
|
9
|
+
|
10
|
+
First, you'll need to define what files can be saved in what objects (any Ruby class). You can do that by including the `Saviour::BasicModel` module, example:
|
11
|
+
|
12
|
+
```
|
13
|
+
class MyObject
|
14
|
+
include Saviour::BasicModel
|
15
|
+
|
16
|
+
attach_file :image, ImageUploader
|
17
|
+
attach_file :scheme, FileUploader
|
18
|
+
end
|
19
|
+
```
|
20
|
+
|
21
|
+
Now, you can assign and work with the files associated to instances of MyObject with the following api:
|
22
|
+
|
23
|
+
```
|
24
|
+
# New file
|
25
|
+
|
26
|
+
a = MyObject.new
|
27
|
+
a.image = File.open('/path/image.jpg')
|
28
|
+
a.image.assign File.open('newfile.jpg')
|
29
|
+
a.image.changed? # => true
|
30
|
+
saved_path = a.image.write # => persists file in the storage and returns the path in which the file has been saved
|
31
|
+
|
32
|
+
b = MyObject.new
|
33
|
+
b.image.set_path!(saved_path) # => Link this image to the persisted image from before
|
34
|
+
b.image.exists? # => true
|
35
|
+
b.image.read # -> return bytes
|
36
|
+
b.image.delete # -> delete
|
37
|
+
b.image.exists? # => false
|
38
|
+
|
39
|
+
# ...
|
40
|
+
```
|
41
|
+
|
42
|
+
If you want to work directly managing the files associated to those objects, you can use the `Saviour::File` public API directly.
|
43
|
+
|
44
|
+
However, Saviour is designed to work with models that are saved in some kind of persistent storage, like a database of some sort. This is why Saviour also provides a generic `LifeCycle` service which you can use to simulate the persistence lifecycle of the object. You can then use:
|
45
|
+
|
46
|
+
```
|
47
|
+
a = MyObject.new image: File.open('image.jpg')
|
48
|
+
|
49
|
+
Saviour::LifeCycle.new(a).save!
|
50
|
+
Saviour::LifeCycle.new(a).delete!
|
51
|
+
```
|
52
|
+
|
53
|
+
`save!` will have the effect of saving all the attachments associated with the object, and `delete!` will have the effect of removing all the files associated with this object from the file storage defined.
|
54
|
+
|
55
|
+
Using LifeCycle you consider the object as a whole, while working with individual files you have more control, but you always operate with individual files.
|
56
|
+
|
57
|
+
The feature of versions, for example, only applies when you use the LifeCycle approach, since, by definition, a version is automatically constructed from the original file while this one is saved, and this involved operating in two or more attachments at the same time over an specific object. Since in Saviour versions can be managed exactly like regular attachments, such behavior don't apply when you work with File instances directly.
|
58
|
+
|
59
|
+
|
60
|
+
# Saviour API for developers
|
61
|
+
|
62
|
+
If you want to develop a new gem to integrate Saviour with another persistence technology, you need to do two things.
|
63
|
+
|
64
|
+
1) Write a class with the api #read, #write and #persisted? and give it to Savior, so he knows how to work with the persistence layer.
|
65
|
+
2) Write a new module for the final users to include in their models, providing the expected hooks so that the usage of the `LifeCycle` is automatic and transparent.
|
66
|
+
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -56,12 +56,14 @@ to an ActiveRecord object) and processings. See the following example of a model
|
|
56
56
|
|
57
57
|
```
|
58
58
|
class Post < ActiveRecord::Base
|
59
|
-
|
59
|
+
include Saviour::Model
|
60
|
+
|
61
|
+
# The posts table must have an `image` string column.
|
60
62
|
attach_file :image, PostImageUploader
|
61
63
|
end
|
62
64
|
|
63
65
|
class PostImageUploader < Saviour::BaseUploader
|
64
|
-
store_dir
|
66
|
+
store_dir { "/default/path/#{model.id}/#{attached_as}" }
|
65
67
|
|
66
68
|
process :resize, width: 500, height: 500
|
67
69
|
|
@@ -281,7 +283,7 @@ and UrlSource.
|
|
281
283
|
|
282
284
|
### StringSource
|
283
285
|
|
284
|
-
This is just a wrapper class that gives no additional
|
286
|
+
This is just a wrapper class that gives no additional behavior except for implementing the required API. Use it as:
|
285
287
|
|
286
288
|
```
|
287
289
|
foo = Saviour::StringSource.new("my raw contents", "filename.jpg")
|
@@ -310,7 +312,7 @@ example:
|
|
310
312
|
|
311
313
|
```
|
312
314
|
class ExampleUploader < Saviour::BaseUploader
|
313
|
-
store_dir
|
315
|
+
store_dir { "/default/path/#{model.id}" }
|
314
316
|
|
315
317
|
process :resize, width: 50, height: 50
|
316
318
|
|
@@ -324,7 +326,7 @@ class ExampleUploader < Saviour::BaseUploader
|
|
324
326
|
end
|
325
327
|
|
326
328
|
version(:thumb) do
|
327
|
-
store_dir
|
329
|
+
store_dir { "/default/path/#{model.id}/versions" }
|
328
330
|
process :resize, with: 10, height: 10
|
329
331
|
end
|
330
332
|
|
@@ -431,6 +433,7 @@ Example of validations:
|
|
431
433
|
|
432
434
|
```
|
433
435
|
class Post < ActiveRecord::Base
|
436
|
+
include Saviour::Model
|
434
437
|
attach_file :image, PostImageUploader
|
435
438
|
|
436
439
|
attach_validation(:image) do |contents, filename|
|
@@ -447,6 +450,7 @@ Validations can also be declared passing a method name instead of a block, like
|
|
447
450
|
|
448
451
|
```
|
449
452
|
class Post < ActiveRecord::Base
|
453
|
+
include Saviour::Model
|
450
454
|
attach_file :image, PostImageUploader
|
451
455
|
attach_validation :image, :check_size
|
452
456
|
|
@@ -481,8 +485,8 @@ This is a compilation of common questions or features regarding file uploads.
|
|
481
485
|
|
482
486
|
### Digested filename
|
483
487
|
|
484
|
-
A common use case is to create a processor to include a digest of the file in the filename
|
485
|
-
for the user, but a simple example of such processor is this:
|
488
|
+
A common use case is to create a processor to include a digest of the file in the filename, in order to automatically
|
489
|
+
expire caches. The implementation is left for the user, but a simple example of such processor is this:
|
486
490
|
|
487
491
|
```
|
488
492
|
def digest_filename(contents, filename, opts = {})
|
@@ -497,8 +501,35 @@ for the user, but a simple example of such processor is this:
|
|
497
501
|
end
|
498
502
|
```
|
499
503
|
|
500
|
-
### Getting metadata from the file
|
501
504
|
### How to recreate versions
|
505
|
+
|
506
|
+
Recreating a version based on the master file can be easily done by just assigning the master file to the version and
|
507
|
+
saving the model. You just need a little bit more code in order to preserve the current version filename, for example,
|
508
|
+
if that's something you want.
|
509
|
+
|
510
|
+
An example service that can do that is the following:
|
511
|
+
|
512
|
+
```
|
513
|
+
class SaviourRecreateVersionsService
|
514
|
+
def initialize(model)
|
515
|
+
@model = model
|
516
|
+
end
|
517
|
+
|
518
|
+
def recreate!(attached_as, *versions)
|
519
|
+
base = @model.send(attached_as).read
|
520
|
+
|
521
|
+
versions.each do |version|
|
522
|
+
current_filename = @model.send(attached_as, version).filename
|
523
|
+
@model.send(attached_as, version).assign(Saviour::StringSource.new(base, current_filename))
|
524
|
+
end
|
525
|
+
|
526
|
+
@model.save!
|
527
|
+
end
|
528
|
+
end
|
529
|
+
```
|
530
|
+
|
531
|
+
### Getting metadata from the file
|
532
|
+
|
502
533
|
### Caching across redisplays in normal forms
|
503
534
|
### Introspection (Class.attached_files)
|
504
535
|
### Processing in background
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Saviour
|
2
|
+
class AttributeNameCalculator
|
3
|
+
def initialize(attached_as, version = nil)
|
4
|
+
@attached_as, @version = attached_as, version
|
5
|
+
end
|
6
|
+
|
7
|
+
def name
|
8
|
+
if @version
|
9
|
+
"#{@attached_as}_#{@version}"
|
10
|
+
else
|
11
|
+
@attached_as.to_s
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Saviour
|
2
|
+
class BaseIntegrator
|
3
|
+
def initialize(klass)
|
4
|
+
@klass = klass
|
5
|
+
end
|
6
|
+
|
7
|
+
def file_instantiator_hook(model, file_instance, attach_as, version)
|
8
|
+
# noop
|
9
|
+
end
|
10
|
+
|
11
|
+
def attach_file_hook(klass, attach_as, uploader_klass)
|
12
|
+
# noop
|
13
|
+
end
|
14
|
+
|
15
|
+
def setup!
|
16
|
+
raise "You cannot include Saviour twice in the same class" if @klass.respond_to?(:attached_files)
|
17
|
+
|
18
|
+
@klass.send :extend, ClassAttribute
|
19
|
+
@klass.class_attribute :attached_files
|
20
|
+
@klass.attached_files = {}
|
21
|
+
|
22
|
+
a = self
|
23
|
+
b = @klass
|
24
|
+
|
25
|
+
@klass.define_singleton_method "attach_file" do |attach_as, uploader_klass|
|
26
|
+
a.attach_file_hook(self, attach_as, uploader_klass)
|
27
|
+
|
28
|
+
versions = uploader_klass.versions || []
|
29
|
+
b.attached_files[attach_as] ||= []
|
30
|
+
b.attached_files[attach_as] += versions
|
31
|
+
|
32
|
+
b.class_eval do
|
33
|
+
define_method(attach_as) do |version = nil|
|
34
|
+
instance_variable_get("@__uploader_#{version}_#{attach_as}") || begin
|
35
|
+
new_file = ::Saviour::File.new(uploader_klass, self, attach_as, version)
|
36
|
+
a.file_instantiator_hook(self, new_file, attach_as, version)
|
37
|
+
|
38
|
+
instance_variable_set("@__uploader_#{version}_#{attach_as}", new_file)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
define_method("#{attach_as}=") do |value|
|
43
|
+
send(attach_as).assign(value)
|
44
|
+
end
|
45
|
+
|
46
|
+
define_method("#{attach_as}_changed?") do
|
47
|
+
send(attach_as).changed?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/saviour/config.rb
CHANGED
@@ -17,7 +17,6 @@ module Saviour
|
|
17
17
|
Thread.current["Saviour::Config"][:processing_enabled] = value
|
18
18
|
end
|
19
19
|
|
20
|
-
|
21
20
|
def storage
|
22
21
|
Thread.current["Saviour::Config"] ||= {}
|
23
22
|
Thread.current["Saviour::Config"][:storage] || Thread.main["Saviour::Config"][:storage] || NotImplemented.new
|
data/lib/saviour/file.rb
CHANGED
@@ -2,47 +2,32 @@ require 'securerandom'
|
|
2
2
|
|
3
3
|
module Saviour
|
4
4
|
class File
|
5
|
-
|
6
|
-
def initialize(source)
|
7
|
-
@source = source
|
8
|
-
end
|
9
|
-
|
10
|
-
def detected_filename
|
11
|
-
original_filename || path_filename
|
12
|
-
end
|
13
|
-
|
14
|
-
def original_filename
|
15
|
-
@source.respond_to?(:original_filename) && @source.original_filename.present? && @source.original_filename
|
16
|
-
end
|
17
|
-
|
18
|
-
def path_filename
|
19
|
-
@source.respond_to?(:path) && @source.path.present? && ::File.basename(@source.path)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
5
|
+
attr_reader :persisted_path
|
23
6
|
|
24
7
|
def initialize(uploader_klass, model, attached_as, version = nil)
|
25
8
|
@uploader_klass, @model, @attached_as = uploader_klass, model, attached_as
|
26
9
|
@version = version
|
27
|
-
|
28
|
-
@persisted = !!persisted_path
|
29
10
|
@source_was = @source = nil
|
30
11
|
end
|
31
12
|
|
13
|
+
def set_path!(path)
|
14
|
+
@persisted_path = path
|
15
|
+
end
|
16
|
+
|
32
17
|
def exists?
|
33
|
-
persisted? && Config.storage.exists?(persisted_path)
|
18
|
+
persisted? && Config.storage.exists?(@persisted_path)
|
34
19
|
end
|
35
20
|
|
36
21
|
def read
|
37
|
-
persisted? && exists? && Config.storage.read(persisted_path)
|
22
|
+
persisted? && exists? && Config.storage.read(@persisted_path)
|
38
23
|
end
|
39
24
|
|
40
25
|
def delete
|
41
|
-
persisted? && exists? && Config.storage.delete(persisted_path)
|
26
|
+
persisted? && exists? && Config.storage.delete(@persisted_path)
|
42
27
|
end
|
43
28
|
|
44
29
|
def public_url
|
45
|
-
persisted? && Config.storage.public_url(persisted_path)
|
30
|
+
persisted? && Config.storage.public_url(@persisted_path)
|
46
31
|
end
|
47
32
|
|
48
33
|
alias_method :url, :public_url
|
@@ -52,13 +37,13 @@ module Saviour
|
|
52
37
|
|
53
38
|
@source_data = nil
|
54
39
|
@source = object
|
55
|
-
@
|
40
|
+
@persisted_path = nil if object
|
56
41
|
|
57
42
|
object
|
58
43
|
end
|
59
44
|
|
60
45
|
def persisted?
|
61
|
-
|
46
|
+
!!@persisted_path
|
62
47
|
end
|
63
48
|
|
64
49
|
def changed?
|
@@ -66,7 +51,7 @@ module Saviour
|
|
66
51
|
end
|
67
52
|
|
68
53
|
def filename
|
69
|
-
|
54
|
+
::File.basename(@persisted_path) if persisted?
|
70
55
|
end
|
71
56
|
|
72
57
|
def with_copy
|
@@ -96,7 +81,7 @@ module Saviour
|
|
96
81
|
|
97
82
|
path = uploader.write(source_data, filename_to_be_assigned)
|
98
83
|
@source_was = @source
|
99
|
-
@
|
84
|
+
@persisted_path = path
|
100
85
|
path
|
101
86
|
end
|
102
87
|
|
@@ -114,11 +99,5 @@ module Saviour
|
|
114
99
|
def uploader
|
115
100
|
@uploader ||= @uploader_klass.new(version: @version, data: {model: @model, attached_as: @attached_as})
|
116
101
|
end
|
117
|
-
|
118
|
-
def persisted_path
|
119
|
-
if @model.persisted? || @model.destroyed?
|
120
|
-
@model.read_attribute(ColumnNamer.new(@attached_as, @version).name)
|
121
|
-
end
|
122
|
-
end
|
123
102
|
end
|
124
103
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Saviour
|
2
|
+
class LifeCycle
|
3
|
+
def initialize(model, persistence_klass = nil)
|
4
|
+
raise "Please provide an object compatible with Saviour." unless model.class.respond_to?(:attached_files)
|
5
|
+
|
6
|
+
@persistence_klass = persistence_klass
|
7
|
+
@model = model
|
8
|
+
end
|
9
|
+
|
10
|
+
def delete!
|
11
|
+
attached_files.each do |column, versions|
|
12
|
+
(versions + [nil]).each { |version| @model.send(column, version).delete if @model.send(column, version).exists? }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def save!
|
17
|
+
attached_files.each do |column, versions|
|
18
|
+
base_file_changed = @model.send(column).changed?
|
19
|
+
original_content = @model.send(column).source_data if base_file_changed
|
20
|
+
|
21
|
+
versions.each do |version|
|
22
|
+
if @model.send(column, version).changed?
|
23
|
+
upload_file(column, version)
|
24
|
+
elsif base_file_changed
|
25
|
+
@model.send(column, version).assign(StringSource.new(original_content, default_version_filename(column, version)))
|
26
|
+
upload_file(column, version)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
upload_file(column, nil) if base_file_changed
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def default_version_filename(column, version)
|
38
|
+
filename = @model.send(column).filename_to_be_assigned
|
39
|
+
"#{::File.basename(filename, ".*")}_#{version}#{::File.extname(filename)}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def upload_file(column, version)
|
43
|
+
name = AttributeNameCalculator.new(column, version).name
|
44
|
+
persistence_layer = @persistence_klass.new(@model) if @persistence_klass
|
45
|
+
current_path = persistence_layer.read(name) if persistence_layer
|
46
|
+
|
47
|
+
Config.storage.delete(current_path) if current_path
|
48
|
+
|
49
|
+
new_path = @model.send(column, version).write
|
50
|
+
persistence_layer.write(name, new_path) if persistence_layer
|
51
|
+
end
|
52
|
+
|
53
|
+
def attached_files
|
54
|
+
@model.class.attached_files || {}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Saviour
|
2
|
+
class SourceFilenameExtractor
|
3
|
+
def initialize(source)
|
4
|
+
@source = source
|
5
|
+
end
|
6
|
+
|
7
|
+
def detected_filename
|
8
|
+
original_filename || path_filename
|
9
|
+
end
|
10
|
+
|
11
|
+
def original_filename
|
12
|
+
value = @source.original_filename if @source.respond_to?(:original_filename)
|
13
|
+
value if !value.nil? && value != ''
|
14
|
+
end
|
15
|
+
|
16
|
+
def path_filename
|
17
|
+
value = @source.path if @source.respond_to?(:path)
|
18
|
+
::File.basename(value) if !value.nil? && value != ''
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/saviour/url_source.rb
CHANGED
@@ -24,7 +24,7 @@ module Saviour
|
|
24
24
|
private
|
25
25
|
|
26
26
|
def resolve(uri, max_redirects = MAX_REDIRECTS)
|
27
|
-
raise RuntimeError, "Max number of allowed redirects reached (#{MAX_REDIRECTS}) when resolving #{
|
27
|
+
raise RuntimeError, "Max number of allowed redirects reached (#{MAX_REDIRECTS}) when resolving #{uri}" if max_redirects == 0
|
28
28
|
|
29
29
|
response = Net::HTTP.get_response(uri)
|
30
30
|
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Port and simplification of ActiveSupport class attribute
|
2
|
+
module Saviour
|
3
|
+
module ClassAttribute
|
4
|
+
def class_attribute(*attrs)
|
5
|
+
attrs.each do |name|
|
6
|
+
singleton_class.instance_eval do
|
7
|
+
undef_method(name) if method_defined?(name)
|
8
|
+
end
|
9
|
+
|
10
|
+
define_singleton_method(name) { nil }
|
11
|
+
|
12
|
+
singleton_class.instance_eval do
|
13
|
+
undef_method("#{name}=") if method_defined?("#{name}=")
|
14
|
+
end
|
15
|
+
|
16
|
+
define_singleton_method("#{name}=") do |val|
|
17
|
+
singleton_class.class_eval do
|
18
|
+
undef_method(name) if method_defined?(name)
|
19
|
+
define_method(name) { val }
|
20
|
+
end
|
21
|
+
val
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/saviour/version.rb
CHANGED