File: lcdconf2model.pl

package info (click to toggle)
libconfig-model-lcdproc-perl 2.055-2
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 344 kB
  • sloc: perl: 320; makefile: 11
file content (451 lines) | stat: -rwxr-xr-x 14,417 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
#!/usr/bin/perl

use strict;
use warnings;

# This script uses all the information available in LCDd.conf to create a model
# for LCDd configuration file

# How does this work ?

# The conventions used in LCDd.conf template file are written in a way
# which makes it relatively easy to parse to get all required
# information to build a model.

# All drivers are listed, most parameters have default values and
# legal values written in comments in a uniform way. Hence this file
# (and comments) can be parsed to retrieve the information required to
# create a consistent model for LcdProc configuration. Some useful
# parameters are commented out in LCD.conf. So some processing is
# required to be able to create a model with these commented
# parameters.  See below for this processing.

# This script performs the following tasks:
# 1/ check whether generating the model is necessary (or possible)
# 2/ pre-process LCDd.conf template
# 3/ parse the new LCDd.conf template
# 4/ mine the information there and translate them in a format suitable to create
#    a model. Comments are used to provide default and legal values and also to provide
#    user documentation
# 5/ Write the resulting LCDd model

use Config::Model 2.141;
use Config::Model::Itself 2.022;    # to create the model

use 5.010;
use Path::Tiny;
use Getopt::Long;

my $verbose = 0;
my $show_model = 0;
my $force = 0;
my $source = "lcdproc/LCDd.conf" ;

my $result = GetOptions (
    "verbose"  => \$verbose,
    "model" => \$show_model,
    "force" => \$force,
    "file=s" => \$source,
);

die "Unknown option. Expected -verbose, -force, -file  or -model" unless $result ;

########################
#
# Step 1: Check whether generating lcdproc model is necessary.

my $target = "lib/Config/Model/models/LCDd.pl";
my $script = "script/lcdconf2model.pl";

if (-e $target and -M $target < -M $script and -M $target < -M $source) {
    say "LcdProc model is up to date";
    exit unless $force;
}

say "Building lcdproc model from upstream LCDd.conf file $source" ;

###########################
#
# Step 2: pre-process LCDd.conf (INI file)

# Here's the LCDd.conf pre-processing mentioned above

# read LCDd.conf
my @lines    = path($source)->lines;

# un-comment commented parameters and put value as default value
foreach my $line (@lines) {
    $line =~ s/^#(\w+)=(.*)/# [default: $2]\n$1=$2/;
}

# write pre-processed files
my $path = path('.');
my $tmp = $path->child('tmp');
$tmp->mkpath;
$tmp->child('LCDd.conf')->spew(@lines);

###########################
#
# Step 3: parse LCDd.conf (INI file)

# Problem: comments must also be retrieved and associated with INI
# class and parameters

# Fortunately, Config::Model::Backend::IniFile can already perform this
# task.

# On the other hand, Config::Model::Backend::IniFile must store its
# values in a configuration tree. A model suitable for LCDd.conf that
# accepts any INI class and any INI parameter must be created

# Dump stack trace in case of error
Config::Model::Exception::Any->Trace(1) ;

# one model to rule them all
my $model = Config::Model->new();

# The model for pre-precessed LCDd.conf must be made of 2 classes:
# - the main config class that contains INI class names (named Dummy here)
# - the child class that contains data from a elements of the INI
#   classes (named Dummy::Class)

# For techinical reason, the lower class (Dummy::Class) must be
# created first.

# The class is used to store any parameter found in an INI class
$model->create_config_class(
    name   => 'Dummy::Class',
    accept => [
        'Hello|GoodBye|key' => {
            type => 'list',
            cargo => { qw/type  leaf value_type uniline/}
        },
        '.*' => {
            type => 'leaf',
            value_type => 'uniline'
        }
    ],
);

# This class contains any INI class, and use Dummy::Class to hold parameters.
$model->create_config_class(
    name   => 'Dummy',
    accept => [
        '.*' => {
            type => 'node',
            config_class_name => 'Dummy::Class'
        }
    ],
    rw_config => {
        backend => 'IniFile',
        config_dir => 'tmp', # created above
        file => 'LCDd.conf'
    }
);

# Now the dummy configuration class is created. Let's create a
# configuration tree to store the data from LCDd.conf

my $dummy = $model->instance(
    instance_name   => 'dummy',
    root_class_name => 'Dummy',
)-> config_root;

##############################################
#
# Step 4: Mine the LCDd.conf information and create a model
#

# Create a meta tree that will contain LCDd model
my $meta_root = $model->instance(
    root_class_name => 'Itself::Model',
    instance_name   => 'meta_model',
) -> config_root;

# Create LCDd configuration class and store the first comment from LCDd.conf as
# class description
$meta_root->grab("class:LCDd class_description")->store( $dummy->annotation );

