Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Config Packages] Enable optional map spec #1351

Merged
merged 6 commits into from
Aug 8, 2023
Merged

Conversation

savil
Copy link
Collaborator

@savil savil commented Aug 7, 2023

Summary

Motivation
User feedback is leading us to add more parameters besides version to the packages config.
For instance, some packages may only need to be added on certain platforms.

Previously, packages were specified as []string. For example:

{
  "packages": [
    "[email protected]",
    "hello",
    "cowsay@latest"
  ],
  ...
}

In the new format, they can also be specified as:

{
   "packages": {
      "python": "3.9",
      "hello": "",
      "cowsay": {
        "version": "latest",
      }
   }
}

NOTE: python is an inline-version, while cowsay has a struct value which can
accomodate other fields like platforms.

Approach
For now, we enable both packages as List and Map to work. The default continues to be List.
We do this by adding a Packages struct and Package struct with custom JSON marshalling and unmarshalling.

Future PRs

  1. Introduce Packages.platforms and/or Packages.excludedPlatforms fields. Use them when installing to nix profile, and generating the flake.nix.
  2. Introduce devbox add and devbox rm flags for platform-specific packages.
  3. Detect package usage failures when due to platform mis-match and suggest using CLI commands to run to make platform-specific.

How was it tested?

Added TestJsonifyConfigPackages and other unit-tests

testscripts pass

devbox init produces the same output as before.

Copy link
Collaborator Author

savil commented Aug 7, 2023

Current dependencies on/for this PR:

This comment was auto-generated by Graphite.

Base automatically changed from savil/rm-cuego-complete to main August 7, 2023 21:02
@savil savil force-pushed the savil/config-packages branch 4 times, most recently from 93e1315 to 22fa857 Compare August 7, 2023 22:11
@savil savil marked this pull request as ready for review August 7, 2023 22:16
Copy link
Contributor

@mikeland73 mikeland73 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really nice implementation! great encapsulation, super easy to read.

}

// If we have a regular package, we want to marshal the entire struct:
type Alias Package // Use an alias-type to avoid infinite recursion
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is cool! I've gotten into infinite recursion issue when implementing custom marshaling and I don't remember the solution being this clean.

nit, make lower case

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, and lower cased

}

// parseVersionedName parses the name and version from package@version representation
func parseVersionedName(versionedName string) (name, version string) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see searcher.ParseVersionedPackage

this does a little more, but hopefully you can use it to prevent some code duplication.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ohh good call. Yes, I can use it. There are some subtle differences so I still need this function here, but can call into searcher.ParseVersionedPackage in the implementation.

I've augmented the searcher.ParseVersionedPackage to handle a couple cases I do here. And added test-cases.

Comment on lines 170 to 171
type Alias Package // Use an alias-type to avoid infinite recursion
alias := &Alias{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit lower case, maybe

type packageAlias Package // Use an alias-type to avoid infinite recursion
alias := &packageAlias{}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lowercased

Copy link
Collaborator

@gcurtis gcurtis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the tests! I also like the idea of aliasing the type for (un)marshalling.

The map order mattering is unfortunate. Maybe we can eventually stop relying on that.

// Remove removes a package from the list of packages
func (pkgs *Packages) Remove(versionedName string) error {
name, version := parseVersionedName(versionedName)
for idx, pkg := range pkgs.Collection {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slices.DeleteFunc might be a good fit here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice! done.

orderedmap "github.com/wk8/go-ordered-map/v2"
)

type jsonKind int
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could reuse json.Delim.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leaving in place for now, since it feels cleaner.

internal/devconfig/packages.go Outdated Show resolved Hide resolved

func (pkgs *Packages) UnmarshalJSON(data []byte) error {

// First, attempt to unmarshal as a list of strings (legacy format)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could also check the first byte for '[' or '{'.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leaving in place, since its cleaner

@@ -38,13 +38,21 @@ require (
github.com/spf13/cobra v1.6.1
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.2
github.com/wk8/go-ordered-map/v2 v2.1.8
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are there two orderedmap dependencies?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh I added the iancoleman one first, and then switched. This wk8 one is more modern and has a nicer API.

I'll remove the iancoleman one.

// We use orderedmap to preserve the order of the packages. While the JSON
// specification specifies that maps are unordered, we do rely on the order
// for certain functionality.
orderedMap := orderedmap.New[string, Package]()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could also use a json.Decoder to preserve the order and assign the key to the name field as you go.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the interests of time, I'm going to keep orderedmap for now. It seems to work well, and has a clean api.

The other library I used (see here) used json.Decoder in its implementation and its fairly non-trivial.


func (pkgs *Packages) MarshalJSON() ([]byte, error) {
if pkgs.jsonKind == jsonList {
packagesList := []string{}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
packagesList := []string{}
packagesList := make([]string, 0, len(pkgs.Collection))

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return errors.WithStack(err)
}

// more robust way to copy all fields from alias?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try *p = Package(*alias)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahhh thanks!!! Didn't know there was a built-in struct-copy function

Copy link
Collaborator Author

@savil savil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the comments! responses inline, and updates made.

@@ -38,13 +38,21 @@ require (
github.com/spf13/cobra v1.6.1
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.2
github.com/wk8/go-ordered-map/v2 v2.1.8
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh I added the iancoleman one first, and then switched. This wk8 one is more modern and has a nicer API.

I'll remove the iancoleman one.

// Remove removes a package from the list of packages
func (pkgs *Packages) Remove(versionedName string) error {
name, version := parseVersionedName(versionedName)
for idx, pkg := range pkgs.Collection {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice! done.


func (pkgs *Packages) MarshalJSON() ([]byte, error) {
if pkgs.jsonKind == jsonList {
packagesList := []string{}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

// We use orderedmap to preserve the order of the packages. While the JSON
// specification specifies that maps are unordered, we do rely on the order
// for certain functionality.
orderedMap := orderedmap.New[string, Package]()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the interests of time, I'm going to keep orderedmap for now. It seems to work well, and has a clean api.

The other library I used (see here) used json.Decoder in its implementation and its fairly non-trivial.

Comment on lines 170 to 171
type Alias Package // Use an alias-type to avoid infinite recursion
alias := &Alias{}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lowercased

}

// If we have a regular package, we want to marshal the entire struct:
type Alias Package // Use an alias-type to avoid infinite recursion
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, and lower cased

}

// parseVersionedName parses the name and version from package@version representation
func parseVersionedName(versionedName string) (name, version string) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ohh good call. Yes, I can use it. There are some subtle differences so I still need this function here, but can call into searcher.ParseVersionedPackage in the implementation.

I've augmented the searcher.ParseVersionedPackage to handle a couple cases I do here. And added test-cases.

internal/devconfig/packages.go Outdated Show resolved Hide resolved
orderedmap "github.com/wk8/go-ordered-map/v2"
)

type jsonKind int
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leaving in place for now, since it feels cleaner.


func (pkgs *Packages) UnmarshalJSON(data []byte) error {

// First, attempt to unmarshal as a list of strings (legacy format)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leaving in place, since its cleaner

@savil savil merged commit 1b5ece8 into main Aug 8, 2023
15 checks passed
@savil savil deleted the savil/config-packages branch August 8, 2023 19:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants