package options import ( "encoding/json" "fmt" "io" "log/slog" "net/url" "os" "reflect" "github.com/bcicen/jstream" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" ) type nixValueJSON struct { Type string `mapstructure:"_type"` Text string } type linkJSON struct { Name string URL string `json:"url"` } type nixOptionJSON struct { Declarations []linkJSON Default *nixValueJSON Description string Example *nixValueJSON Loc []string ReadOnly bool RelatedPackages string Type string } func ValueTypeToString(valueType jstream.ValueType) string { switch valueType { case jstream.Unknown: return "unknown" case jstream.Null: return "null" case jstream.String: return "string" case jstream.Number: return "number" case jstream.Boolean: return "boolean" case jstream.Array: return "array" case jstream.Object: return "object" } return "very strange" } func makeGitHubFileURL(userRepo string, ref string, subPath string) string { url, _ := url.JoinPath("https://github.com/", userRepo, "blob", ref, subPath) return url } // make configurable? var channelRepoMap = map[string]string{ "nixpkgs": "NixOS/nixpkgs", "nix-darwin": "LnL7/nix-darwin", "home-manager": "nix-community/home-manager", } func MakeChannelLink(channel string, ref string, subPath string) (*Link, error) { if channelRepoMap[channel] == "" { return nil, fmt.Errorf("don't know what repository relates to channel <%s>", channel) } return &Link{ Name: fmt.Sprintf("<%s/%s>", channel, subPath), URL: makeGitHubFileURL(channelRepoMap[channel], ref, subPath), }, nil } func convertNixValue(nj *nixValueJSON) *NixValue { if nj == nil { return nil } switch nj.Type { case "", "literalExpression": return &NixValue{ Text: nj.Text, } case "literalMD": return &NixValue{ Markdown: Markdown(nj.Text), } default: slog.Warn("got unexpected NixValue type", "type", nj.Type, "text", nj.Text) return nil } } func Process(inpath string, outpath string, channel string, revision string) error { infile, err := os.Open(inpath) if err != nil { return errors.WithMessagef(err, "failed to open input file %s", inpath) } defer infile.Close() outfile, err := os.Create(outpath) if err != nil { return errors.WithMessagef(err, "failed to open output file %s", outpath) } if outpath != "/dev/stdout" { defer outfile.Close() } dec := jstream.NewDecoder(infile, 1).EmitKV() var optJSON nixOptionJSON ms, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ ErrorUnused: true, ZeroFields: true, Result: &optJSON, Squash: true, DecodeHook: mapstructure.TextUnmarshallerHookFunc(), }) if err != nil { return errors.WithMessage(err, "could not create mapstructure decoder") } _, err = outfile.WriteString("[\n") if err != nil { return errors.WithMessage(err, "could not write to output") } for mv := range dec.Stream() { if err := dec.Err(); err != nil { return errors.WithMessage(err, "could not decode JSON") } if mv.ValueType != jstream.Object { return errors.Errorf("unexpected object type %s", ValueTypeToString(mv.ValueType)) } kv := mv.Value.(jstream.KV) x := kv.Value.(map[string]interface{}) var decls []*Link for _, decl := range x["declarations"].([]interface{}) { optJSON = nixOptionJSON{} switch decl := reflect.ValueOf(decl); decl.Kind() { case reflect.String: s := decl.String() link, err := MakeChannelLink(channel, revision, s) if err != nil { return errors.WithMessagef(err, "could not make a channel link for channel %s, revision %s and subpath %s", channel, revision, s, ) } decls = append(decls, link) case reflect.Map: v := decl.Interface().(map[string]interface{}) link := Link{ Name: v["name"].(string), URL: v["url"].(string), } decls = append(decls, &link) default: println("kind", decl.Kind().String()) panic("unexpected object type") } } if len(decls) > 0 { x["declarations"] = decls } err = ms.Decode(x) // stores in optJSON if err != nil { return errors.WithMessagef(err, "failed to decode option %#v", x) } var decs = make([]Link, len(optJSON.Declarations)) for i, d := range optJSON.Declarations { decs[i] = Link(d) } opt := NixOption{ Option: kv.Key, Declarations: decs, Default: convertNixValue(optJSON.Default), Description: Markdown(optJSON.Description), Example: convertNixValue(optJSON.Example), RelatedPackages: Markdown(optJSON.RelatedPackages), Loc: optJSON.Loc, Type: optJSON.Type, } b, err := json.MarshalIndent(opt, "", " ") if err != nil { return errors.WithMessagef(err, "failed to encode option %#v", opt) } _, err = outfile.Write(b) if err != nil { return errors.WithMessage(err, "failed to write to output") } _, err = outfile.WriteString(",\n") if err != nil { return errors.WithMessage(err, "failed to write to output") } } if outpath != "/dev/stdout" { _, err = outfile.Seek(-2, io.SeekCurrent) if err != nil { return errors.WithMessage(err, "could not write to output") } } _, err = outfile.WriteString("\n]\n") if err != nil { return errors.WithMessage(err, "could not write to output") } return nil }