picatrix 0.5.0 → 0.5.5
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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +17 -37
- data/Rakefile +35 -0
- data/bin/console +8 -4
- data/bin/post-receive.sh +3 -0
- data/bin/setup +7 -1
- data/generated/zoo.dot +37 -0
- data/generated/zoo.dot.png +0 -0
- data/{lib/picatrix/generated/view_submissions.rb → generated/zoo/resources/animals.rb} +10 -13
- data/generated/zoo/support/mason.rb +114 -0
- data/generated/zoo/support/namespaces.json +7 -0
- data/generated/zoo/support/picatrix.mason.pb.rb +95 -0
- data/generated/zoo/support/zoo.pb.rb +76 -0
- data/generated/zoo/support/zoo.proto +65 -0
- data/generated/zoo/zoo.rb +116 -0
- data/lib/picatrix.rb +25 -10
- data/lib/picatrix/buffy.rb +38 -40
- data/lib/picatrix/cruddy.rb +18 -16
- data/lib/picatrix/jenny.rb +31 -5
- data/lib/picatrix/link_relation_index.rb +1 -1
- data/lib/picatrix/pacman.rb +12 -6
- data/lib/picatrix/protoc.rb +7 -0
- data/lib/picatrix/prototyper.rb +56 -0
- data/lib/picatrix/routes.rb +40 -11
- data/{definitions/mason.proto → lib/picatrix/support/picatrix.mason.proto} +16 -13
- data/lib/picatrix/templates/app.proto +42 -0
- data/lib/picatrix/templates/app.rb +24 -136
- data/lib/picatrix/templates/mason.rb +114 -0
- data/lib/picatrix/templates/namespaces.json +7 -0
- data/lib/picatrix/templates/type.rb +6 -3
- data/lib/picatrix/transform_to_hash.rb +2 -2
- data/lib/picatrix/version.rb +1 -1
- data/notes.md +34 -0
- metadata +20 -10
- data/definitions/app.proto +0 -47
- data/lib/picatrix/fixtures/view_submission.rb +0 -19
- data/lib/picatrix/generated/app.pb.rb +0 -79
- data/lib/picatrix/generated/app.rb +0 -209
- data/lib/picatrix/generated/mason.pb.rb +0 -90
- data/lib/picatrix/generated/mason/namespaces.json +0 -7
data/lib/picatrix/jenny.rb
CHANGED
@@ -26,30 +26,56 @@ module Picatrix
|
|
26
26
|
@meta = {}
|
27
27
|
|
28
28
|
if File.exist?(
|
29
|
-
|
29
|
+
namespaces_path = "generated/#{app_name}/support/namespaces.json"
|
30
30
|
) #TODO this should be coming in from graph instead
|
31
31
|
@namespaces = JSON.parse(
|
32
32
|
File.read(
|
33
|
-
|
33
|
+
namespaces_path
|
34
34
|
)
|
35
35
|
)
|
36
36
|
@prefix = "#{@namespaces["@namespaces"].keys.first}:"
|
37
37
|
else
|
38
|
-
puts "no generated/
|
38
|
+
puts "no generated/#{app_name}/support/namespaces.json file"
|
39
39
|
@namespaces = {}
|
40
40
|
@prefix = ""
|
41
41
|
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def just_do_it
|
45
|
+
setup_framework_for(app_name)
|
46
|
+
generate_sinatra_of(app_name)
|
47
|
+
end
|
48
|
+
|
49
|
+
def setup_framework_for(app_name)
|
50
|
+
template(
|
51
|
+
'templates/mason.rb',
|
52
|
+
File.join("../../generated/#{app_name}/support/mason.rb")
|
53
|
+
)
|
54
|
+
|
55
|
+
Protoc.to_ruby(app_name)
|
56
|
+
|
57
|
+
begin
|
58
|
+
jenny_path = File.dirname( __FILE__ )
|
59
|
+
$:.unshift File.expand_path( File.join( jenny_path, "..", "..", "generated" ) )
|
60
|
+
$:.unshift File.expand_path( File.join( jenny_path, "..", "..", "generated", "#{app_name}" ) )
|
61
|
+
$:.unshift File.expand_path( File.join( jenny_path, "..", "..", "generated", "#{app_name}", "support" ) )
|
62
|
+
|
63
|
+
require_all("./generated/#{app_name}/**/*.rb")
|
64
|
+
rescue LoadError => e
|
65
|
+
puts "#{e}"
|
66
|
+
end
|
42
67
|
|
43
68
|
@control_templates = Cruddy.new(@edges).controls
|
69
|
+
@resources = Prototyper.new(@nodes, {}, app_name).resources
|
44
70
|
@routes = Routes.new(@edges, @control_templates)
|
45
71
|
# make this available to the app scope
|
46
72
|
@root_path = @routes.root_path
|
47
73
|
end
|
48
74
|
|
49
|
-
def
|
75
|
+
def generate_sinatra_of(app_name)
|
50
76
|
template(
|
51
77
|
'templates/app.rb',
|
52
|
-
File.join("generated/#{app_name}.rb")
|
78
|
+
File.join("../../generated/#{app_name}/#{app_name}.rb")
|
53
79
|
)
|
54
80
|
end
|
55
81
|
|
@@ -5,7 +5,7 @@ module Picatrix
|
|
5
5
|
@as_hash = {
|
6
6
|
link_relations: edges.flat_map do |e|
|
7
7
|
source = e.first.at(0)
|
8
|
-
link_relation = e.first.at(1)[:link_relation]
|
8
|
+
link_relation = e.first.at(1)[:link_relation]
|
9
9
|
|
10
10
|
{
|
11
11
|
link_relation.to_sym => "#{source} -(#{e.first.at(1)[:method].upcase})-> #{e.first.at(1)[:target]}"
|
data/lib/picatrix/pacman.rb
CHANGED
@@ -2,21 +2,27 @@ module Picatrix
|
|
2
2
|
# eats dots :)
|
3
3
|
# and gives you back nodes and edges
|
4
4
|
class Pacman
|
5
|
-
attr_accessor :hash
|
6
5
|
attr_accessor :nodes, :edges
|
7
6
|
|
8
7
|
def initialize(file_name)
|
9
|
-
|
8
|
+
hash = TransformToHash.new.apply(
|
10
9
|
DigraphParser.new.parse(
|
11
10
|
File.read(file_name)
|
12
11
|
)
|
13
12
|
).flat_map(&:entries).
|
14
13
|
group_by(&:first).
|
15
|
-
map {|k,v| Hash[k, v.map(&:last)]}
|
14
|
+
map {|k,v| Hash[k, v.map(&:last)]}.
|
15
|
+
reduce({}, :merge)
|
16
16
|
|
17
|
-
|
18
|
-
@
|
19
|
-
|
17
|
+
@nodes = hash[:node].reduce({}, :merge)
|
18
|
+
@edges = hash[:edge]
|
19
|
+
end
|
20
|
+
|
21
|
+
def graph_hash
|
22
|
+
{
|
23
|
+
node: nodes,
|
24
|
+
edge: edges
|
25
|
+
}
|
20
26
|
end
|
21
27
|
end
|
22
28
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'protobuf'
|
2
|
+
require 'faker'
|
3
|
+
|
4
|
+
module Picatrix
|
5
|
+
class Prototyper
|
6
|
+
include Thor::Base
|
7
|
+
include Thor::Actions
|
8
|
+
|
9
|
+
attr_accessor :destination_stack, :options,
|
10
|
+
:resources
|
11
|
+
|
12
|
+
source_root File.dirname(__FILE__)
|
13
|
+
|
14
|
+
PROTOBUF_FIXTURES = {
|
15
|
+
Protobuf::Field::Int32Field => "Faker::Number.number(6)",
|
16
|
+
Protobuf::Field::StringField => "Faker::Lorem.word"
|
17
|
+
}
|
18
|
+
|
19
|
+
def initialize(nodes, options = {}, app_name = "app")
|
20
|
+
# thor related
|
21
|
+
@options = options
|
22
|
+
@destination_stack = [self.class.source_root]
|
23
|
+
|
24
|
+
@resources = nodes.keys.select {|n| n if n.pluralize == n}
|
25
|
+
resources.each do |resource|
|
26
|
+
@resource_name = resource.camelize
|
27
|
+
@fields = @resource_name.singularize.
|
28
|
+
constantize.fields.inject({}) do |memo, field|
|
29
|
+
|
30
|
+
field_value =
|
31
|
+
if PROTOBUF_FIXTURES[field.type_class]
|
32
|
+
# TODO this is too general need some way to signal
|
33
|
+
# primary key which is addressable
|
34
|
+
if field.name.to_s.include?("id")
|
35
|
+
"id = #{PROTOBUF_FIXTURES[field.type_class]}"
|
36
|
+
else
|
37
|
+
PROTOBUF_FIXTURES[field.type_class]
|
38
|
+
end
|
39
|
+
elsif field.type_class.to_s.demodulize == "Controls"
|
40
|
+
"#{field.type_class}.new(minimal_controls_for(item_id))"
|
41
|
+
else
|
42
|
+
# TODO recursively gen these relations
|
43
|
+
"[#{field.type_class}.new(params = {})]"
|
44
|
+
end
|
45
|
+
|
46
|
+
memo[field.name] = field_value
|
47
|
+
memo
|
48
|
+
end
|
49
|
+
template(
|
50
|
+
'templates/type.rb',
|
51
|
+
File.join("../../generated/#{app_name}/resources/#{resource}.rb")
|
52
|
+
)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/picatrix/routes.rb
CHANGED
@@ -2,39 +2,68 @@ module Picatrix
|
|
2
2
|
# this class is the data structure you'll construct the Sinatra
|
3
3
|
# routes and their corresponding handling method bodies
|
4
4
|
class Routes
|
5
|
-
attr_accessor :unique_routes, :root_path
|
5
|
+
attr_accessor :unique_routes, :root_path, :controls
|
6
6
|
|
7
7
|
def initialize(edges, control_templates)
|
8
|
+
@controls = {}
|
9
|
+
|
10
|
+
edges.collect do |edge|
|
11
|
+
edge.values.first[:target]
|
12
|
+
end.each do |resource|
|
13
|
+
if (control = control_templates[resource])
|
14
|
+
@controls.merge!(control.to_hash)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
8
18
|
@unique_routes = edges.collect do |edge|
|
9
19
|
source = edge.keys.first
|
10
20
|
target = edge.values.first[:target]
|
11
21
|
properties = edge[source]
|
12
22
|
|
13
|
-
link_relation = properties[:link_relation]
|
23
|
+
link_relation = properties[:link_relation]
|
14
24
|
|
15
25
|
if source == "root"
|
16
26
|
#TODO right now app is using this in place of
|
17
27
|
# figuring out parent collections of collections
|
18
28
|
@root_path = target
|
29
|
+
source = target
|
19
30
|
end
|
20
31
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
32
|
+
path = case properties[:method]
|
33
|
+
when :post
|
34
|
+
source.pluralize
|
35
|
+
when :get
|
36
|
+
if is_collection?(target)
|
37
|
+
"#{source.pluralize}"
|
38
|
+
else
|
39
|
+
"#{source.pluralize}/#{link_relation}"
|
40
|
+
end
|
41
|
+
when :patch
|
42
|
+
"#{source.pluralize}/:item_id"
|
43
|
+
when :put
|
44
|
+
"#{source.pluralize}/:item_id"
|
45
|
+
when :delete
|
46
|
+
"#{source.pluralize}/:item_id"
|
47
|
+
else
|
48
|
+
"#{source.pluralize}/#{link_relation}"
|
49
|
+
end
|
25
50
|
|
26
51
|
{
|
27
|
-
path:
|
52
|
+
path: path,
|
53
|
+
target: target,
|
28
54
|
link_relation: link_relation,
|
29
55
|
method: properties[:method],
|
30
|
-
|
56
|
+
response_resource: {
|
31
57
|
name: target.camelcase.constantize,
|
32
|
-
collection: (target
|
33
|
-
}
|
34
|
-
control_templates: templates
|
58
|
+
collection: is_collection?(target)
|
59
|
+
}
|
35
60
|
}
|
36
61
|
# make sure routes are not duplicated
|
37
62
|
end.compact.uniq {|e| [e[:path], e[:method]]}
|
38
63
|
end
|
64
|
+
|
65
|
+
def is_collection?(target)
|
66
|
+
(target == target.pluralize)
|
67
|
+
end
|
39
68
|
end
|
40
69
|
end
|
@@ -1,5 +1,6 @@
|
|
1
|
-
package mason;
|
1
|
+
package picatrix.mason;
|
2
2
|
|
3
|
+
// TODO this doesnt really make sense as enum, should deprecate
|
3
4
|
enum HTTPVerb {
|
4
5
|
GET = 1;
|
5
6
|
PUT = 2;
|
@@ -18,37 +19,39 @@ message Up {
|
|
18
19
|
optional string title = 2;
|
19
20
|
}
|
20
21
|
|
21
|
-
// Filter should be broken out into custom Picatrix stuff
|
22
|
-
// as it's not really part of the Mason spec
|
23
22
|
message Filter {
|
24
|
-
enum HrefTemplate {
|
25
|
-
TRUE = 0;
|
26
|
-
FALSE = 1;
|
27
|
-
}
|
28
|
-
|
29
23
|
optional string href = 1;
|
30
|
-
|
31
|
-
|
24
|
+
required HTTPVerb method = 2 [default = GET];
|
25
|
+
required bool isHrefTemplate = 3 [default = true];
|
26
|
+
optional string title = 4;
|
27
|
+
|
28
|
+
extensions 10 to 19;
|
32
29
|
}
|
33
30
|
|
34
31
|
message Create {
|
35
32
|
optional string href = 1;
|
36
|
-
|
33
|
+
required HTTPVerb method = 2 [default = POST];
|
37
34
|
optional string title = 3;
|
38
35
|
optional string encoding = 4;
|
36
|
+
|
37
|
+
extensions 10 to 19;
|
39
38
|
}
|
40
39
|
|
41
40
|
message Edit {
|
42
41
|
optional string href = 1;
|
43
|
-
|
42
|
+
required HTTPVerb method = 2 [default = PATCH];
|
44
43
|
optional string title = 3;
|
45
44
|
optional string encoding = 4;
|
45
|
+
|
46
|
+
extensions 10 to 19;
|
46
47
|
}
|
47
48
|
|
48
49
|
message Remove {
|
49
50
|
optional string href = 1;
|
50
|
-
|
51
|
+
required HTTPVerb method = 2 [default = DELETE];
|
51
52
|
optional string title = 3;
|
53
|
+
|
54
|
+
extensions 10 to 19;
|
52
55
|
}
|
53
56
|
|
54
57
|
// when viewing an item directly, you see
|
@@ -0,0 +1,42 @@
|
|
1
|
+
import "picatrix.mason.proto";
|
2
|
+
|
3
|
+
// representation objects
|
4
|
+
<% @representation_klasses.each do |klass| %>
|
5
|
+
message <%= klass %> {
|
6
|
+
optional int32 id = 1;
|
7
|
+
|
8
|
+
message Controls {
|
9
|
+
optional picatrix.mason.Self self = 1;
|
10
|
+
}
|
11
|
+
|
12
|
+
optional Controls controls = 2;
|
13
|
+
<% if klass.pluralize == klass %>
|
14
|
+
repeated <%= klass.singularize.camelcase %> collection = 3;
|
15
|
+
<% end %>
|
16
|
+
}
|
17
|
+
<% end %>
|
18
|
+
|
19
|
+
// generic CRUFD controls to extend
|
20
|
+
<% @item_representations.each do |item| %>// per item representation
|
21
|
+
<% ITEM_ACTIONS.each do |action| %>
|
22
|
+
message <%= item %><%= action %>Template {
|
23
|
+
}
|
24
|
+
extend picatrix.mason.<%= action %> {
|
25
|
+
optional <%= item %><%= action %>Template <%= action.underscore %>_template = 10;
|
26
|
+
}
|
27
|
+
<% end %>
|
28
|
+
<% end %>
|
29
|
+
<% @col_representations.each do |item| %>// per collection representation
|
30
|
+
<% COL_ACTIONS.each do |action| %>
|
31
|
+
message <%= item %><%= action %>Template {
|
32
|
+
}
|
33
|
+
extend picatrix.mason.<%= action %> {
|
34
|
+
optional <%= item %><%= action %>Template <%= action.underscore %>_template = 10;
|
35
|
+
}
|
36
|
+
<% end %>
|
37
|
+
<% end %>
|
38
|
+
// totally custom controls
|
39
|
+
<% @link_relations.each do |link_relation| %>
|
40
|
+
message <%= link_relation %> {
|
41
|
+
}
|
42
|
+
<% end %>
|
@@ -3,9 +3,18 @@ require 'sinatra/json'
|
|
3
3
|
require 'protobuf'
|
4
4
|
require 'require_all'
|
5
5
|
|
6
|
-
|
6
|
+
$:.unshift(File.join(File.dirname(__FILE__)))
|
7
|
+
$:.unshift(File.join(File.dirname(__FILE__), 'support'))
|
8
|
+
$:.unshift(File.join(File.dirname(__FILE__), 'resources'))
|
9
|
+
|
10
|
+
require("./generated/<%= @app_name %>/support/picatrix.mason.pb")
|
11
|
+
require("./generated/<%= @app_name %>/support/mason")
|
12
|
+
require("./generated/<%= @app_name %>/support/<%= @app_name %>.pb")
|
13
|
+
require_all("generated/<%= @app_name %>/resources/*.rb")
|
7
14
|
|
8
15
|
class <%= @app_name.camelize %> < Sinatra::Base
|
16
|
+
include Picatrix::Mason
|
17
|
+
|
9
18
|
set :public_folder => "public", :static => true
|
10
19
|
|
11
20
|
get '/rels' do
|
@@ -23,142 +32,21 @@ class <%= @app_name.camelize %> < Sinatra::Base
|
|
23
32
|
end
|
24
33
|
|
25
34
|
<% @routes.unique_routes.each do |route| %>
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
<% if route[:control_templates][route[:path]][route[:method]] %>
|
33
|
-
controls = send(:<%= route[:control_templates][route[:path]][route[:method]] %>, <%= route[:resource][:name] %>).call("<%= route[:path] %>", params)
|
34
|
-
<% else %>
|
35
|
-
controls = {}
|
36
|
-
<% end %>
|
37
|
-
|
38
|
-
json(
|
39
|
-
{:@controls => controls}.
|
40
|
-
merge!({"<%= route[:resource][:name].to_s.underscore %>" => items})
|
41
|
-
)
|
42
|
-
end
|
43
|
-
<% else %>
|
44
|
-
<%= route[:method] %> "/<%= route[:path] %>/:item_id" do
|
45
|
-
item = <%= route[:resource][:name].to_s.pluralize.constantize %>Resource.<%= send_method_for(route[:method]).to_s %>(params[:item_id])
|
46
|
-
|
47
|
-
<% if route[:control_templates][route[:path]][route[:method]] %>
|
48
|
-
controls = send(:<%= route[:control_templates][route[:path]][route[:method]] %>, <%= route[:resource][:name] %>).call("<%= route[:path] %>", params)
|
49
|
-
<% else %>
|
35
|
+
<%= route[:method] %> "/<%= route[:path] %>" do
|
36
|
+
<% if route[:response_resource][:collection] %>
|
37
|
+
response_body = {"<%= route[:response_resource][:name].to_s.underscore %>" => <%= route[:response_resource][:name] %>Resource.as_hashes}
|
38
|
+
<% else %>
|
39
|
+
response_body = <%= route[:response_resource][:name].to_s.pluralize.constantize %>Resource.<%= send_method_for(route[:method]).to_s %>(params[:item_id]).to_hash.except(:controls)
|
40
|
+
<% end %>
|
50
41
|
controls = {}
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
42
|
+
<% if @routes.controls[route[:target]][route[:method]] %>
|
43
|
+
<% @routes.controls[route[:target]][route[:method]].each do |control| %>
|
44
|
+
controls.merge!(send("<%= control %>".to_sym, <%= route[:response_resource][:name] %>).call("<%= route[:target] %>".pluralize, params))
|
45
|
+
<% end %>
|
46
|
+
<% end %>
|
47
|
+
json({
|
48
|
+
:@controls => controls
|
49
|
+
}.merge!(response_body))
|
57
50
|
end
|
58
|
-
<% end %>
|
59
51
|
<% end %>
|
60
|
-
|
61
|
-
def mason_up(type)
|
62
|
-
if type == collection
|
63
|
-
proc do |path, params|
|
64
|
-
{
|
65
|
-
up: Mason::Up.new(
|
66
|
-
{
|
67
|
-
href: "http://localhost:9292/#{<%= @root_path %>}"
|
68
|
-
}
|
69
|
-
).to_hash
|
70
|
-
}
|
71
|
-
end
|
72
|
-
else
|
73
|
-
proc do |path, params|
|
74
|
-
{
|
75
|
-
up: Mason::Up.new(
|
76
|
-
{
|
77
|
-
href: "http://localhost:9292/#{path}"
|
78
|
-
}
|
79
|
-
).to_hash
|
80
|
-
}
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
def mason_self(item)
|
85
|
-
proc do |path, params|
|
86
|
-
if params[:item_id]
|
87
|
-
{
|
88
|
-
self: Mason::Self.new(
|
89
|
-
{
|
90
|
-
href: "http://localhost:9292/#{path}/#{params[:item_id]}"
|
91
|
-
}
|
92
|
-
).to_hash
|
93
|
-
}
|
94
|
-
else
|
95
|
-
{
|
96
|
-
self: Mason::Self.new(
|
97
|
-
{
|
98
|
-
href: "http://localhost:9292/#{path}"
|
99
|
-
}
|
100
|
-
).to_hash
|
101
|
-
}
|
102
|
-
end
|
103
|
-
end
|
104
|
-
end
|
105
|
-
def edit(item)
|
106
|
-
proc do |path, params|
|
107
|
-
{
|
108
|
-
edit: ("Edit" + item.to_s.singularize.camelize).constantize.new(
|
109
|
-
{
|
110
|
-
form: Mason::Edit.new(
|
111
|
-
href: "http://localhost:9292/#{path}/#{params[:item_id]}"
|
112
|
-
)
|
113
|
-
}
|
114
|
-
).to_hash
|
115
|
-
}
|
116
|
-
end
|
117
|
-
end
|
118
|
-
def remove(item)
|
119
|
-
proc do |path, params|
|
120
|
-
{
|
121
|
-
remove: Mason::Remove.new(
|
122
|
-
{
|
123
|
-
href: "http://localhost:9292/#{path}/#{params[:item_id]}"
|
124
|
-
}
|
125
|
-
).to_hash
|
126
|
-
}
|
127
|
-
end
|
128
|
-
end
|
129
|
-
def create(item)
|
130
|
-
proc do |path, params|
|
131
|
-
{
|
132
|
-
create: ("Create" + item.to_s.singularize.camelize).constantize.new(
|
133
|
-
{
|
134
|
-
form: Mason::Create.new(
|
135
|
-
href: ""
|
136
|
-
)
|
137
|
-
}
|
138
|
-
)
|
139
|
-
}
|
140
|
-
end
|
141
|
-
end
|
142
|
-
def filter(collection)
|
143
|
-
# TODO this prob cant really work here
|
144
|
-
proc do |path, params|
|
145
|
-
if params[:item_id]
|
146
|
-
{
|
147
|
-
"<%= @prefix %>filter": Mason::Filter.new(
|
148
|
-
{
|
149
|
-
href: "http://localhost:9292/#{path}/#{params[:item_id]}"
|
150
|
-
}
|
151
|
-
).to_hash
|
152
|
-
}
|
153
|
-
else
|
154
|
-
{
|
155
|
-
"<%= @prefix %>filter": Mason::Filter.new(
|
156
|
-
{
|
157
|
-
href: "http://localhost:9292/#{path}"
|
158
|
-
}
|
159
|
-
).to_hash
|
160
|
-
}
|
161
|
-
end
|
162
|
-
end
|
163
|
-
end
|
164
52
|
end
|