clir-data_manager 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
data/README.md ADDED
@@ -0,0 +1,279 @@
1
+ # Clir::DataManager
2
+
3
+ 1. You define (deeply) the instance properties,
4
+ 2. you "attach" the DataManager to your class,
5
+ 3. you can then create, edit, remove and save your instances in command line.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'clir/data_manager'
13
+ ```
14
+
15
+ En attendant que le gem soit complet, mettre le code suivant dans le programme devant l'utiliser (pas besoin de faire `bundle install` etc.) :
16
+
17
+ ~~~
18
+
19
+ $LOAD_PATH.unshift File.join(Dir.home,'Programmes','Gems','clir-data_manager','lib')
20
+
21
+ ~~~
22
+
23
+ And then execute:
24
+
25
+ $ bundle install
26
+
27
+ Or install it yourself as:
28
+
29
+ $ gem install clir-data_manager
30
+
31
+ ## Usage
32
+
33
+ Pour qu'une classe quelconque puisse utiliser le gem, lui mettre :
34
+
35
+ ~~~ruby
36
+ module MonModule
37
+ class MaClasse
38
+ include ClirDataManagerConstants # <==== usefull
39
+
40
+ DATA_PROPERTIES = [...] # <== see below
41
+ @@save_system = :card # <== see below
42
+ @@save_format = :yaml # <== see below
43
+ @@save_location = '/path/to/folder' # <== see below
44
+ end
45
+ end
46
+
47
+ Clir::DataManager.new(MonModule::MaClasse)
48
+
49
+ ~~~
50
+
51
+ ### Définition du système de sauvegarde
52
+
53
+ Il y a 6 façons de sauvegarder les données avec `Clir::DataManager` :
54
+
55
+ * dans un unique fichier, au format CSV ou YAML,
56
+ * dans des fiches individuelles, au format CSV ou YAML,
57
+ * par un fichier de configuration, au format CSV ou YAML [Ce système n'est pas encore implémenté, il fonctionnera sur la base d'un fichier de configuration qui définira la façon de sauvegarder les données, toujours en CSV ou YAML]
58
+
59
+
60
+
61
+ ### Définition des propriétés de l'instance
62
+
63
+ Le module `ClirDataManagerConstants` permet de charger les constants comme `REQUIRED`, `EDITABLE`, etc.
64
+
65
+ Il faut ensuite définir les propriétés des instances de cette classe, à l'aide la constante `DATA_PROPERTIES` :
66
+
67
+ ~~~ruby
68
+ module MonModule
69
+ class MaClasse
70
+
71
+ DATA_PROPERTIES = [
72
+ {prop: :id, type: :id, name: "ID", specs: REQUIRED, default: :new_id},
73
+ {prop: :name, type: :string, name:"Votre nom", specs:REQUIRED|EDITABLE|DISPLAYABLE},
74
+ {prop: :name, type: :string, name:"Votre genre", specs:ALL, default: 'F'}
75
+ # ...
76
+ ]
77
+
78
+ end
79
+ end
80
+ ~~~
81
+
82
+ On pourrait aussi envoyer les données lors de l'installation du manager de propriétés :
83
+
84
+ ~~~ruby
85
+
86
+ pdata = [
87
+ {prop: :id, ...}
88
+ ]
89
+
90
+ Clir::DataManager.new(MonModule::MaClasse, pdata)
91
+
92
+ ~~~
93
+
94
+ Pour créer la première instance, il suffit ensuite de faire :
95
+
96
+ ~~~ruby
97
+
98
+ MonModule::MaClasse.new.create
99
+
100
+ ~~~
101
+
102
+ Pour éditer ou afficher une instance de la classe :
103
+
104
+ ~~~ruby
105
+ i = MonModule::MaClasse.new()
106
+
107
+ i.edit
108
+
109
+ i.show
110
+
111
+ ~~~
112
+
113
+ Les données sont toujours placées dans la propriété `@data` de l'instance, qui peut être définie à l'instanciation, une fois qu'on a les données :
114
+
115
+ ~~~ruby
116
+
117
+ module MonModule
118
+ class MaClasse
119
+
120
+ def initialize(data)
121
+ @data = data
122
+ end
123
+ end
124
+ end
125
+ ~~~
126
+
127
+ Noter qu'il n'est pas nécessaire de faire `attribute_reader: :data` puisque data est automatiquement défini en accesseur.
128
+
129
+ Noter qu'il n'est pas non plus nécessaire de créer toutes les méthodes-propriété qui sont nécessaires habituellement pour récupérer et définir les valeurs.
130
+
131
+ À partir du moment où `DATA_PROPERTIES` définit :
132
+
133
+ ~~~ruby
134
+ DATA_PROPERTIES = [
135
+ # ...
136
+ {prop: :maprop, ...}
137
+ # ...
138
+ ]
139
+ ~~~
140
+
141
+ … alors le manager de propriétés définit les méthodes :
142
+
143
+ ~~~ruby
144
+ module MonModule
145
+ class MaClasse
146
+
147
+ def maprop
148
+ return @data[:maprop]
149
+ end
150
+
151
+ def maprop=(value)
152
+ @data.merge!(maprop: value)
153
+ end
154
+
155
+ end
156
+ end
157
+ ~~~
158
+
159
+ Donc, on récupère et définit les valeurs de cette manière :
160
+
161
+ ~~~ruby
162
+
163
+ i = MonModule::MaClasse.new({maprop: "Valeur courante"})
164
+
165
+ i.maprop
166
+ # => "Valeur courante"
167
+
168
+ i.maprop = "Nouvelle valeur"
169
+
170
+ i.maprop
171
+ # => "Nouvelle valeur"
172
+
173
+ ~~~
174
+
175
+ ### Définition des propriétés
176
+
177
+ C'est donc la grosse partie pour utiliser profitablement de `DataManager`. Une bonne définition des propriétés conduit à une utilisation tout à fait efficace.
178
+
179
+ #### Identifiant de la propriété
180
+
181
+ Cet identifiant se définit à l'aide de l'attribut `:prop`.
182
+
183
+ ~~~ruby
184
+ DATA_PROPERTIES = [
185
+ { prop: :maprop }
186
+ ]
187
+ ~~~
188
+
189
+ C'est par ce nom que l'instance connaitra la valeur consignée. Pour définir quelqu'un, par exemple, on aura :
190
+
191
+ ~~~ruby
192
+ DATA_PROPERTIES = [
193
+ { prop: :prenom },
194
+ { prop: :nom },
195
+ ]
196
+ ~~~
197
+
198
+ Et une instance pourra utiliser :
199
+
200
+ ~~~ruby
201
+ simone = MaClasse.new
202
+ simone.prenom
203
+ # => "Simone"
204
+ simone.nom
205
+ # => "De Beauvoir"
206
+ ~~~
207
+
208
+ #### Question personnalisée
209
+
210
+ Par défaut, la question « Nouvelle valeur pour “`<propriété>`” » est posée pour modifier une propriété.
211
+
212
+ On peut néanmoins définir une autre question avec l'attribut `:quest` qui peut contenir des valeurs de template <b>qui ne doivent utiliser que les valeurs dans les `data` de l'instance</b>.
213
+
214
+ TODO: Plus tard on pourra aussi imaginer évaluer la question ou fournir d'autres valeurs au template (par exemple un attribut `:quest_values` qui pourrait être définie en dur ou par une procédure qui utiliserait l'instance en premier argument).
215
+
216
+ #### Valeur par défaut dans les propriétés
217
+
218
+ Une donnée propriété peut définir la valeur par défaut de cette propriété pour une instance donnée.
219
+
220
+ ~~~ruby
221
+ DATA_PROPERTIES = [
222
+ {prop: :maprop ... default: <...> }
223
+ ]
224
+ ~~~
225
+
226
+ Cette valeur peut être de diférent type :
227
+
228
+ * une valeur explicite :
229
+
230
+ ~~~ruby
231
+ default: "Ma valeur par défafut"
232
+ default: 12
233
+ default: true
234
+ ~~~
235
+
236
+ * une procédure :
237
+
238
+ ~~~ruby
239
+ default: Proc.new() { |instance| instance.name.length }
240
+ ~~~
241
+
242
+ > Noter que l'instance est toujours transmise en premier argument.
243
+
244
+ * une méthode d'instance :
245
+
246
+ ~~~ruby
247
+ class MaClasse
248
+ def valeur_default_pour_prop
249
+ return "oui"
250
+ end
251
+ end
252
+
253
+ default: :valeur_default_pour_prop
254
+ ~~~
255
+
256
+ * une méthode de classe :
257
+
258
+ ~~~ruby
259
+ class MaClasse
260
+ def self.valeur_default_pour_prop
261
+ @@valeur_prop += 1
262
+ end
263
+ end
264
+
265
+ default: :valeur_default_pour_prop
266
+ ~~~
267
+
268
+
269
+
270
+ ## Development
271
+
272
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
273
+
274
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
275
+
276
+ ## Contributing
277
+
278
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/clir-data_manager.
279
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "clir/data_manager"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,34 @@
1
+ require_relative 'lib/clir/data_manager/version'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "clir-data_manager"
5
+ s.version = Clir::DataManager::VERSION
6
+ s.authors = ["PhilippePerret"]
7
+ s.email = ["philippe.perret@yahoo.fr"]
8
+ s.licenses = ['Nonstandard']
9
+
10
+ s.summary = %q{Properties Manager for classes and instances}
11
+ s.description = %q{This gem provides useful Classes and methods to deal with instances properties edition and display}
12
+ s.homepage = "https://github.com/PhilippePerret/clir-data_manager"
13
+ s.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
14
+
15
+ # s.metadata["allowed_push_host"] = "https://github.com/PhilippePerret/clir-data_manager"
16
+ s.metadata["allowed_push_host"] = "https://rubygems.org"
17
+
18
+ s.add_dependency 'clir', '~> 0.4'
19
+ s.add_development_dependency 'minitest', '~> 5.10'
20
+ s.add_development_dependency 'minitest-color', '>= 5'
21
+
22
+ s.metadata["homepage_uri"] = s.homepage
23
+ s.metadata["source_code_uri"] = s.homepage
24
+ s.metadata["changelog_uri"] = "https://github.com/PhilippePerret/clir-data_manager/CHANGELOG.md"
25
+
26
+ # Specify which files should be added to the gem when it is released.
27
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
28
+ s.files = Dir.chdir(File.expand_path('..', __FILE__)) do
29
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|features)/}) }
30
+ end
31
+ s.bindir = "exe"
32
+ s.executables = s.files.grep(%r{^exe/}) { |f| File.basename(f) }
33
+ s.require_paths = ["lib"]
34
+ end
@@ -0,0 +1,30 @@
1
+ =begin
2
+ Clir::DataManager::Manager::Displayer
3
+ -------------------------------------
4
+ To display a instance values
5
+ =end
6
+ module Clir
7
+ module DataManager
8
+ class Manager
9
+ class Displayer
10
+
11
+ attr_reader :manager
12
+
13
+ def initialize(manager)
14
+ @manager = manager
15
+ end
16
+
17
+ def show(instance, options)
18
+ liste = []
19
+ manager.each_property do |property|
20
+ next if not(property.displayable?(instance))
21
+ # puts "Je dois afficher la propriété @#{property.prop} de specs #{property.specs}"
22
+ liste << [property.name, property.formated_value_in(instance)]
23
+ end
24
+ puts labelize(liste)
25
+ end
26
+
27
+ end #/class Displayer
28
+ end #/class Manager
29
+ end #/module DataManager
30
+ end #/module Clir
@@ -0,0 +1,208 @@
1
+ =begin
2
+ Clir::DataManager::Manager::Editor
3
+ -------------------------------------
4
+ To edit a instance values (i.e. a proprerty)
5
+
6
+ 2 editing system:
7
+
8
+ 1. Property after property
9
+ 2. [default] All properties are displayed and user chose which
10
+ one to edit. When all required properties are defined, the
11
+ user can save the values.
12
+
13
+
14
+ =end
15
+ module Clir
16
+ module DataManager
17
+ class Manager
18
+ class Editor
19
+
20
+ attr_reader :manager
21
+
22
+ def initialize(manager)
23
+ @manager = manager
24
+ end
25
+
26
+ def edit(instance, options)
27
+ data_modified = false
28
+ data_saved = false
29
+ error = nil
30
+ while true
31
+ #
32
+ # Pour choisir la propriété à définir (et l'index par défaut)
33
+ #
34
+ choices, index_default = set_choices_with(instance)
35
+ clear unless debug?
36
+ #
37
+ # Si une erreur a été rencontrée
38
+ #
39
+ puts error.rouge unless error.nil?
40
+ #
41
+ # L'user peut choisir la propriété à définir ou le choix
42
+ # "Enregistrer" pour enregistrer l'instance
43
+ #
44
+ case prop = Q.select((options[:question]||(MSG[:define_thing] % instance.class.class_name)).jaune, choices, { per_page:choices.count, default: (index_default unless choices.empty?), filter:true, show_help:false, echo:false} )
45
+ when NilClass
46
+ break
47
+ when :save
48
+ if not(all_required?(instance))
49
+ error = MSG[:all_required_data_must_be_defined]
50
+ elsif data_modified && confirmed?(instance)
51
+ instance.save
52
+ data_saved = true
53
+ break
54
+ end
55
+ else
56
+ modified = prop.edit(instance, options)
57
+ data_modified = true if modified
58
+ end
59
+ end
60
+ # / while
61
+ if data_modified && not(data_saved)
62
+ unless Q.yes?(MSG[:data_not_saved_cancel].orange)
63
+ instance.save # on sauve les données, finalement
64
+ end
65
+ end
66
+ end
67
+
68
+ # Confirmation (ou non) des données de l'instance
69
+ def confirmed?(instance)
70
+ clear unless debug?
71
+ instance.show
72
+ puts "\n\n"
73
+ return Q.yes?(MSG[:q_confirm_data].jaune)
74
+ end
75
+
76
+ # @return TRUE si toutes les propriétés requises sont définies
77
+ # pour l'instance donnée
78
+ def all_required?(instance)
79
+ manager.properties.each do |property|
80
+ if property.required?(instance) && instance.send(property.prop).nil?
81
+ return false
82
+ end
83
+ end
84
+ return true
85
+ end
86
+
87
+ # Tty-prompt choices panel
88
+ # Should be update after each user input
89
+ #
90
+ # @return [choices, index_default]
91
+ # At creation, index_default is the first required property which
92
+ # is undefined (1-start)
93
+ #
94
+ def set_choices_with(instance)
95
+ requirement_missing = false
96
+ max_label_width = 0
97
+ cs = manager.properties.map do |property|
98
+ next if not(property.editable?(instance))
99
+ pvalue = property.formated_value_in(instance)
100
+ pname = property.name(instance)
101
+ pname || raise(ERRORS[:no_name_for_property] % property.prop.inspect)
102
+ max_label_width = pname.length if pname.length > max_label_width
103
+ [pname, pvalue]
104
+ end.compact
105
+
106
+ # Ajout de la gouttière entre label et valeur
107
+ max_label_width += 4
108
+
109
+ #
110
+ # Le menu qui devra être sélectionné
111
+ #
112
+ index_default = nil
113
+ #
114
+ # Index courant réel
115
+ #
116
+ # Avant qu'il existe des propriétés conditionnelles (:if) on
117
+ # pouvait prendre l'index de la propriété pour savoir quel menu
118
+ # sélectionner. Maintenant, on doit utiliser cette variable
119
+ # +real_current_index+ pour connaitre l'index réel du menu à
120
+ # sélectionner ensuite.
121
+ #
122
+ # On commence à 1 pour avoir le premier choix à 2 car le
123
+ # premier item est toujours l'item "Enregistrer"
124
+ #
125
+ real_current_index = 1
126
+
127
+ #
128
+ # Boucle pour récupérer tous les menus à afficher
129
+ #
130
+ cs = manager.properties.map do |property|
131
+ #
132
+ # Si ce n'est pas une propriété éditable (pas ses :specs), on
133
+ # la passe.
134
+ #
135
+ next if not(property.editable?(instance))
136
+ #
137
+ # Est-ce une propriété requise ? (en fonction ou non de l'instance)
138
+ #
139
+ is_required = property.required?(instance)
140
+
141
+ real_current_index += 1 if index_default.nil?
142
+ curval = instance.send(property.prop)
143
+ isdef = curval != nil
144
+ #
145
+ # Si c'est une création d'instance, on se placera automati-
146
+ # quement sur le premier champ (property) qui n'est pas défini
147
+ #
148
+ if index_default.nil? && instance.new? && not(isdef)
149
+ index_default = real_current_index
150
+ end
151
+ #
152
+ # Si c'est une propriété requise et qu'elle n'est pas définie,
153
+ # on indique qu'il manque des définitions avant d'avoir la
154
+ # possibilité d'enregistrer. On en profite aussi, si c'est la
155
+ # première, pour définir l'index par défaut
156
+ #
157
+ if is_required && not(isdef)
158
+ requirement_missing = true
159
+ index_default = real_current_index if index_default.nil?
160
+ end
161
+ # choix = cs.shift
162
+ # choix = "#{property.name(instance).ljust(max_label_width)}#{curval}"
163
+ #
164
+ # Faut-il utiliser une méthode de formatage d'affichage
165
+ #
166
+ fvalue = property.formated_value_in(instance)
167
+ choix = "#{property.name(instance).ljust(max_label_width)}#{fvalue}"
168
+ #
169
+ # Couleur en fonction de propriété requise ou non
170
+ #
171
+ choix = choix.send(isdef ? :bleu : :orange) if is_required
172
+ #
173
+ # Marque de propriété requise
174
+ #
175
+ choix = (is_required ? '* '.rouge : ' ') + choix
176
+ #
177
+ # Le choice à utiliser
178
+ #
179
+ {name: choix, value: property}
180
+ end.compact
181
+
182
+ #
183
+ # Si toutes les valeurs requises sont définies, on peut ajouter
184
+ # un menu pour enregistrer
185
+ #
186
+ savable = not(requirement_missing)
187
+ choix_save = {name: MSG[:save].send(savable ? :bleu : :gris), value: :save}
188
+ choix_save.merge!(disabled: "(#{MESSAGES[:still_required_values]})".gris) if not(savable)
189
+ cs.unshift(choix_save)
190
+ #
191
+ # On a toujours la possibilité d'annuler l'édition
192
+ #
193
+ cs << CHOIX_RENONCER
194
+ #
195
+ # Si l'index par défaut n'a pas pu être déterminé, on prend le
196
+ # milieu de la liste de propriétés
197
+ #
198
+ index_default = cs.count / 2 if index_default.nil?
199
+ #
200
+ # On retourne la liste des choix et l'item sélectionné
201
+ #
202
+ return [cs, index_default]
203
+ end
204
+
205
+ end #/class Editor
206
+ end #/class Manager
207
+ end #/module DataManager
208
+ end #/module Clir