packages feed

yarn2nix-0.10.1: src/Distribution/Nixpkgs/Nodejs/License.hs

{-# LANGUAGE NoImplicitPrelude, OverloadedStrings, RecordWildCards, FlexibleInstances, GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE TypeApplications #-}
  {-|
Description: Convert @package.json@ license fields to nixpkgs license attribute sets
-}
module Distribution.Nixpkgs.Nodejs.License
  ( -- * Conversion Logic
    nodeLicenseToNixpkgs
    -- * License Lookup Table
  , LicensesBySpdxId
  ) where

import Protolude

import qualified Data.Aeson as A
import qualified Nix.Expr as Nix
import qualified Data.Map.Strict as Map
import qualified Data.Aeson.BetterErrors as Json
import qualified Data.Scientific as Scientific

-- newtype to circumvent the default instance: we don't want
-- the key of the JSON object to be the key of the HashMap,
-- but one of its values (spdxId).
-- | Lookup table from SPDX identifier (as 'Text') to 'NixpkgsLicense'.
newtype LicensesBySpdxId
  = LicensesBySpdxId (Map Text NixpkgsLicense)
  deriving stock (Show, Eq)
  deriving newtype (Semigroup, Monoid)

-- | Representation of a nixpkgs license set as found in
--   @lib.licenses@. There doesn't seem to be a strict
--   definition of what is required and what is optional,
--   the distribution of 'Maybe' and non-'Maybe' values
--   is based on the current situation in @lib/licenses.nix@.
data NixpkgsLicense
  = NixpkgsLicense ([(Text, LicenseValue)])
  deriving stock (Show, Eq)
data LicenseValue
  = LText Text
  | LBool Bool
  | LInt Int
  deriving stock (Show, Eq)

-- | Static version of @lib.licenses.unfree@,
--   so @UNLICENSED@ can be handled correctly
--   even if no lookup table is provided.
--
-- TODO: this will go out of sync with the nixpkgs definitions every once in a while, how to fix?
unfreeLicense :: NixpkgsLicense
unfreeLicense = NixpkgsLicense $ [
    ("shortName", LText "unfree")
  , ("deprecated", LBool False)
  , ("fullName", LText "Unfree")
  , ("redistributable", LBool False)
  , ("free", LBool False)
 ]

instance A.FromJSON LicensesBySpdxId where
  parseJSON = Json.toAesonParser identity ((Json.forEachInObject $ \_key -> do
    Json.keyMay "spdxId" Json.asText
    >>= \case
      Nothing -> pure Nothing
      Just spdxId -> do
        spdxLicense <- (Json.eachInObject $ Json.withValue $ \case
          A.String t -> Right $ LText t
          A.Bool b -> Right $ LBool b
          A.Number s -> case Scientific.toBoundedInteger @Int s of
            Just i -> Right $ LInt i
            Nothing -> Left $ "Not an integer: " <> (s & show)
          A.Null -> Left "Cannot parse Null as license value for now"
          A.Object _ -> Left "Cannot parse Object as license value for now"
          A.Array _ -> Left "Cannot parse Array as license value for now")
          <&> NixpkgsLicense
        pure $ Just (spdxId, spdxLicense)
   )
      <&> catMaybes
      <&> Map.fromList
      <&> LicensesBySpdxId
   )


-- | Build nix attribute set for given 'NixpkgsLicense'.
--
--   The resulting nix value of @nixpkgsLicenseExpression x@
--   should be equal to @lib.licenses.<attrName x>@ for the
--   same version of nixpkgs used.
nixpkgsLicenseExpression :: NixpkgsLicense -> Nix.NExpr
nixpkgsLicenseExpression (NixpkgsLicense m) =
  m
  <&> second licenseValueToNExpr
  & Nix.attrsE

licenseValueToNExpr :: LicenseValue -> Nix.NExpr
licenseValueToNExpr = \case
  LText t -> Nix.mkStr t
  LInt i -> Nix.mkInt (i & fromIntegral @Int @Integer)
  LBool b -> Nix.mkBool b

-- | Implements the logic for converting from an (optional)
--   @package.json@ @license@ field to a nixpkgs @meta.license@
--   set. Since support for multiple licenses is poor in nixpkgs
--   at the moment, we don't attempt to convert SPDX expressions
--   like @(ISC OR GPL-3.0-only)@.
--
--   See <https://docs.npmjs.com/files/package.json#license> for
--   details on npm's @license@ field.
nodeLicenseToNixpkgs :: Text -> LicensesBySpdxId -> Nix.NExpr
nodeLicenseToNixpkgs nodeLicense licSet = do
  if nodeLicense == "UNLICENSED"
    then nixpkgsLicenseExpression unfreeLicense
    else case lookupSpdxId nodeLicense licSet of
      Nothing -> Nix.mkStr nodeLicense
      Just license -> license

-- | Lookup function for 'LicensesBySpdxId' which directly returns a 'NExpr'.
--   This function only looks up by SPDX identifier and does not take
--   npm-specific quirks into account.
--
--   Use 'nodeLicenseToNixpkgs' when dealing with the @license@ field
--   of a npm-ish javascript package.
lookupSpdxId :: Text -> LicensesBySpdxId -> Maybe Nix.NExpr
lookupSpdxId lic (LicensesBySpdxId licSet) =
  licSet
  & Map.lookup lic
  <&> nixpkgsLicenseExpression