-
-
Notifications
You must be signed in to change notification settings - Fork 1
feat: 設定情報をファイルから読むconfigパッケージを実装 #1530
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
Merged
Merged
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { describe, expect, it } from 'vitest'; | ||
|
|
||
| import { Config } from '../model/config.js'; | ||
| import { DummyConfigStore } from './dummy.js'; | ||
|
|
||
| const testConfig = Config.new({ | ||
| instanceName: 'Pulsate Demo Server', | ||
| instanceFqdn: 'demo.pulsate.dev', | ||
| openRegistration: true, | ||
| maintainerAccount: '@pulsateprj@demo.pulsate.dev', | ||
| maintainerEmail: 'contact@pulsate.dev', | ||
| }); | ||
|
|
||
| describe('DummyConfigStore', () => { | ||
| it('should return the config', () => { | ||
| const store = new DummyConfigStore(testConfig); | ||
| const config = store.fetch(); | ||
|
|
||
| expect(config.getInstanceName()).toBe('Pulsate Demo Server'); | ||
| expect(config.getInstanceFqdn()).toBe('demo.pulsate.dev'); | ||
| expect(config.isOpenRegistration()).toBe(true); | ||
| expect(config.getMaintainerAccount()).toBe('@pulsateprj@demo.pulsate.dev'); | ||
| expect(config.getMaintainerEmail()).toBe('contact@pulsate.dev'); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import { Ether } from '@mikuroxina/mini-fn'; | ||
| import { type ConfigStore, configStoreSymbol } from '../mod.js'; | ||
| import type { Config } from '../model/config.js'; | ||
|
|
||
| export class DummyConfigStore implements ConfigStore { | ||
| private readonly config: Config; | ||
|
|
||
| constructor(config: Config) { | ||
| this.config = config; | ||
| } | ||
|
|
||
| fetch(): Config { | ||
| return this.config; | ||
| } | ||
| } | ||
|
|
||
| export const dummyConfigStore = (config: Config) => | ||
| Ether.newEther(configStoreSymbol, () => new DummyConfigStore(config)); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; | ||
| import { tmpdir } from 'node:os'; | ||
| import { join } from 'node:path'; | ||
|
|
||
| import { describe, expect, it } from 'vitest'; | ||
|
|
||
| import { ConfigInvalidError } from '../model/errors.js'; | ||
| import { LocalConfigStore } from './local.js'; | ||
|
|
||
| describe('LocalConfigStore', () => { | ||
| const createTempConfig = (content: string): string => { | ||
| const dir = mkdtempSync(join(tmpdir(), 'pulsate-config-test-')); | ||
| const filePath = join(dir, 'config.yaml'); | ||
| writeFileSync(filePath, content); | ||
| return filePath; | ||
| }; | ||
|
|
||
| it('should load config from a valid YAML file', () => { | ||
| const filePath = createTempConfig(` | ||
| instance_name: "Pulsate Demo Server" | ||
| instance_fqdn: "demo.pulsate.dev" | ||
| open_registration: true | ||
| maintainer_account: "@pulsateprj@demo.pulsate.dev" | ||
| maintainer_email: "contact@pulsate.dev" | ||
| `); | ||
|
|
||
| const store = new LocalConfigStore(filePath); | ||
| const config = store.fetch(); | ||
|
|
||
| expect(config.getInstanceName()).toBe('Pulsate Demo Server'); | ||
| expect(config.getInstanceFqdn()).toBe('demo.pulsate.dev'); | ||
| expect(config.isOpenRegistration()).toBe(true); | ||
| expect(config.getMaintainerAccount()).toBe('@pulsateprj@demo.pulsate.dev'); | ||
| expect(config.getMaintainerEmail()).toBe('contact@pulsate.dev'); | ||
|
|
||
| rmSync(join(filePath, '..'), { recursive: true }); | ||
| }); | ||
|
|
||
| it('should throw when file does not exist', () => { | ||
| expect(() => new LocalConfigStore('/nonexistent/path.yaml')).toThrow(); | ||
| }); | ||
|
|
||
| it('should throw ConfigInvalidError when required field is empty', () => { | ||
| const filePath = createTempConfig(` | ||
| instance_name: "" | ||
| instance_fqdn: "demo.pulsate.dev" | ||
| open_registration: true | ||
| maintainer_account: "@pulsateprj@demo.pulsate.dev" | ||
| maintainer_email: "contact@pulsate.dev" | ||
| `); | ||
|
|
||
| expect(() => new LocalConfigStore(filePath)).toThrow(ConfigInvalidError); | ||
|
|
||
| rmSync(join(filePath, '..'), { recursive: true }); | ||
| }); | ||
|
|
||
| it('should throw when YAML is malformed', () => { | ||
| const filePath = createTempConfig('{{{invalid yaml'); | ||
|
|
||
| expect(() => new LocalConfigStore(filePath)).toThrow(); | ||
|
|
||
| rmSync(join(filePath, '..'), { recursive: true }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { readFileSync } from 'node:fs'; | ||
|
|
||
| import { Ether } from '@mikuroxina/mini-fn'; | ||
| import { parse } from 'yaml'; | ||
|
|
||
| import { type ConfigStore, configStoreSymbol } from '../mod.js'; | ||
| import { type AccountName, Config } from '../model/config.js'; | ||
|
|
||
| export class LocalConfigStore implements ConfigStore { | ||
| private readonly config: Config; | ||
|
|
||
| constructor(filePath: string) { | ||
| const content = readFileSync(filePath, 'utf-8'); | ||
| const data = parse(content); | ||
|
|
||
| this.config = Config.new({ | ||
| instanceName: data.instance_name, | ||
| instanceFqdn: data.instance_fqdn, | ||
| openRegistration: data.open_registration, | ||
| maintainerAccount: data.maintainer_account as AccountName, | ||
| maintainerEmail: data.maintainer_email, | ||
| }); | ||
| } | ||
|
|
||
| fetch(): Config { | ||
| return this.config; | ||
| } | ||
| } | ||
|
|
||
| export const localConfigStore = (filePath: string) => | ||
| Ether.newEther(configStoreSymbol, () => new LocalConfigStore(filePath)); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { Ether } from '@mikuroxina/mini-fn'; | ||
|
|
||
| import type { Config } from './model/config.js'; | ||
|
|
||
| export interface ConfigStore { | ||
| fetch(): Config; | ||
| } | ||
| export const configStoreSymbol = Ether.newEtherSymbol<ConfigStore>(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import { describe, expect, it } from 'vitest'; | ||
|
|
||
| import { Config } from './config.js'; | ||
| import { ConfigInvalidError } from './errors.js'; | ||
|
|
||
| const validArgs = { | ||
| instanceName: 'Pulsate Demo Server', | ||
| instanceFqdn: 'demo.pulsate.dev', | ||
| openRegistration: true, | ||
| maintainerAccount: '@pulsateprj@demo.pulsate.dev' as const, | ||
| maintainerEmail: 'contact@pulsate.dev', | ||
| }; | ||
|
|
||
| describe('Config', () => { | ||
| it('should create config with valid args', () => { | ||
| const config = Config.new(validArgs); | ||
|
|
||
| expect(config.getInstanceName()).toBe('Pulsate Demo Server'); | ||
| expect(config.getInstanceFqdn()).toBe('demo.pulsate.dev'); | ||
| expect(config.isOpenRegistration()).toBe(true); | ||
| expect(config.getMaintainerAccount()).toBe('@pulsateprj@demo.pulsate.dev'); | ||
| expect(config.getMaintainerEmail()).toBe('contact@pulsate.dev'); | ||
| }); | ||
|
|
||
| it('should throw when instanceName is empty', () => { | ||
| expect(() => Config.new({ ...validArgs, instanceName: '' })).toThrow( | ||
| ConfigInvalidError, | ||
| ); | ||
| }); | ||
|
|
||
| it('should throw when instanceFqdn is empty', () => { | ||
| expect(() => Config.new({ ...validArgs, instanceFqdn: '' })).toThrow( | ||
| ConfigInvalidError, | ||
| ); | ||
| }); | ||
|
|
||
| it('should throw when maintainerAccount is invalid format', () => { | ||
| expect(() => | ||
| Config.new({ ...validArgs, maintainerAccount: '@@' as const }), | ||
| ).toThrow(ConfigInvalidError); | ||
| }); | ||
|
|
||
| it('should throw when maintainerEmail is empty', () => { | ||
| expect(() => Config.new({ ...validArgs, maintainerEmail: '' })).toThrow( | ||
| ConfigInvalidError, | ||
| ); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| import { ConfigInvalidError } from './errors.js'; | ||
|
|
||
| export type AccountName = `@${string}@${string}`; | ||
|
|
||
| export interface CreateConfigArgs { | ||
| instanceName: string; | ||
| instanceFqdn: string; | ||
| openRegistration: boolean; | ||
| maintainerAccount: AccountName; | ||
| maintainerEmail: string; | ||
| } | ||
|
|
||
| export class Config { | ||
| private constructor(arg: CreateConfigArgs) { | ||
| this.instanceName = arg.instanceName; | ||
| this.instanceFqdn = arg.instanceFqdn; | ||
| this.openRegistration = arg.openRegistration; | ||
| this.maintainerAccount = arg.maintainerAccount; | ||
| this.maintainerEmail = arg.maintainerEmail; | ||
| } | ||
|
|
||
| private readonly instanceName: string; | ||
| getInstanceName(): string { | ||
| return this.instanceName; | ||
| } | ||
|
|
||
| private readonly instanceFqdn: string; | ||
| getInstanceFqdn(): string { | ||
| return this.instanceFqdn; | ||
| } | ||
|
|
||
| private readonly openRegistration: boolean; | ||
| isOpenRegistration(): boolean { | ||
| return this.openRegistration; | ||
| } | ||
|
|
||
| private readonly maintainerAccount: AccountName; | ||
| getMaintainerAccount(): AccountName { | ||
| return this.maintainerAccount; | ||
| } | ||
|
|
||
| private readonly maintainerEmail: string; | ||
| getMaintainerEmail(): string { | ||
| return this.maintainerEmail; | ||
| } | ||
|
|
||
| public static new(arg: CreateConfigArgs): Config { | ||
| if (arg.instanceName === '') { | ||
| throw new ConfigInvalidError('instanceName is required', { | ||
| cause: arg.instanceName, | ||
| }); | ||
| } | ||
| if (arg.instanceFqdn === '') { | ||
| throw new ConfigInvalidError('instanceFqdn is required', { | ||
| cause: arg.instanceFqdn, | ||
| }); | ||
| } | ||
|
|
||
| // validate maintainerAccount: must be `@user@host` with non-empty parts | ||
| const parts = arg.maintainerAccount.split('@').filter(Boolean); | ||
| if (parts.length !== 2) { | ||
| throw new ConfigInvalidError( | ||
| 'maintainerAccount must be in @user@host format', | ||
| { cause: arg.maintainerAccount }, | ||
| ); | ||
| } | ||
|
|
||
| if (arg.maintainerEmail === '') { | ||
| throw new ConfigInvalidError('maintainerEmail is required', { | ||
| cause: arg.maintainerEmail, | ||
| }); | ||
| } | ||
|
|
||
| return new Config(arg); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| export class ConfigInvalidError extends Error { | ||
| constructor(message: string, options: { cause: unknown }) { | ||
| super(message); | ||
| this.name = 'ConfigInvalidError'; | ||
| this.cause = options.cause; | ||
| } | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.