# append my own text
my $extra_description = "Model information was extracted from /etc/LCDd.conf";
$meta_root->load(qq!class:LCDd class_description.="\n\n$extra_description"!);

# add legal stuff
$meta_root->load( qq!
    class:LCDd
        copyright:0="2011-2017, Dominique Dumont"
        copyright:1="1999-2017, William Ferrell and others"
        license="GPL-2"
!
);

# add INI backend (So LCDd model will be able to read INI files)
$meta_root->load( qq!
    class:LCDd
        rw_config
            backend=ini_file
            config_dir="/etc"
            file="LCDd.conf"
            quote_value=shell_style
!
);

# Note: all the load calls above could be done in one call. They are
# split in several class to clarify what's going on.

# Now, let's use the information retrieved by /etc/LCDd.conf
# and stored in Dummy tree.
# @ini_classes array contains all INI classes found in LCDd.conf,
# make sure to put server in first, and sort the rest
my @ini_classes = sort grep { $_ ne 'server'} $dummy->get_element_name;
unshift @ini_classes, 'server' ;

# Now before actually mining LCDd.conf information, we must prepare
# subs to handle them. This is done using a dispatch table.
my %dispatch;

# first create the default case which will be used for most parameters
# This subs is passed: the INI class name, the INI parameter name
# the comment attached to the parameter, the INI value, and an optional
# value type
$dispatch{_default_} = sub {
    my ( $ini_class, $ini_param, $info_r, $ini_v, $value_type ) = @_;

    # prepare a string to create the ini_class model
    my $load = qq!class:"$ini_class" element:$ini_param type=leaf !;
    $value_type ||= 'uniline';

    # get semantic information from comment (written between square brackets)
    my $square_model = '';

    my $square_rexp = '\[(\s*\w+\s*:[^\]]*)\]';
    if ($$info_r =~ /$square_rexp/s) {
        my $info = $1 ;
        say "class $ini_class element $ini_param info: '$info'" if $verbose;
        $$info_r =~ s/$square_rexp//gs; # remove all remaining square_rexp
        $square_model .= ' '. info_to_model($info,$value_type, $info_r) ;
    }

    unless ($square_model) {
        # or use the value found in INI file as default
        $ini_v =~ s/^"//g;
        $ini_v =~ s/"$//g;
        $square_model .= qq! value_type=$value_type!;
        $square_model .= qq! default="$ini_v"! if length($ini_v);
    }

    # get model information from comment (written between curly brackets)
    my $curly_model = '';
    my $curly_rexp = '{%(\s*\w+.*?)%}' ;
    while ($$info_r =~ /$curly_rexp/s) {
        $curly_model = $1 ;
        say "class $ini_class element $ini_param model snippet: '$curly_model'"
            if $verbose;
        $$info_r =~ s/$curly_rexp//s;
    }

    # return a string containing model specifications
    # spec in curly model may override spec in square model
    return $load . $square_model . $curly_model ;
};

# Now let's take care of the special cases. This one deals with "Driver"
# parameter found in INI [server] class
$dispatch{"LCDd::server"}{Driver} = sub {
    my ( $class, $elt, $info_r, $ini_v ) = @_;
    my $load = qq!class:"$class" element:$elt type=check_list !;
    my @drivers = split /\W+/, $$info_r;
    while ( @drivers and ( shift @drivers ) !~ /supported/ ) { }
    $load .= 'choice=' . join( ',', @drivers ) . ' ';

    #say $load; exit;
    return $load;
};

# Ensure that DriverPath ends with a slash by adding a match clause
$dispatch{"LCDd::server"}{DriverPath} = sub {
    return $dispatch{_default_}->( @_ ) . q! match="/$"! ;
};

# like default but ensure that the parameter is integer
$dispatch{"LCDd::server"}{WaitTime}
    = $dispatch{"LCDd::server"}{ReportLevel}
    = $dispatch{"LCDd::picolcd"}{LircFlushThreshold}
    = $dispatch{"LCDd::server"}{Port}
    = sub {
        my ( $class, $elt, $info_r, $ini_v ) = @_;
        return $dispatch{_default_}->( @_, 'integer' );
    };

# special dispatch case
my %override ;

# Handle display content
$override{"LCDd::server"}{GoodBye}
    = $override{"LCDd::server"}{Hello}
    = $override{"LCDd::linux_input"}{key}
    = sub {
        my ( $class, $elt ) = @_;
        my $ret = qq( class:"$class" element:$elt type=list ) ;
        $ret .= 'cargo type=leaf value_type=uniline';
        return $ret ;
    };

# Now really mine LCDd.conf information using Dummy tree

