Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
N
nfkit
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Locked Files
Issues
0
Issues
0
List
Boards
Labels
Service Desk
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Security & Compliance
Security & Compliance
Dependency List
License Compliance
Packages
Packages
List
Container Registry
Analytics
Analytics
CI / CD
Code Review
Insights
Issues
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
nanahira
nfkit
Commits
7448d3f4
Commit
7448d3f4
authored
Feb 14, 2026
by
nanahira
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
add configurer
parent
c1d07104
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
318 additions
and
1 deletion
+318
-1
index.ts
index.ts
+2
-0
src/configurer/configurer.ts
src/configurer/configurer.ts
+214
-0
src/configurer/index.ts
src/configurer/index.ts
+1
-0
tests/configurer.spec.ts
tests/configurer.spec.ts
+99
-0
tsconfig.json
tsconfig.json
+2
-1
No files found.
index.ts
View file @
7448d3f4
...
...
@@ -11,3 +11,5 @@ export * from './src/observe-diff';
export
*
from
'
./src/memorize
'
;
export
*
from
'
./src/may-be-array
'
;
export
*
from
'
./src/app-context
'
;
export
*
from
'
./src/configurer
'
;
src/configurer/configurer.ts
0 → 100644
View file @
7448d3f4
export
class
ConfigurerInstance
<
T
extends
Record
<
string
,
string
>>
{
constructor
(
public
config
:
T
,
private
readonly
defaultConfig
:
T
,
)
{}
getString
<
K
extends
keyof
T
>
(
key
:
K
):
string
{
return
(
this
.
config
[
key
]
||
this
.
defaultConfig
[
key
])
as
string
;
}
getInt
<
K
extends
keyof
T
>
(
key
:
K
):
number
{
return
parseInt
(
this
.
getString
(
key
));
}
getFloat
<
K
extends
keyof
T
>
(
key
:
K
):
number
{
return
parseFloat
(
this
.
getString
(
key
));
}
getBoolean
<
K
extends
keyof
T
>
(
key
:
K
):
boolean
{
const
defaultBoolean
=
parseConfigBoolean
(
this
.
defaultConfig
[
key
],
false
);
return
parseConfigBoolean
(
this
.
getString
(
key
),
defaultBoolean
);
}
getStringArray
<
K
extends
keyof
T
>
(
key
:
K
):
string
[]
{
return
convertStringArray
(
this
.
getString
(
key
));
}
getIntArray
<
K
extends
keyof
T
>
(
key
:
K
):
number
[]
{
return
convertIntArray
(
this
.
getString
(
key
));
}
getFloatArray
<
K
extends
keyof
T
>
(
key
:
K
):
number
[]
{
return
convertFloatArray
(
this
.
getString
(
key
));
}
getBooleanArray
<
K
extends
keyof
T
>
(
key
:
K
):
boolean
[]
{
const
defaultBoolean
=
parseConfigBoolean
(
this
.
defaultConfig
[
key
],
false
);
return
convertBooleanArray
(
this
.
getString
(
key
),
defaultBoolean
);
}
}
export
class
Configurer
<
T
extends
Record
<
string
,
string
>>
{
constructor
(
public
defaultConfig
:
T
)
{}
loadConfig
(
options
:
{
env
?:
Record
<
string
,
string
>
;
obj
?:
any
}
=
{},
):
ConfigurerInstance
<
T
>
{
const
readConfig
=
options
?.
obj
&&
typeof
options
.
obj
===
'
object
'
?
options
.
obj
:
{};
const
normalizedConfig
=
normalizeConfigByDefaultKeys
(
readConfig
,
this
.
defaultConfig
,
);
return
new
ConfigurerInstance
<
T
>
(
{
...
this
.
defaultConfig
,
...
normalizedConfig
,
...(
options
?.
env
||
{}),
}
as
T
,
this
.
defaultConfig
,
);
}
generateExampleObject
():
Record
<
string
,
string
|
number
|
string
[]
|
number
[]
>
{
return
Object
.
fromEntries
(
Object
.
entries
(
this
.
defaultConfig
).
map
(([
key
,
value
])
=>
{
if
(
value
.
includes
(
'
,
'
))
{
return
[
toCamelCaseKey
(
key
),
value
.
split
(
'
,
'
).
map
((
v
)
=>
toTypedValue
(
v
)),
];
}
return
[
toCamelCaseKey
(
key
),
toTypedValue
(
value
)];
}),
);
}
}
export
type
TypeFromConfigurer
<
C
extends
ConfigurerInstance
<
any
>>
=
C
extends
ConfigurerInstance
<
infer
T
>
?
T
:
never
;
function
toCamelCaseKey
(
key
:
string
):
string
{
const
lower
=
key
.
toLowerCase
();
return
lower
.
replace
(
/_
([
a-z0-9
])
/g
,
(
_
,
ch
:
string
)
=>
ch
.
toUpperCase
());
}
function
normalizeConfigValue
(
value
:
unknown
):
string
|
undefined
{
if
(
value
==
null
)
{
return
undefined
;
}
if
(
typeof
value
===
'
string
'
)
{
return
value
;
}
if
(
typeof
value
===
'
number
'
)
{
return
value
.
toString
();
}
if
(
typeof
value
===
'
boolean
'
)
{
return
value
?
'
1
'
:
'
0
'
;
}
if
(
Array
.
isArray
(
value
))
{
return
value
.
map
((
item
)
=>
normalizeArrayItem
(
item
)).
join
(
'
,
'
);
}
return
String
(
value
);
}
function
normalizeArrayItem
(
value
:
unknown
):
string
{
if
(
typeof
value
===
'
string
'
)
{
return
value
;
}
if
(
typeof
value
===
'
number
'
)
{
return
value
.
toString
();
}
if
(
typeof
value
===
'
boolean
'
)
{
return
value
?
'
1
'
:
'
0
'
;
}
return
String
(
value
);
}
function
normalizeConfigByDefaultKeys
<
T
extends
Record
<
string
,
string
>>
(
readConfig
:
Record
<
string
,
unknown
>
,
defaultConfig
:
T
,
):
Partial
<
T
>
{
const
normalizedConfig
:
Partial
<
T
>
=
{};
for
(
const
key
of
Object
.
keys
(
defaultConfig
)
as
Array
<
keyof
T
>
)
{
const
rawKey
=
key
as
string
;
const
camelKey
=
toCamelCaseKey
(
rawKey
);
const
value
=
readConfig
[
camelKey
]
!==
undefined
?
readConfig
[
camelKey
]
:
readConfig
[
rawKey
];
const
normalized
=
normalizeConfigValue
(
value
);
if
(
normalized
!==
undefined
)
{
normalizedConfig
[
key
]
=
normalized
as
T
[
typeof
key
];
}
}
return
normalizedConfig
;
}
function
parseConfigBoolean
(
value
:
unknown
,
defaultValue
=
false
):
boolean
{
if
(
typeof
value
===
'
boolean
'
)
{
return
value
;
}
if
(
typeof
value
===
'
number
'
)
{
return
value
!==
0
;
}
if
(
typeof
value
===
'
string
'
)
{
const
normalized
=
value
.
trim
().
toLowerCase
();
if
(
defaultValue
)
{
return
!
(
normalized
===
'
0
'
||
normalized
===
'
false
'
||
normalized
===
'
null
'
);
}
return
!
(
normalized
===
''
||
normalized
===
'
0
'
||
normalized
===
'
false
'
||
normalized
===
'
null
'
);
}
if
(
value
==
null
)
{
return
defaultValue
;
}
return
Boolean
(
value
);
}
function
convertStringArray
(
str
:
string
):
string
[]
{
return
(
str
?.
split
(
'
,
'
)
.
map
((
s
)
=>
s
.
trim
())
.
filter
((
s
)
=>
s
)
||
[]
);
}
function
convertIntArray
(
str
:
string
):
number
[]
{
return
(
str
?.
split
(
'
,
'
)
.
map
((
s
)
=>
parseInt
(
s
.
trim
()))
.
filter
((
n
)
=>
!
isNaN
(
n
))
||
[]
);
}
function
convertFloatArray
(
str
:
string
):
number
[]
{
return
(
str
?.
split
(
'
,
'
)
.
map
((
s
)
=>
parseFloat
(
s
.
trim
()))
.
filter
((
n
)
=>
!
isNaN
(
n
))
||
[]
);
}
function
convertBooleanArray
(
str
:
string
,
defaultValue
=
false
):
boolean
[]
{
return
(
str
?.
split
(
'
,
'
)
.
map
((
s
)
=>
parseConfigBoolean
(
s
.
trim
(),
defaultValue
))
.
filter
((
item
)
=>
typeof
item
===
'
boolean
'
)
||
[]
);
}
function
toTypedValue
(
value
:
string
):
string
|
number
{
const
trimmed
=
value
.
trim
();
if
(
/^
\d
+$/
.
test
(
trimmed
))
{
return
Number
.
parseInt
(
trimmed
,
10
);
}
return
trimmed
;
}
src/configurer/index.ts
0 → 100644
View file @
7448d3f4
export
*
from
'
./configurer
'
;
tests/configurer.spec.ts
0 → 100644
View file @
7448d3f4
import
{
Configurer
}
from
'
../src/configurer/configurer
'
;
type
TestConfig
=
{
HOST
:
string
;
PORT
:
string
;
ENABLE_RECONNECT
:
string
;
NO_CONNECT_COUNT_LIMIT
:
string
;
ALT_VERSIONS
:
string
;
FLOAT_VALUES
:
string
;
BOOL_VALUES
:
string
;
};
const
defaultConfig
:
TestConfig
=
{
HOST
:
'
::
'
,
PORT
:
'
7911
'
,
ENABLE_RECONNECT
:
'
1
'
,
NO_CONNECT_COUNT_LIMIT
:
''
,
ALT_VERSIONS
:
'
2330,2331
'
,
FLOAT_VALUES
:
'
1.5,2.75
'
,
BOOL_VALUES
:
'
1,0,true,false,null
'
,
};
describe
(
'
Configurer
'
,
()
=>
{
test
(
'
loadConfig merges with priority: default < obj < env
'
,
()
=>
{
const
configurer
=
new
Configurer
(
defaultConfig
);
const
instance
=
configurer
.
loadConfig
({
obj
:
{
host
:
'
0.0.0.0
'
,
PORT
:
9000
,
},
env
:
{
PORT
:
'
10000
'
,
},
});
expect
(
instance
.
getString
(
'
HOST
'
)).
toBe
(
'
0.0.0.0
'
);
expect
(
instance
.
getString
(
'
PORT
'
)).
toBe
(
'
10000
'
);
});
test
(
'
getBoolean uses boolean default parsed from default config with defaultValue=false
'
,
()
=>
{
const
configurer
=
new
Configurer
(
defaultConfig
);
const
withEmptyReconnect
=
configurer
.
loadConfig
({
env
:
{
ENABLE_RECONNECT
:
''
,
},
});
expect
(
withEmptyReconnect
.
getBoolean
(
'
ENABLE_RECONNECT
'
)).
toBe
(
true
);
const
withEmptyCountLimit
=
configurer
.
loadConfig
({
env
:
{
NO_CONNECT_COUNT_LIMIT
:
''
,
},
});
expect
(
withEmptyCountLimit
.
getBoolean
(
'
NO_CONNECT_COUNT_LIMIT
'
)).
toBe
(
false
,
);
});
test
(
'
getInt/getFloat/get arrays parse from getString
'
,
()
=>
{
const
configurer
=
new
Configurer
(
defaultConfig
);
const
instance
=
configurer
.
loadConfig
({
env
:
{
PORT
:
'
8080
'
,
ALT_VERSIONS
:
'
3000,abc,3002
'
,
FLOAT_VALUES
:
'
3.5,NaN,4.25
'
,
BOOL_VALUES
:
'
1,0,true,false,,null
'
,
},
});
expect
(
instance
.
getInt
(
'
PORT
'
)).
toBe
(
8080
);
expect
(
instance
.
getFloat
(
'
PORT
'
)).
toBe
(
8080
);
expect
(
instance
.
getStringArray
(
'
ALT_VERSIONS
'
)).
toEqual
([
'
3000
'
,
'
abc
'
,
'
3002
'
,
]);
expect
(
instance
.
getIntArray
(
'
ALT_VERSIONS
'
)).
toEqual
([
3000
,
3002
]);
expect
(
instance
.
getFloatArray
(
'
FLOAT_VALUES
'
)).
toEqual
([
3.5
,
4.25
]);
expect
(
instance
.
getBooleanArray
(
'
BOOL_VALUES
'
)).
toEqual
([
true
,
false
,
true
,
false
,
true
,
false
,
]);
});
test
(
'
generateExampleObject converts key to camelCase and parses number/array values
'
,
()
=>
{
const
configurer
=
new
Configurer
(
defaultConfig
);
const
example
=
configurer
.
generateExampleObject
();
expect
(
example
.
host
).
toBe
(
'
::
'
);
expect
(
example
.
port
).
toBe
(
7911
);
expect
(
example
.
altVersions
).
toEqual
([
2330
,
2331
]);
expect
(
example
.
floatValues
).
toEqual
([
'
1.5
'
,
'
2.75
'
]);
});
});
tsconfig.json
View file @
7448d3f4
...
...
@@ -7,7 +7,8 @@
"emitDecoratorMetadata"
:
true
,
"experimentalDecorators"
:
true
,
"declaration"
:
true
,
"sourceMap"
:
true
"sourceMap"
:
true
,
"types"
:
[
"jest"
]
},
"compileOnSave"
:
true
,
"allowJs"
:
true
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment