{-# LANGUAGE OverloadedStrings #-}

{- |
Module      : OpenTelemetry.Metric.InstrumentName
Copyright   : (c) Ian Duncan, 2026
License     : BSD-3
Description : Instrument name and unit validation per the metrics API spec.
Stability   : experimental

The SDK validates instrument names and units at creation time. The API itself
does not validate (per spec), but these functions are used by the SDK to reject
invalid names before registering instruments.

Spec: <https://opentelemetry.io/docs/specs/otel/metrics/api/#instrument-name-syntax>
-}
module OpenTelemetry.Metric.InstrumentName (
  validateInstrumentName,
  validateInstrumentUnit,
) where

import Data.Char (isAscii)
import Data.Text (Text)
import qualified Data.Text as T


{- | @Nothing@ if valid; @Just err@ with a short English reason if invalid.
Implements the stable rules from specification/metrics/api.md (instrument name ABNF; ASCII only).

@since 0.0.1.0
-}
validateInstrumentName :: Text -> Maybe Text
validateInstrumentName :: Text -> Maybe Text
validateInstrumentName Text
t
  | Text -> Bool
T.null Text
t = Text -> Maybe Text
forall a. a -> Maybe a
Just Text
"instrument name must not be empty"
  | Text -> Int
T.length Text
t Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> Int
255 = Text -> Maybe Text
forall a. a -> Maybe a
Just Text
"instrument name exceeds 255 characters"
  | Bool
otherwise =
      let c0 :: Char
c0 = HasCallStack => Text -> Int -> Char
Text -> Int -> Char
T.index Text
t Int
0
      in if Bool -> Bool
not (Char -> Bool
isAsciiAlpha Char
c0)
           then Text -> Maybe Text
forall a. a -> Maybe a
Just Text
"instrument name must start with an ASCII letter"
           else Int -> Maybe Text
go Int
1
  where
    go :: Int -> Maybe Text
    go :: Int -> Maybe Text
go Int
i
      | Int
i Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
>= Text -> Int
T.length Text
t = Maybe Text
forall a. Maybe a
Nothing
      | Bool
otherwise =
          let c :: Char
c = HasCallStack => Text -> Int -> Char
Text -> Int -> Char
T.index Text
t Int
i
          in if Char -> Bool
isValidChar Char
c
               then Int -> Maybe Text
go (Int
i Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
1)
               else Text -> Maybe Text
forall a. a -> Maybe a
Just Text
"instrument name contains invalid characters"
    isAsciiAlpha :: Char -> Bool
isAsciiAlpha Char
c =
      Char -> Bool
isAscii Char
c
        Bool -> Bool -> Bool
&& ((Char
c Char -> Char -> Bool
forall a. Ord a => a -> a -> Bool
>= Char
'A' Bool -> Bool -> Bool
&& Char
c Char -> Char -> Bool
forall a. Ord a => a -> a -> Bool
<= Char
'Z') Bool -> Bool -> Bool
|| (Char
c Char -> Char -> Bool
forall a. Ord a => a -> a -> Bool
>= Char
'a' Bool -> Bool -> Bool
&& Char
c Char -> Char -> Bool
forall a. Ord a => a -> a -> Bool
<= Char
'z'))
    isAsciiDigit :: Char -> Bool
isAsciiDigit Char
c = Char
c Char -> Char -> Bool
forall a. Ord a => a -> a -> Bool
>= Char
'0' Bool -> Bool -> Bool
&& Char
c Char -> Char -> Bool
forall a. Ord a => a -> a -> Bool
<= Char
'9'
    isValidChar :: Char -> Bool
isValidChar Char
c =
      Char -> Bool
isAscii Char
c
        Bool -> Bool -> Bool
&& ( Char -> Bool
isAsciiAlpha Char
c
               Bool -> Bool -> Bool
|| Char -> Bool
isAsciiDigit Char
c
               Bool -> Bool -> Bool
|| Char
c Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
'_'
               Bool -> Bool -> Bool
|| Char
c Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
'.'
               Bool -> Bool -> Bool
|| Char
c Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
'-'
               Bool -> Bool -> Bool
|| Char
c Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
'/'
           )


{- | Unit is optional; when present it must be ASCII and at most 63 code units (specification/metrics/api.md).

@since 0.0.1.0
-}
validateInstrumentUnit :: Text -> Maybe Text
validateInstrumentUnit :: Text -> Maybe Text
validateInstrumentUnit Text
u
  | Text -> Bool
T.null Text
u = Maybe Text
forall a. Maybe a
Nothing
  | Text -> Int
T.length Text
u Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> Int
63 = Text -> Maybe Text
forall a. a -> Maybe a
Just Text
"instrument unit exceeds 63 characters"
  | Bool
otherwise =
      let step :: Int -> Maybe a
step Int
i
            | Int
i Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
>= Text -> Int
T.length Text
u = Maybe a
forall a. Maybe a
Nothing
            | Bool
otherwise =
                let c :: Char
c = HasCallStack => Text -> Int -> Char
Text -> Int -> Char
T.index Text
u Int
i
                in if Char -> Bool
isAscii Char
c then Int -> Maybe a
step (Int
i Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
1) else a -> Maybe a
forall a. a -> Maybe a
Just a
"instrument unit must be ASCII"
      in Int -> Maybe Text
forall {a}. IsString a => Int -> Maybe a
step Int
0