2020-04-20 18:34:07 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"go/ast"
|
|
|
|
"go/parser"
|
|
|
|
"go/token"
|
|
|
|
"reflect"
|
|
|
|
"sort"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
"unicode"
|
|
|
|
|
2020-08-24 10:11:18 +00:00
|
|
|
"github.com/ipfs/go-cid"
|
|
|
|
"github.com/ipfs/go-filestore"
|
2020-09-02 22:45:57 +00:00
|
|
|
metrics "github.com/libp2p/go-libp2p-core/metrics"
|
2020-08-24 10:11:18 +00:00
|
|
|
"github.com/libp2p/go-libp2p-core/network"
|
|
|
|
"github.com/libp2p/go-libp2p-core/peer"
|
2020-09-02 23:31:41 +00:00
|
|
|
protocol "github.com/libp2p/go-libp2p-core/protocol"
|
2020-08-24 10:11:18 +00:00
|
|
|
pubsub "github.com/libp2p/go-libp2p-pubsub"
|
|
|
|
"github.com/multiformats/go-multiaddr"
|
|
|
|
|
2020-04-20 18:34:07 +00:00
|
|
|
"github.com/filecoin-project/go-address"
|
|
|
|
"github.com/filecoin-project/go-bitfield"
|
2020-08-24 10:11:18 +00:00
|
|
|
datatransfer "github.com/filecoin-project/go-data-transfer"
|
|
|
|
"github.com/filecoin-project/go-fil-markets/retrievalmarket"
|
2020-05-20 18:23:51 +00:00
|
|
|
"github.com/filecoin-project/go-jsonrpc/auth"
|
2020-08-24 10:11:18 +00:00
|
|
|
"github.com/filecoin-project/go-multistore"
|
|
|
|
|
2020-09-07 03:49:10 +00:00
|
|
|
"github.com/filecoin-project/go-state-types/abi"
|
|
|
|
"github.com/filecoin-project/go-state-types/crypto"
|
|
|
|
"github.com/filecoin-project/go-state-types/exitcode"
|
2020-08-24 10:11:18 +00:00
|
|
|
|
2020-04-20 18:34:07 +00:00
|
|
|
"github.com/filecoin-project/lotus/api"
|
2020-07-01 21:01:27 +00:00
|
|
|
"github.com/filecoin-project/lotus/api/apistruct"
|
2020-04-20 18:34:07 +00:00
|
|
|
"github.com/filecoin-project/lotus/build"
|
|
|
|
"github.com/filecoin-project/lotus/chain/types"
|
|
|
|
"github.com/filecoin-project/lotus/node/modules/dtypes"
|
|
|
|
)
|
|
|
|
|
|
|
|
var ExampleValues = map[reflect.Type]interface{}{
|
2020-05-20 18:23:51 +00:00
|
|
|
reflect.TypeOf(auth.Permission("")): auth.Permission("write"),
|
|
|
|
reflect.TypeOf(""): "string value",
|
|
|
|
reflect.TypeOf(uint64(42)): uint64(42),
|
|
|
|
reflect.TypeOf(byte(7)): byte(7),
|
|
|
|
reflect.TypeOf([]byte{}): []byte("byte array"),
|
2020-04-20 18:34:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func addExample(v interface{}) {
|
|
|
|
ExampleValues[reflect.TypeOf(v)] = v
|
|
|
|
}
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
c, err := cid.Decode("bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4")
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
ExampleValues[reflect.TypeOf(c)] = c
|
|
|
|
|
|
|
|
c2, err := cid.Decode("bafy2bzacebp3shtrn43k7g3unredz7fxn4gj533d3o43tqn2p2ipxxhrvchve")
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
tsk := types.NewTipSetKey(c, c2)
|
|
|
|
|
|
|
|
ExampleValues[reflect.TypeOf(tsk)] = tsk
|
|
|
|
|
|
|
|
addr, err := address.NewIDAddress(1234)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
ExampleValues[reflect.TypeOf(addr)] = addr
|
|
|
|
|
2020-08-20 04:49:10 +00:00
|
|
|
pid, err := peer.Decode("12D3KooWGzxzKZYveHXtpG6AsrUJBcWxHBFS2HsEoGTxrMLvKXtf")
|
2020-04-20 18:34:07 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
addExample(pid)
|
2020-08-24 10:11:18 +00:00
|
|
|
addExample(&pid)
|
2020-04-20 18:34:07 +00:00
|
|
|
|
|
|
|
addExample(bitfield.NewFromSet([]uint64{5}))
|
2020-06-15 16:30:49 +00:00
|
|
|
addExample(abi.RegisteredSealProof_StackedDrg32GiBV1)
|
2020-06-18 12:30:00 +00:00
|
|
|
addExample(abi.RegisteredPoStProof_StackedDrgWindow32GiBV1)
|
2020-04-20 18:34:07 +00:00
|
|
|
addExample(abi.ChainEpoch(10101))
|
|
|
|
addExample(crypto.SigTypeBLS)
|
|
|
|
addExample(int64(9))
|
2020-06-18 12:30:00 +00:00
|
|
|
addExample(12.3)
|
|
|
|
addExample(123)
|
|
|
|
addExample(uintptr(0))
|
2020-04-20 18:34:07 +00:00
|
|
|
addExample(abi.MethodNum(1))
|
|
|
|
addExample(exitcode.ExitCode(0))
|
|
|
|
addExample(crypto.DomainSeparationTag_ElectionProofProduction)
|
|
|
|
addExample(true)
|
|
|
|
addExample(abi.UnpaddedPieceSize(1024))
|
|
|
|
addExample(abi.UnpaddedPieceSize(1024).Padded())
|
|
|
|
addExample(abi.DealID(5432))
|
|
|
|
addExample(filestore.StatusFileChanged)
|
|
|
|
addExample(abi.SectorNumber(9))
|
|
|
|
addExample(abi.SectorSize(32 * 1024 * 1024 * 1024))
|
|
|
|
addExample(api.MpoolChange(0))
|
|
|
|
addExample(network.Connected)
|
|
|
|
addExample(dtypes.NetworkName("lotus"))
|
|
|
|
addExample(api.SyncStateStage(1))
|
2020-09-08 18:54:37 +00:00
|
|
|
addExample(build.FullAPIVersion)
|
2020-04-20 18:34:07 +00:00
|
|
|
addExample(api.PCHInbound)
|
|
|
|
addExample(time.Minute)
|
2020-08-24 10:11:18 +00:00
|
|
|
addExample(datatransfer.TransferID(3))
|
|
|
|
addExample(datatransfer.Ongoing)
|
|
|
|
addExample(multistore.StoreID(50))
|
|
|
|
addExample(retrievalmarket.ClientEventDealAccepted)
|
|
|
|
addExample(retrievalmarket.DealStatusNew)
|
|
|
|
addExample(network.ReachabilityPublic)
|
2020-09-17 15:31:09 +00:00
|
|
|
addExample(build.NewestNetworkVersion)
|
2020-06-11 00:47:28 +00:00
|
|
|
addExample(&types.ExecutionTrace{
|
2020-06-18 12:30:00 +00:00
|
|
|
Msg: exampleValue(reflect.TypeOf(&types.Message{}), nil).(*types.Message),
|
|
|
|
MsgRct: exampleValue(reflect.TypeOf(&types.MessageReceipt{}), nil).(*types.MessageReceipt),
|
2020-04-20 18:34:07 +00:00
|
|
|
})
|
|
|
|
addExample(map[string]types.Actor{
|
2020-06-18 12:30:00 +00:00
|
|
|
"t01236": exampleValue(reflect.TypeOf(types.Actor{}), nil).(types.Actor),
|
2020-04-20 18:34:07 +00:00
|
|
|
})
|
|
|
|
addExample(map[string]api.MarketDeal{
|
2020-06-18 12:30:00 +00:00
|
|
|
"t026363": exampleValue(reflect.TypeOf(api.MarketDeal{}), nil).(api.MarketDeal),
|
2020-04-20 18:34:07 +00:00
|
|
|
})
|
|
|
|
addExample(map[string]api.MarketBalance{
|
2020-06-18 12:30:00 +00:00
|
|
|
"t026363": exampleValue(reflect.TypeOf(api.MarketBalance{}), nil).(api.MarketBalance),
|
2020-04-20 18:34:07 +00:00
|
|
|
})
|
2020-08-24 10:11:18 +00:00
|
|
|
addExample(map[string]*pubsub.TopicScoreSnapshot{
|
|
|
|
"/blocks": {
|
|
|
|
TimeInMesh: time.Minute,
|
|
|
|
FirstMessageDeliveries: 122,
|
|
|
|
MeshMessageDeliveries: 1234,
|
|
|
|
InvalidMessageDeliveries: 3,
|
|
|
|
},
|
|
|
|
})
|
2020-09-02 22:45:57 +00:00
|
|
|
addExample(map[string]metrics.Stats{
|
2020-09-02 23:42:55 +00:00
|
|
|
"12D3KooWSXmXLJmBR1M7i9RW9GQPNUhZSzXKzxDHWtAgNuJAbyEJ": {
|
2020-09-02 22:45:57 +00:00
|
|
|
RateIn: 100,
|
|
|
|
RateOut: 50,
|
|
|
|
TotalIn: 174000,
|
|
|
|
TotalOut: 12500,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
addExample(map[protocol.ID]metrics.Stats{
|
2020-09-02 23:42:55 +00:00
|
|
|
"/fil/hello/1.0.0": {
|
2020-09-02 22:45:57 +00:00
|
|
|
RateIn: 100,
|
|
|
|
RateOut: 50,
|
|
|
|
TotalIn: 174000,
|
|
|
|
TotalOut: 12500,
|
|
|
|
},
|
|
|
|
})
|
2020-04-20 18:34:07 +00:00
|
|
|
|
|
|
|
maddr, err := multiaddr.NewMultiaddr("/ip4/52.36.61.156/tcp/1347/p2p/12D3KooWFETiESTf1v4PGUvtnxMAcEFMzLZbJGg4tjWfGEimYior")
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// because reflect.TypeOf(maddr) returns the concrete type...
|
|
|
|
ExampleValues[reflect.TypeOf(struct{ A multiaddr.Multiaddr }{}).Field(0).Type] = maddr
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-06-18 12:30:00 +00:00
|
|
|
func exampleValue(t, parent reflect.Type) interface{} {
|
2020-04-20 18:34:07 +00:00
|
|
|
v, ok := ExampleValues[t]
|
|
|
|
if ok {
|
|
|
|
return v
|
|
|
|
}
|
|
|
|
|
|
|
|
switch t.Kind() {
|
|
|
|
case reflect.Slice:
|
|
|
|
out := reflect.New(t).Elem()
|
2020-06-18 12:30:00 +00:00
|
|
|
reflect.Append(out, reflect.ValueOf(exampleValue(t.Elem(), t)))
|
2020-04-20 18:34:07 +00:00
|
|
|
return out.Interface()
|
|
|
|
case reflect.Chan:
|
2020-06-18 12:30:00 +00:00
|
|
|
return exampleValue(t.Elem(), nil)
|
2020-04-20 18:34:07 +00:00
|
|
|
case reflect.Struct:
|
2020-06-18 12:30:00 +00:00
|
|
|
es := exampleStruct(t, parent)
|
2020-04-20 18:34:07 +00:00
|
|
|
v := reflect.ValueOf(es).Elem().Interface()
|
|
|
|
ExampleValues[t] = v
|
|
|
|
return v
|
|
|
|
case reflect.Array:
|
|
|
|
out := reflect.New(t).Elem()
|
|
|
|
for i := 0; i < t.Len(); i++ {
|
2020-06-18 12:30:00 +00:00
|
|
|
out.Index(i).Set(reflect.ValueOf(exampleValue(t.Elem(), t)))
|
2020-04-20 18:34:07 +00:00
|
|
|
}
|
|
|
|
return out.Interface()
|
|
|
|
|
|
|
|
case reflect.Ptr:
|
|
|
|
if t.Elem().Kind() == reflect.Struct {
|
2020-06-18 12:30:00 +00:00
|
|
|
es := exampleStruct(t.Elem(), t)
|
2020-04-20 18:34:07 +00:00
|
|
|
//ExampleValues[t] = es
|
|
|
|
return es
|
|
|
|
}
|
|
|
|
case reflect.Interface:
|
|
|
|
return struct{}{}
|
|
|
|
}
|
|
|
|
|
|
|
|
panic(fmt.Sprintf("No example value for type: %s", t))
|
|
|
|
}
|
|
|
|
|
2020-06-18 12:30:00 +00:00
|
|
|
func exampleStruct(t, parent reflect.Type) interface{} {
|
2020-04-20 18:34:07 +00:00
|
|
|
ns := reflect.New(t)
|
|
|
|
for i := 0; i < t.NumField(); i++ {
|
|
|
|
f := t.Field(i)
|
2020-06-18 12:30:00 +00:00
|
|
|
if f.Type == parent {
|
|
|
|
continue
|
|
|
|
}
|
2020-04-20 18:34:07 +00:00
|
|
|
if strings.Title(f.Name) == f.Name {
|
2020-06-18 12:30:00 +00:00
|
|
|
ns.Elem().Field(i).Set(reflect.ValueOf(exampleValue(f.Type, t)))
|
2020-04-20 18:34:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ns.Interface()
|
|
|
|
}
|
|
|
|
|
|
|
|
type Visitor struct {
|
|
|
|
Methods map[string]ast.Node
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *Visitor) Visit(node ast.Node) ast.Visitor {
|
|
|
|
st, ok := node.(*ast.TypeSpec)
|
|
|
|
if !ok {
|
|
|
|
return v
|
|
|
|
}
|
|
|
|
|
|
|
|
if st.Name.Name != "FullNode" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
iface := st.Type.(*ast.InterfaceType)
|
|
|
|
for _, m := range iface.Methods.List {
|
|
|
|
if len(m.Names) > 0 {
|
|
|
|
v.Methods[m.Names[0].Name] = m
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return v
|
|
|
|
}
|
|
|
|
|
2020-05-19 16:49:47 +00:00
|
|
|
const noComment = "There are not yet any comments for this method."
|
|
|
|
|
2020-06-02 14:29:39 +00:00
|
|
|
func parseApiASTInfo() (map[string]string, map[string]string) { //nolint:golint
|
2020-04-20 18:34:07 +00:00
|
|
|
fset := token.NewFileSet()
|
|
|
|
pkgs, err := parser.ParseDir(fset, "./api", nil, parser.AllErrors|parser.ParseComments)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Println("parse error: ", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
ap := pkgs["api"]
|
|
|
|
|
|
|
|
f := ap.Files["api/api_full.go"]
|
|
|
|
|
|
|
|
cmap := ast.NewCommentMap(fset, f, f.Comments)
|
|
|
|
|
|
|
|
v := &Visitor{make(map[string]ast.Node)}
|
|
|
|
ast.Walk(v, pkgs["api"])
|
|
|
|
|
2020-05-19 15:48:58 +00:00
|
|
|
groupDocs := make(map[string]string)
|
2020-04-20 18:34:07 +00:00
|
|
|
out := make(map[string]string)
|
|
|
|
for mn, node := range v.Methods {
|
|
|
|
cs := cmap.Filter(node).Comments()
|
|
|
|
if len(cs) == 0 {
|
2020-05-19 16:49:47 +00:00
|
|
|
out[mn] = noComment
|
2020-04-20 18:34:07 +00:00
|
|
|
} else {
|
2020-05-19 15:48:58 +00:00
|
|
|
for _, c := range cs {
|
|
|
|
if strings.HasPrefix(c.Text(), "MethodGroup:") {
|
|
|
|
parts := strings.Split(c.Text(), "\n")
|
|
|
|
groupName := strings.TrimSpace(parts[0][12:])
|
|
|
|
comment := strings.Join(parts[1:], "\n")
|
|
|
|
groupDocs[groupName] = comment
|
2020-05-19 16:49:47 +00:00
|
|
|
|
|
|
|
break
|
2020-05-19 15:48:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-19 16:49:47 +00:00
|
|
|
last := cs[len(cs)-1].Text()
|
|
|
|
if !strings.HasPrefix(last, "MethodGroup:") {
|
|
|
|
out[mn] = last
|
|
|
|
} else {
|
|
|
|
out[mn] = noComment
|
|
|
|
}
|
2020-04-20 18:34:07 +00:00
|
|
|
}
|
|
|
|
}
|
2020-05-19 15:48:58 +00:00
|
|
|
return out, groupDocs
|
2020-04-20 18:34:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type MethodGroup struct {
|
|
|
|
GroupName string
|
|
|
|
Header string
|
|
|
|
Methods []*Method
|
|
|
|
}
|
|
|
|
|
|
|
|
type Method struct {
|
|
|
|
Comment string
|
|
|
|
Name string
|
|
|
|
InputExample string
|
|
|
|
ResponseExample string
|
|
|
|
}
|
|
|
|
|
|
|
|
func methodGroupFromName(mn string) string {
|
|
|
|
i := strings.IndexFunc(mn[1:], func(r rune) bool {
|
|
|
|
return unicode.IsUpper(r)
|
|
|
|
})
|
|
|
|
if i < 0 {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return mn[:i+1]
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
2020-05-19 15:48:58 +00:00
|
|
|
comments, groupComments := parseApiASTInfo()
|
2020-04-20 18:34:07 +00:00
|
|
|
|
|
|
|
groups := make(map[string]*MethodGroup)
|
|
|
|
|
|
|
|
var api struct{ api.FullNode }
|
|
|
|
t := reflect.TypeOf(api)
|
|
|
|
for i := 0; i < t.NumMethod(); i++ {
|
|
|
|
m := t.Method(i)
|
|
|
|
|
|
|
|
groupName := methodGroupFromName(m.Name)
|
|
|
|
|
|
|
|
g, ok := groups[groupName]
|
|
|
|
if !ok {
|
|
|
|
g = new(MethodGroup)
|
2020-05-19 15:48:58 +00:00
|
|
|
g.Header = groupComments[groupName]
|
2020-04-20 18:34:07 +00:00
|
|
|
g.GroupName = groupName
|
|
|
|
groups[groupName] = g
|
|
|
|
}
|
|
|
|
|
|
|
|
var args []interface{}
|
|
|
|
ft := m.Func.Type()
|
|
|
|
for j := 2; j < ft.NumIn(); j++ {
|
|
|
|
inp := ft.In(j)
|
2020-06-18 12:30:00 +00:00
|
|
|
args = append(args, exampleValue(inp, nil))
|
2020-04-20 18:34:07 +00:00
|
|
|
}
|
|
|
|
|
2020-06-18 12:37:36 +00:00
|
|
|
v, err := json.MarshalIndent(args, "", " ")
|
2020-04-20 18:34:07 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
2020-06-18 12:30:00 +00:00
|
|
|
outv := exampleValue(ft.Out(0), nil)
|
2020-04-20 18:34:07 +00:00
|
|
|
|
2020-06-18 12:37:36 +00:00
|
|
|
ov, err := json.MarshalIndent(outv, "", " ")
|
2020-04-20 18:34:07 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
g.Methods = append(g.Methods, &Method{
|
|
|
|
Name: m.Name,
|
|
|
|
Comment: comments[m.Name],
|
|
|
|
InputExample: string(v),
|
|
|
|
ResponseExample: string(ov),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
var groupslice []*MethodGroup
|
|
|
|
for _, g := range groups {
|
|
|
|
groupslice = append(groupslice, g)
|
|
|
|
}
|
|
|
|
|
|
|
|
sort.Slice(groupslice, func(i, j int) bool {
|
|
|
|
return groupslice[i].GroupName < groupslice[j].GroupName
|
|
|
|
})
|
|
|
|
|
2020-06-18 12:56:00 +00:00
|
|
|
fmt.Printf("# Groups\n")
|
|
|
|
|
|
|
|
for _, g := range groupslice {
|
|
|
|
fmt.Printf("* [%s](#%s)\n", g.GroupName, g.GroupName)
|
|
|
|
for _, method := range g.Methods {
|
|
|
|
fmt.Printf(" * [%s](#%s)\n", method.Name, method.Name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-01 21:01:27 +00:00
|
|
|
permStruct := reflect.TypeOf(apistruct.FullNodeStruct{}.Internal)
|
|
|
|
commonPermStruct := reflect.TypeOf(apistruct.CommonStruct{}.Internal)
|
|
|
|
|
2020-04-20 18:34:07 +00:00
|
|
|
for _, g := range groupslice {
|
2020-05-27 20:53:20 +00:00
|
|
|
g := g
|
2020-04-20 18:34:07 +00:00
|
|
|
fmt.Printf("## %s\n", g.GroupName)
|
|
|
|
fmt.Printf("%s\n\n", g.Header)
|
|
|
|
|
|
|
|
sort.Slice(g.Methods, func(i, j int) bool {
|
|
|
|
return g.Methods[i].Name < g.Methods[j].Name
|
|
|
|
})
|
|
|
|
|
|
|
|
for _, m := range g.Methods {
|
|
|
|
fmt.Printf("### %s\n", m.Name)
|
|
|
|
fmt.Printf("%s\n\n", m.Comment)
|
|
|
|
|
2020-07-01 21:01:27 +00:00
|
|
|
meth, ok := permStruct.FieldByName(m.Name)
|
|
|
|
if !ok {
|
|
|
|
meth, ok = commonPermStruct.FieldByName(m.Name)
|
|
|
|
if !ok {
|
|
|
|
panic("no perms for method: " + m.Name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
perms := meth.Tag.Get("perm")
|
|
|
|
|
|
|
|
fmt.Printf("Perms: %s\n\n", perms)
|
|
|
|
|
2020-06-18 12:37:36 +00:00
|
|
|
if strings.Count(m.InputExample, "\n") > 0 {
|
|
|
|
fmt.Printf("Inputs:\n```json\n%s\n```\n\n", m.InputExample)
|
|
|
|
} else {
|
|
|
|
fmt.Printf("Inputs: `%s`\n\n", m.InputExample)
|
|
|
|
}
|
|
|
|
|
|
|
|
if strings.Count(m.ResponseExample, "\n") > 0 {
|
|
|
|
fmt.Printf("Response:\n```json\n%s\n```\n\n", m.ResponseExample)
|
|
|
|
} else {
|
|
|
|
fmt.Printf("Response: `%s`\n\n", m.ResponseExample)
|
|
|
|
}
|
2020-04-20 18:34:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|