Turning a set of parent-child relationships into a hierarchical structure - perl

I have an LDAP directory that I'm querying using Net::LDAP. This gives me a set of parent-child relationships.
It's a directory of people - and includes a 'manager' DN (which is another field within the directory).
I'm having real trouble turning this manager->person set of records into a hierarchical structure.
What I've got so far is:
#!/usr/bin/env perl
use strict;
use warnings;
use Net::LDAP;
use Data::Dumper;
my %people;
my $ldap = Net::LDAP->new('my_ldap_server');
my $result = $ldap->bind('bind_dn');
die if $result->code;
my $search = $ldap->search(
base => 'ou=yaddayadda',
scope => 'subtree',
filter => 'objectClass=person',
attrs => ['manager'],
);
foreach my $found ( $search->entries ) {
my $mgr = $found->get_value('manager');
my $dn = $result->dn;
push( #{ $people{$mgr} }, $dn );
}
What this gives me is a hash of managers and the people who work for them (using DN, which is unique).
An entry from %people looks like:
$VAR1 = {
'cn=Firstname Lastname,ou=OrgUnit' => [
'cn=Personame Lastname,ou=OrgUnit',
'cn=AnotherPerson NameHere,ou=OrgUnit',
],
'cn=AnotherPerson NameHere,ou=OrgUnit' => [
'cn=Someone Else,ou=OrgUnit',
]
};
But I'm having trouble with turning that parent-child mapping into a hierarchical structure.
e.g.:
'ceo' => [
'pa' => [],
'head_of_dept' => [
'person' => [],
'person_with_staff' => [ 'person3', 'person4' ]
]
]
I'm at something of a loss for how to accomplish this. It seems it shouldn't be too hard to do, given that each person is unique within the organisation structure.
NB - in the above, I've got cn=AnotherPerson NameHere,ou=OrgUnit who has a subordinate, and I'm after making a nested mapping out of this:
e.g.:
$VAR1 = {
'cn=Firstname Lastname,ou=OrgUnit' => [
'cn=Personame Lastname,ou=OrgUnit',
'cn=AnotherPerson NameHere,ou=OrgUnit',
[
'cn=Someone Else,ou=OrgUnit'
]
]
};

What you need is a directed graph, and I suggest using the Graph::Directed module, whose methods are documented in Graph
This program will build the graph for you, but without any data I couldn't test it beyond making sure it compiles
use strict;
use warnings 'all';
use feature 'say';
use Net::LDAP;
use Graph::Directed;
use Data::Dumper;
my $ldap = Net::LDAP->new('my_ldap_server');
my $result = $ldap->bind('bind_dn');
die if $result->code;
my $search = $ldap->search(
base => 'ou=yaddayadda',
scope => 'subtree',
filter => 'objectClass=person',
attrs => ['manager'],
);
my $g = Graph::Directed->new;
for my $found ( $search->entries ) {
my $mgr = $found->get_value('manager');
my $dn = $result->dn;
$g->add_edge($mgr, $dn);
}
say $g;
The resulting Graph::Directed object has a stringification overload so you can examine it superficially by simply printing it, but when you want to interrogate the structure further you will need to know some of the terms of graph theory. For instance, $g->source_vertices will return a list of all nodes that have descendants but no parents—in this case, a list of senior management, or $g->is_cyclic will return true if your data has any loops anywhere
Here's an example of a program that uses your brief sample data to display a hierarchical tree of nodes
use strict;
use warnings 'all';
use Graph::Directed;
my $data = {
'cn=Firstname Lastname,ou=OrgUnit' => [
'cn=Personame Lastname,ou=OrgUnit',
'cn=AnotherPerson NameHere,ou=OrgUnit',
],
'cn=AnotherPerson NameHere,ou=OrgUnit' =>
[ 'cn=Someone Else,ou=OrgUnit', ]
};
my $g = Graph::Directed->new;
for my $mgr ( keys %$data ) {
$g->add_edge($mgr, $_) for #{ $data->{$mgr} };
}
dump_tree($_) for $g->source_vertices;
sub dump_tree {
my ($node, $level) = ( #_, 0);
print ' ' x $level, $node, "\n";
dump_tree($_, $level+1) for $g->successors($node);
}
output
cn=Firstname Lastname,ou=OrgUnit
cn=AnotherPerson NameHere,ou=OrgUnit
cn=Someone Else,ou=OrgUnit
cn=Personame Lastname,ou=OrgUnit

#Hunter McMillen unfortunately deleted his very good but slightly off answer. Here is my attempt to augment his code by turning the relationship from underling -> boss towards boss -> underlings.
To simulate the LDAP responses, I created a simple Moose class.
package Person;
use Moose;
has name => ( is => 'ro' );
has boss => ( is => 'ro', predicate => 'has_boss' );
package main;
use strict;
use warnings;
use Data::Printer;
# make a randomized list of people
my %people = map { $_->name => $_ }
map {
Person->new(
name => $_->[0], ( $_->[1] ? ( boss => $_->[1] ) : () )
)
} (
[qw( ceo )], [qw( head_of_dept ceo)],
[qw( person head_of_dept)], [qw( person_with_staff head_of_dept )],
[qw( person3 person_with_staff )], [qw( person4 person_with_staff )],
);
my %manages;
foreach my $p (values %people) {
push #{ $manages{ $p->boss } }, $p->name if $p->has_boss;
}
# this part shamelessly stolen from #HunterMcMillen's deleted answer
sub build_tree {
my ($person) = #_;
my #subtrees;
foreach my $managee ( #{ $manages{$person} } ) {
push #subtrees, build_tree($managee);
}
return { $person => \#subtrees };
}
p build_tree 'ceo';
Here's the output.
\ {
ceo [
[0] {
head_of_dept [
[0] {
person []
},
[1] {
person_with_staff [
[0] {
person4 []
},
[1] {
person3 []
}
]
}
]
}
]
}
This should be more or less what you want.

Related

How to print an element from an array which is inside the hash in perl

I'm trying to print the outputs from an API which are in multidimensional format.
use strict;
use warnings;
use Data::Dumper;
my $content={
'school_set' => 'SSET1234',
'result' => [
{
'school_name' => 'school_abc',
'display_value' => 'IL25',
'school_link' => 'example.com',
'status' => 'registerd',
'status_message' => 'only arts',
'school_id' => '58c388d40596191f',
}
],
'school_table' => 'arts_schools'
};
print "school_name is=".$content{result}[0]{school_name};
print "school_status is=".$content{result}[3]{status};
output
Global symbol "%content" requires explicit package name (did you forget to declare "my %content"?) at test8.pl line 20.
Global symbol "%content" requires explicit package name (did you forget to declare "my %content"?) at test8.pl line 21.
I have to print the outputs like below from the result.
school_name = school_abc
school_status = registered
If $content is a hash reference, you need to dereference it first. Use the arrow operator for that:
$content->{result}[0]{school_name}
The syntax without the arrow is only possible for %content.
my %content = ( result => [ { school_name => 'abc' } ] );
print $content{result}[0]{school_name};
If you want to print all the results, you have to loop over the array somehow. For example
#!/usr/bin/perl
use warnings;
use strict;
my $content = {
'result' => [
{
'school_name' => 'school_abc',
'status' => 'registerd',
},
{
'school_name' => 'school_def',
'status' => 'pending',
}
],
};
for my $school (#{ $content->{result} }) {
print "school_name is $school->{school_name}, status is $school->{status}\n";
}
Your data structure assumes an array, perhaps it would be useful to utilize loop output for the data of interest.
The data presented as hash reference and will require de-referencing to loop through an array.
Following code snippet is based on your posted code and demonstrates how desired output can be achieved.
use strict;
use warnings;
use feature 'say';
my $dataset = {
'school_set' => 'SSET1234',
'result' => [
{
'school_name' => 'school_abc',
'display_value' => 'IL25',
'school_link' => 'example.com',
'status' => 'registerd',
'status_message' => 'only arts',
'school_id' => '58c388d40596191f',
}
],
'school_table' => 'arts_schools'
};
for my $item ( #{$dataset->{result}} ) {
say "school_name is = $item->{school_name}\n"
. "school_status is = $item->{status}";
}
exit 0;
Output
school_name is = school_abc
school_status is = registerd

Perl - Sort multidimensional array

I have a variable that looks like this:
do {
my $a = {
computers => [
{
report_date_epoch => 1591107993595,
serial_number => "C02YK1TAFVCF",
username => "fake1\#example.com",
},
{
report_date_epoch => 1626877069476,
serial_number => "C03XF8AWJG5H",
username => "fake2\#example.com",
},
...
And I'd like to sort it by the epoch number into a new variable without the computers array.
The list of hashrefs in the arrayref for computer key sorted
use warnings;
use strict;
use feature 'say';
use Data::Dump qw(dd);
my $v = {
computers => [
{
report_date_epoch => 1591107993595,
serial_number => "C02YK1TAFVCF",
username => "fake1\#example.com",
},
{
report_date_epoch => 1626877069476,
serial_number => "C03XF8AWJG5H",
username => "fake2\#example.com",
}
]
};
my #by_epoch =
sort { $a->{report_date_epoch} <=> $b->{report_date_epoch} }
#{$v->{computers}};
dd $_ for #by_epoch;
I use Data::Dump to print complex data structures. (There are other good ones as well.)
Or use a core (installed) tool
use Data::Dumper;
...
say Dumper $_ for #by_epoch;
Sort::Key is so much cleaner than the builtin sort. Not much difference in this instance, but it only gets better as the task becomes more complex.
use Sort::Key qw( ikeysort );
my #by_report_date =
ikeysort { $_->{report_date_epoch} } # Sort them.
#{ $a->{computers} }; # Get the elements of the array of computers.

How to get some values with a loop from Hashes of hashes in Perl

I have a config.file to do some tests and i would like to get some values from this one also.
Here my config.file:
my $folder = 'E:\FOLDER\Test\WEB';
{
license => [ 'kit-licence.zip',
'kit-work.zip'
],
programs => [
#template society =>\%program_work
'VIKTOR DESCRIPTION PRODUCT' => {
name => 'VIKTOR ',
parameters => [
Count_id => '06 (Viktor)',
Birth_date => '1995-04-30',
Marriage_date => '2014-05-26',
Divorce_date => '2015-03-30',
Activities_folder => $folder.'\VIKTOR\independent worker',
Activities_format => 'Enterprise Format (V35)',
Description_File_from => $folder.'\VIKTOR\FILE\description.xlm',
]
},
'OLIVER NEW OBJECT' => {
name => 'OLIVER ',
parameters => [
Count_id => '06 (oliver)',
Birth_date => '1990-04-30',
Marriage_date => '2011-03-26',
Divorce_date => '2014-01-30',
Activities_folder => $folder.'\OLIVER\independent worker',
Activities_format => 'Enterprise Format (V35)',
Description_File_from => $folder.'\OLIVER\FILE\description.xlm',
]
},
]
};
My file test is following:
#test.pl
use Modern::Perl;
my $config = do 'work.conf';
use Data::Dumper;
say Dumper( $config );
To get parameters in Programs for Viktor for example, I can do this:
%programs = #{ $config->{programs} };
for my $prog (values %programs) {
my %param = #{ $prog->{parameters} };
for my $name (sort keys %param){
print $name, ': ', $param{$name},"\n";
}
}
But in my case, I want to be able to get parameters for every user. Here it's just for Viktor. I would like to get them for Oliver or for another user. For that, and to differentiate all users, I have to use the "template society" which is the name to differentiate every user. For example, for Viktor, it's: "VIKTOR DESCRIPTION PRODUCT". For Oliver: "OLIVER NEW OBJECT".
How can I do that?
Same thing for "License":
license => [ 'kit-licence.zip',
'kit-work.zip'
],
programs => [..
I would like to get the license by name of each one. For example, 'kit-license.zip'.
And not by "hard coding" like that:
use File::Spec::Functions qw/catfile/;
my $filename = catfile($::svn, ${$config->{license}}[0]);
my $filename1 = catfile($::svn, ${$config->{license}}[1]);
Perhaps in a loop, but I didn't find.
PS: Don't ask me why they are all divorced. I really don't know.
You're already doing a good job converting those array refs into hashes. But the values is making your life hard. You need the key and the value at the same time. You can use each to do that.
my %programs = #{ $config->{programs} };
while (my ($template_society, $value) = each %programs ) {
my %param = #{ $value->{parameters} };
print "$template_society\n";
for my $name ( sort keys %param ) {
print "\t", $name, ': ', $param{$name}, "\n";
}
}
This will produce the following output:
VIKTOR DESCRIPTION PRODUCT
Activities_folder: \VIKTOR\independent worker
Activities_format: Enterprise Format (V35)
Birth_date: 1995-04-30
Count_id: 06 (Viktor)
Description_File_from: \VIKTOR\FILE\description.xlm
Divorce_date: 2015-03-30
Marriage_date: 2014-05-26
OLIVER NEW OBJECT
Activities_folder: \OLIVER\independent worker
Activities_format: Enterprise Format (V35)
Birth_date: 1990-04-30
Count_id: 06 (oliver)
Description_File_from: \OLIVER\FILE\description.xlm
Divorce_date: 2014-01-30
Marriage_date: 2011-03-26
The each built-in returns both the key and the value of a hash per iteration, and undef once it's done. That's why you need to put it in a while loop.
If you don't like the each approach, you can also use keys instead of values to get the keys ($template_society) and use that to look up the appropriate value.
my %programs = #{ $config->{programs} };
foreach my $template_society (keys %programs ) {
my %param = #{ $programs{$template_society}->{parameters} };
print "$template_society\n";
for my $name ( sort keys %param ) {
print "\t", $name, ': ', $param{$name}, "\n";
}
}
This will give you the same output.
To get all your licence paths you need to store them in an array and use a loop to process your array ref into that array. The easiest and most concise way to do that is using map.
my #licences = map { catfile($::svn, $_ ) } #{ $config->{license} };
It's like a foreach loop, just shorter. The BLOCK is basically a function that gets the current iteration item in $_. It's essentially the same as the following, just more perlish.
my #licences;
foreach my $licence (#{ $config->{license} } ) {
push #licences, catfile($::svn, $licence );
}
Do not attempt to create variables like $foo1, $foo2 and so on dynamically. That will not work. See this1 for an explanation why.
Finally a word on $::svn: if you are in a package, you should put your code into a function and accept $svn as an argument. Working with globals or package variables from different packages is tricky and messy and you will at some point shoot yourself in the foot with it.
1: The normal document is currently broken, so I used archive.org to get it

executing a function within an array within a hash in perl

I have a Perl data structurte like so
%myhash = (
k1 => v1,
kArray => [
{
name => "anonymous hash",
...
},
\&funcThatReturnsHash,
{
name => "another anonymous hash",
...
}
]
);
Elsewhere I iterate through the list in kArray which contains a bunch of hashes. I would like to either process the actual hash OR the hash returned by the function.
foreach my $elem( #{myhash{kArray}} ) {
if (ref($elem) == "CODE") {
%thisHash = &$elem;
}
else {
%thisHash = %$elem;
}
...
}
However ref ($elem) is always scalar or undefined. I tried func, &func, \&func, \%{&func}, in %myhash to no effect.
how do I extract the hash within the function in the main body?
Apart from the code sample you give being invalid Perl, the main problems seem to be that you are using == to compare strings instead of eq, and you are assigning a hash reference to a hash variable %thishash. I assure you that ref $elem never returns SCALAR with the data you show
It would help you enormously if you followed the common advice to use strict and use warnings at the top of your code
This will work for you
for my $elem ( #{ $myhash{kArray} } ) {
my $this_hash;
if ( ref $elem eq 'CODE' ) {
$this_hash = $elem->();
}
else {
$this_hash = $elem;
}
# Do stuff with $this_hash
}
or you could just use a map like this
use strict;
use warnings;
use 5.010;
use Data::Dump;
my %myhash = (
k1 => v1,
kArray => [
{
name => "anonymous hash",
},
\&funcThatReturnsHash,
{
name => "another anonymous hash",
}
]
);
for my $hash ( map { ref eq 'CODE' ? $_->() : $_ } #{ $myhash{kArray} } ) {
say $hash->{name};
}
sub funcThatReturnsHash {
{ name => 'a third anonymous hash' };
}
output
anonymous hash
a third anonymous hash
another anonymous hash
If you turn on strict and warnings, you'll see that:
foreach my $elem(#{mynahs{kArray}}) {
Isn't valid. You need at the very least a $ before mynahs.
But given something like this - your approach works - here's an example using map to 'run' the code references:
#!/usr/bin/perl
use strict;
use warnings;
use Data::Dumper;
sub gimme_hash {
return { 'fish' => 'paste' };
}
my $stuff =
[ { 'anon1' => 'value' },
\&gimme_hash,
{ 'anon2' => 'anothervalue' }, ];
my $newstuff = [ map { ref $_ eq "CODE" ? $_->() : $_ } #$stuff ];
print Dumper $newstuff;
Turns that hash into:
$VAR1 = [
{
'anon1' => 'value'
},
{
'fish' => 'paste'
},
{
'anon2' => 'anothervalue'
}
];
But your approach does work:
foreach my $element ( #$stuff ) {
my %myhash;
if ( ref $element eq "CODE" ) {
%myhash = %{$element -> ()};
}
else {
%myhash = %$element;
}
print Dumper \%myhash;
}
Gives:
$VAR1 = {
'anon1' => 'value'
};
$VAR1 = {
'fish' => 'paste'
};
$VAR1 = {
'anon2' => 'anothervalue'
};

List of paths into hash array tree in Perl

I got an array of paths
C:\A
C:\B\C
D:\AB
and I'd like to have these in a hash array tree so I can go through them in a TT2 template.
What I mean is like this:
#dirs = [
{
name => "C:",
subs => [
{
name => "A",
subs => [],
},
{
name => "B",
subs => [
{
name => "C",
subs => [],
}
],
}
]
},
{
name => "D:",
subs => [
{
name => "AB",
subs => [],
}
],
}
]
I also know that I'm probably doing brainderp here so I'm open to other approaches, only requirement is turning that list of paths into something you can rebuild as a tree with the TT2 Template Toolkit.
Also what's that structure called? I just thought of hash array tree but I bet that's wrong.
Here's a very short approach. Note that this can only be so simple because I changed your data format to a hash of hashes which perfectly matches your tree structure. See the code below to transform the resulting structure to yours.
my $tree = {root => {}};
foreach my $input (<DATA>) { chomp $input;
my $t = $tree;
$t = $t->{$_} //= {} for split /\\/ => $input;
}
use Data::Dumper; print Dumper $tree;
__DATA__
C:\A
C:\B\C
D:\AB
C:\B\A
C:\B\A\C
Output:
$VAR1 = {
'C:' => {
'A' => {},
'B' => {
'A' => {
'C' => {}
},
'C' => {}
}
},
'D:' => {
'AB' => {}
}
};
To transform this data structure into yours, simply use this code:
sub transform {
my $tree = shift;
my #children = ();
while (my ($name, $children) = each %$tree) {
push #children, {
name => $name,
subs => [ transform($children) ],
}
}
return #children;
}
my $AoH_tree = {name => 'root', subs => [transform($tree)] };
Done. :) For a completely different approach with much more sugar, power and readability, but much more LOC, see my other answer.
This is a longer but much more readable and more comfortable solution. You don't have to (and probably don't want to) use this, but maybe it can help (not only you) to learn more about different approaches. It introduces a small Moo class for tree nodes which can add names recursively to itself with readable sorting and stringification methods.
Edit: for a completely different and extremely short alternative, see my other answer. I divided it up in two answers because they are completely different approaches and because this answer is already long enough. ;)
Tree class
Note this is basically no more than your nested AoHoAoH... structure - with a litte bit sugar added. ;)
# define a tree structure
package Tree;
use Moo; # activates strict && warnings
use List::Util 'first';
# name of this node
has name => (is => 'ro');
# array ref of children
has subs => (is => 'rw', isa => sub { die unless ref shift eq 'ARRAY' });
Now after the basic preparations (our objects have one scalar name and one array ref subs) we come to the main part of this answer: the recursive add_deeply method. Note that from here everything reflects the recursive nature of your data structure:
# recursively add to this tree
sub add_deeply {
my ($self, #names) = #_;
my $next_name = shift #names;
# names empty: do nothing
return unless defined $next_name;
# find or create a matching tree
my $subtree = first {$_->name eq $next_name} #{$self->subs};
push #{$self->subs}, $subtree = Tree->new(name => $next_name, subs => [])
unless defined $subtree;
# recurse
$subtree->add_deeply(#names);
}
The following two methods are not that important. Basically they are here to make the output pretty:
# sort using node names
sub sort {
my $self = shift;
$_->sort for #{$self->subs}; # sort my children
$self->subs([ sort {$a->name cmp $b->name} #{$self->subs} ]); # sort me
}
# stringification
use overload '""' => \&to_string;
sub to_string {
my $self = shift;
my $prefix = shift // '';
# prepare
my $str = $prefix . '{TREE name: "' . $self->name . '"';
# stringify children
if (#{$self->subs}) {
$str .= ", children: [\n";
$str .= $_->to_string(" $prefix") for #{$self->subs};
$str .= "$prefix]";
}
# done
return $str . "}\n";
}
How to use this
Now comes the simple part. Just read the input (from __DATA__ here) and add_deeply:
# done with the tree structure: now use it
package main;
# parse and add names to a tree
my $tree = Tree->new(name => 'root', subs => []);
foreach my $line (<DATA>) {
chomp $line;
$tree->add_deeply(split /\\/ => $line);
}
# output
$tree->sort;
print $tree;
__DATA__
C:\A
C:\B\C
D:\AB
C:\B\A
C:\B\A\C
Output:
{TREE name: "root", children: [
{TREE name: "C:", children: [
{TREE name: "A"}
{TREE name: "B", children: [
{TREE name: "A", children: [
{TREE name: "C"}
]}
{TREE name: "C"}
]}
]}
{TREE name: "D:", children: [
{TREE name: "AB"}
]}
]}
I did one with a complex hash structure keeping track of already placed nodes, and then I did this one. More steps, but somewhat leaner code.
while ( <> ) {
chomp;
my $ref = \#dirs;
foreach my $dir ( split /\\/ ) {
my $i = 0;
$i++ while ( $ref->[$i] and $ref->[$i]{name} ne $dir );
my $r = $ref->[$i] ||= { name => $dir, subs => [] };
$ref = $r->{subs};
}
}