# loop over all INI classes
foreach my $ini_class (@ini_classes) {
    say "Handling INI class $ini_class" if $verbose;
    my $ini_obj = $dummy->grab($ini_class);
    my $config_class   = "LCDd::$ini_class";

    # create config class in case there's no parameter in INI file
    $meta_root->load(qq!class:"LCDd::$ini_class" class_description="generated from LCDd.conf"!);

    # loop over all INI parameters and create LCDd::$ini_class elements
    foreach my $ini_param ( $ini_obj->get_element_name ) {
        my ($model_spec) ;

        # test for override
        if (my $sub = $override{$config_class}{$ini_param}) {
            # runs the override sub to get the model string
            $model_spec = $sub->($config_class, $ini_param) ;
        }
        else {
            # retrieve the correct sub from the orveride or dispatch table
            my $sub = $dispatch{$config_class}{$ini_param} || $dispatch{_default_};

            # retrieve INI value
            my $ini_v    = $ini_obj->grab_value($ini_param);

            # retrieve INI comment attached to $ini_param
            my $ini_comment = $ini_obj->grab($ini_param)->annotation;

            # runs the sub to get the model string
            $model_spec = $sub->($config_class, $ini_param, \$ini_comment, $ini_v) ;

            # escape embedded quotes
            $ini_comment =~ s/"/\\"/g;
            $ini_comment =~ s/\n*$//;
            $model_spec .= qq! description="$ini_comment"! if length($ini_comment);
        }

        # show the model without the doc (too verbose)
        say "load -> $model_spec" if $show_model ;

        # load class specification in model
        $meta_root->load($model_spec);
    }

    # Now create a an $ini_class element in LCDd class (to link LCDd
    # class and LCDd::$ini_class)
    my $driver_class_spec = qq!
        class:LCDd
            element:$ini_class
    ! ;

    if ( $ini_class eq 'server' or $ini_class eq 'menu' ) {
        $driver_class_spec .= qq!
            type=node
            config_class_name="LCDd::$ini_class"
        ! ;
    }
    else {
        # Arrange a driver class is shown only if the driver was selected
        # in the [server] class
        $driver_class_spec .= qq!
            type=warped_node
            config_class_name="LCDd::$ini_class"
            level=hidden
            warp
              follow:selected="- server Driver"
              rules:"\$selected.is_set('$ini_class')"
                level=normal
        !;
    }
    $meta_root->load($driver_class_spec);
}

######################
#
# Step 5: write the model


# Itself constructor returns an object to read or write the data
# structure containing the model to be edited. force_write is required
# because writer object, being created *after* loading the model in the
# instance, is not aware of these changes.
my $rw_obj = Config::Model::Itself->new(
    model_object => $meta_root,
    cm_lib_dir => 'lib/Config/Model/',
    force_write => 1,
);

say "Writing all models in file (please wait)";
$rw_obj->write_all;

# mop up
$tmp->remove_tree;

say "Done";

# this function extracts info specified between square brackets and returns a model snippet
sub info_to_model {
    my ($info,$value_type, $info_r) = @_ ;

    $info =~ s/\s+//g;
    my @model ;

    # legal needs to be parsed first to setup value_type first
    my %info = map { split /[:=]/,$_ ,2 ; } split /;/,$info ;

    # use this semantic information to better specify the parameter
    if (my $legal = delete $info{legal} || '') {
        if ( $legal =~ /^([\d.]*)-([\d.]*)$/ or $legal =~ /^>([\d.]+)$/ ) {
            my $bounds = '';
            $bounds.= "min=$1 " if defined $1 and length($1);
            $bounds.= "max=$2 " if defined $2 and length($2);
            my $vt = "value_type=";
            $vt .= $bounds =~ m/\./ ? 'number ' : 'integer ';
            push @model, $vt.$bounds;
        }
        elsif ($legal =~ /^(on,off|off,on)$/ ) {
            push @model, "value_type=boolean write_as=off,on"
        }
        elsif ($legal =~ /^(yes,no|no,yes)$/ ) {
            push @model, "value_type=boolean write_as=no,yes"
        }
        elsif ($legal =~ /^([\w\,]+)$/       ) {
            push @model, "value_type=enum choice=$1"
        }
        else{
            # push back $legal info if no model snippet could be extracted
            say "note: unhandled legal  spec: '$legal'. Sending it back to doc";
            push @model, "value_type=$value_type ";
            $$info_r .= "legal: $legal "
        }
    }
    else {
        push @model, "value_type=$value_type ";
    } ;

    foreach my $k (keys %info) {
        my $v = $info{$k} ;
        die "Undefined value. Something is wrong in info '$info'" unless defined $v ;
        $v = '"'.$v.'"' unless $v=~/^"/ ;

        if ($k =~ /default/ ) {
            # specify upstream default value if it was found in the comment
            push @model ,qq!upstream_default=$v! if length($v);
        }
        elsif ($k =~ /assert/ ) {
            push @model ,qq!warn_unless:0 code=$v -!;
        }
        else {
            push @model, "$k=$v" ;
        }
    }

    return join(' ',@model) ;
}