Make Properties class

jh-changes
Shin'ya UEOKA 5 years ago
parent 2116ac90a6
commit 574692551a
  1. 4
      src/background/repositories/SettingRepository.ts
  2. 4
      src/background/usecases/CompletionsUseCase.ts
  3. 4
      src/background/usecases/parsers.ts
  4. 6
      src/settings/components/form/PropertiesForm.tsx
  5. 13
      src/settings/components/index.tsx
  6. 15
      src/shared/SettingData.ts
  7. 37
      src/shared/Settings.ts
  8. 50
      src/shared/properties.ts
  9. 56
      src/shared/property-defs.ts
  10. 110
      src/shared/settings/Properties.ts
  11. 4
      test/settings/components/form/PropertiesForm.test.tsx
  12. 15
      test/shared/SettingData.test.ts
  13. 31
      test/shared/Settings.test.ts
  14. 18
      test/shared/properties.test.js
  15. 18
      test/shared/property-defs.test.js
  16. 30
      test/shared/settings/Properties.test.ts

@ -1,7 +1,7 @@
import { injectable } from 'tsyringe'; import { injectable } from 'tsyringe';
import MemoryStorage from '../infrastructures/MemoryStorage'; import MemoryStorage from '../infrastructures/MemoryStorage';
import Settings, { valueOf, toJSON } from '../../shared/Settings'; import Settings, { valueOf, toJSON } from '../../shared/Settings';
import * as PropertyDefs from '../../shared/property-defs'; import Properties from '../../shared/settings/Properties';
const CACHED_SETTING_KEY = 'setting'; const CACHED_SETTING_KEY = 'setting';
@ -26,7 +26,7 @@ export default class SettingRepository {
async setProperty( async setProperty(
name: string, value: string | number | boolean, name: string, value: string | number | boolean,
): Promise<void> { ): Promise<void> {
let def = PropertyDefs.defs.find(d => name === d.name); let def = Properties.def(name);
if (!def) { if (!def) {
throw new Error('unknown property: ' + name); throw new Error('unknown property: ' + name);
} }

@ -5,7 +5,7 @@ import CompletionsRepository from '../repositories/CompletionsRepository';
import * as filters from './filters'; import * as filters from './filters';
import SettingRepository from '../repositories/SettingRepository'; import SettingRepository from '../repositories/SettingRepository';
import TabPresenter from '../presenters/TabPresenter'; import TabPresenter from '../presenters/TabPresenter';
import * as PropertyDefs from '../../shared/property-defs'; import Properties from '../../shared/settings/Properties';
const COMPLETION_ITEM_LIMIT = 10; const COMPLETION_ITEM_LIMIT = 10;
@ -129,7 +129,7 @@ export default class CompletionsUseCase {
} }
querySet(name: string, keywords: string): Promise<CompletionGroup[]> { querySet(name: string, keywords: string): Promise<CompletionGroup[]> {
let items = PropertyDefs.defs.map((def) => { let items = Properties.defs().map((def) => {
if (def.type === 'boolean') { if (def.type === 'boolean') {
return [ return [
{ {

@ -1,4 +1,4 @@
import * as PropertyDefs from '../../shared//property-defs'; import Properties from '../../shared/settings/Properties';
const mustNumber = (v: any): number => { const mustNumber = (v: any): number => {
let num = Number(v); let num = Number(v);
@ -16,7 +16,7 @@ const parseSetOption = (
value = !key.startsWith('no'); value = !key.startsWith('no');
key = value ? key : key.slice(2); key = value ? key : key.slice(2);
} }
let def = PropertyDefs.defs.find(d => d.name === key); let def = Properties.def(key);
if (!def) { if (!def) {
throw new Error('Unknown property: ' + key); throw new Error('Unknown property: ' + key);
} }

@ -18,7 +18,7 @@ class PropertiesForm extends React.Component<Props> {
render() { render() {
let types = this.props.types; let types = this.props.types;
let value = this.props.value; let values = this.props.value;
return <div className='form-properties-form'> return <div className='form-properties-form'>
{ {
@ -46,10 +46,10 @@ class PropertiesForm extends React.Component<Props> {
<span className='column-name'>{name}</span> <span className='column-name'>{name}</span>
<input type={inputType} name={name} <input type={inputType} name={name}
className='column-input' className='column-input'
value={value[name] ? value[name] : ''} value={values[name] ? values[name] : ''}
onChange={onChange} onChange={onChange}
onBlur={this.props.onBlur} onBlur={this.props.onBlur}
checked={value[name]} checked={values[name]}
/> />
</label> </label>
</div>; </div>;

@ -8,11 +8,11 @@ import BlacklistForm from './form/BlacklistForm';
import PropertiesForm from './form/PropertiesForm'; import PropertiesForm from './form/PropertiesForm';
import * as settingActions from '../../settings/actions/setting'; import * as settingActions from '../../settings/actions/setting';
import SettingData, { import SettingData, {
JSONTextSettings, FormKeymaps, FormSearch, FormSettings, FormKeymaps, FormSearch, FormSettings, JSONTextSettings,
} from '../../shared/SettingData'; } from '../../shared/SettingData';
import { State as AppState } from '../reducers/setting'; import { State as AppState } from '../reducers/setting';
import * as settings from '../../shared/Settings'; import * as settings from '../../shared/Settings';
import * as PropertyDefs from '../../shared/property-defs'; import Properties from '../../shared/settings/Properties';
const DO_YOU_WANT_TO_CONTINUE = const DO_YOU_WANT_TO_CONTINUE =
'Some settings in JSON can be lost when migrating. ' + 'Some settings in JSON can be lost when migrating. ' +
@ -33,11 +33,6 @@ class SettingsComponent extends React.Component<Props> {
} }
renderFormFields(form: any) { renderFormFields(form: any) {
let types = PropertyDefs.defs.reduce(
(o: {[key: string]: string}, def) => {
o[def.name] = def.type;
return o;
}, {});
return <div> return <div>
<fieldset> <fieldset>
<legend>Keybindings</legend> <legend>Keybindings</legend>
@ -66,7 +61,7 @@ class SettingsComponent extends React.Component<Props> {
<fieldset> <fieldset>
<legend>Properties</legend> <legend>Properties</legend>
<PropertiesForm <PropertiesForm
types={types} types={Properties.types()}
value={form.properties} value={form.properties}
onChange={this.bindPropertiesForm.bind(this)} onChange={this.bindPropertiesForm.bind(this)}
onBlur={this.save.bind(this)} onBlur={this.save.bind(this)}
@ -157,7 +152,7 @@ class SettingsComponent extends React.Component<Props> {
let data = new SettingData({ let data = new SettingData({
source: this.props.source, source: this.props.source,
form: (this.props.form as FormSettings).buildWithProperties( form: (this.props.form as FormSettings).buildWithProperties(
settings.propertiesValueOf(value)), Properties.fromJSON(value))
}); });
this.props.dispatch(settingActions.set(data)); this.props.dispatch(settingActions.set(data));
} }

@ -2,6 +2,7 @@ import * as operations from './operations';
import Settings, * as settings from './Settings'; import Settings, * as settings from './Settings';
import Keymaps from './settings/Keymaps'; import Keymaps from './settings/Keymaps';
import Search from './settings/Search'; import Search from './settings/Search';
import Properties from './settings/Properties';
export class FormKeymaps { export class FormKeymaps {
private data: {[op: string]: string}; private data: {[op: string]: string};
@ -143,14 +144,14 @@ export class FormSettings {
private search: FormSearch; private search: FormSearch;
private properties: settings.Properties; private properties: Properties;
private blacklist: string[]; private blacklist: string[];
constructor( constructor(
keymaps: FormKeymaps, keymaps: FormKeymaps,
search: FormSearch, search: FormSearch,
properties: settings.Properties, properties: Properties,
blacklist: string[], blacklist: string[],
) { ) {
this.keymaps = keymaps; this.keymaps = keymaps;
@ -177,7 +178,7 @@ export class FormSettings {
); );
} }
buildWithProperties(props: settings.Properties): FormSettings { buildWithProperties(props: Properties): FormSettings {
return new FormSettings( return new FormSettings(
this.keymaps, this.keymaps,
this.search, this.search,
@ -199,7 +200,7 @@ export class FormSettings {
return settings.valueOf({ return settings.valueOf({
keymaps: this.keymaps.toKeymaps().toJSON(), keymaps: this.keymaps.toKeymaps().toJSON(),
search: this.search.toSearchSettings().toJSON(), search: this.search.toSearchSettings().toJSON(),
properties: this.properties, properties: this.properties.toJSON(),
blacklist: this.blacklist, blacklist: this.blacklist,
}); });
} }
@ -207,13 +208,13 @@ export class FormSettings {
toJSON(): { toJSON(): {
keymaps: ReturnType<FormKeymaps['toJSON']>; keymaps: ReturnType<FormKeymaps['toJSON']>;
search: ReturnType<FormSearch['toJSON']>; search: ReturnType<FormSearch['toJSON']>;
properties: settings.Properties; properties: ReturnType<Properties['toJSON']>;
blacklist: string[]; blacklist: string[];
} { } {
return { return {
keymaps: this.keymaps.toJSON(), keymaps: this.keymaps.toJSON(),
search: this.search.toJSON(), search: this.search.toJSON(),
properties: this.properties, properties: this.properties.toJSON(),
blacklist: this.blacklist, blacklist: this.blacklist,
}; };
} }
@ -227,7 +228,7 @@ export class FormSettings {
return new FormSettings( return new FormSettings(
FormKeymaps.valueOf(o.keymaps), FormKeymaps.valueOf(o.keymaps),
FormSearch.valueOf(o.search), FormSearch.valueOf(o.search),
settings.propertiesValueOf(o.properties), Properties.fromJSON(o.properties),
settings.blacklistValueOf(o.blacklist), settings.blacklistValueOf(o.blacklist),
); );
} }

@ -1,12 +1,6 @@
import * as PropertyDefs from './property-defs';
import Keymaps from './settings/Keymaps'; import Keymaps from './settings/Keymaps';
import Search from './settings/Search'; import Search from './settings/Search';
import Properties from './settings/Properties';
export interface Properties {
hintchars: string;
smoothscroll: boolean;
complete: string;
}
export default interface Settings { export default interface Settings {
keymaps: Keymaps; keymaps: Keymaps;
@ -15,27 +9,6 @@ export default interface Settings {
blacklist: string[]; blacklist: string[];
} }
export const propertiesValueOf = (o: any): Properties => {
let defNames = new Set(PropertyDefs.defs.map(def => def.name));
let unknownName = Object.keys(o).find(name => !defNames.has(name));
if (unknownName) {
throw new TypeError(`Unknown property name: "${unknownName}"`);
}
for (let def of PropertyDefs.defs) {
if (!Object.prototype.hasOwnProperty.call(o, def.name)) {
continue;
}
if (typeof o[def.name] !== def.type) {
throw new TypeError(`property "${def.name}" is not ${def.type}`);
}
}
return {
...PropertyDefs.defaultValues,
...o,
};
};
export const blacklistValueOf = (o: any): string[] => { export const blacklistValueOf = (o: any): string[] => {
if (!Array.isArray(o)) { if (!Array.isArray(o)) {
throw new TypeError(`"blacklist" is not an array of string`); throw new TypeError(`"blacklist" is not an array of string`);
@ -59,7 +32,7 @@ export const valueOf = (o: any): Settings => {
settings.search = Search.fromJSON(o.search); settings.search = Search.fromJSON(o.search);
break; break;
case 'properties': case 'properties':
settings.properties = propertiesValueOf(o.properties); settings.properties = Properties.fromJSON(o.properties);
break; break;
case 'blacklist': case 'blacklist':
settings.blacklist = blacklistValueOf(o.blacklist); settings.blacklist = blacklistValueOf(o.blacklist);
@ -75,7 +48,7 @@ export const toJSON = (settings: Settings): any => {
return { return {
keymaps: settings.keymaps.toJSON(), keymaps: settings.keymaps.toJSON(),
search: settings.search.toJSON(), search: settings.search.toJSON(),
properties: settings.properties, properties: settings.properties.toJSON(),
blacklist: settings.blacklist, blacklist: settings.blacklist,
}; };
}; };
@ -156,10 +129,10 @@ export const DefaultSetting: Settings = {
'wikipedia': 'https://en.wikipedia.org/w/index.php?search={}' 'wikipedia': 'https://en.wikipedia.org/w/index.php?search={}'
} }
}), }),
properties: { properties: Properties.fromJSON({
hintchars: 'abcdefghijklmnopqrstuvwxyz', hintchars: 'abcdefghijklmnopqrstuvwxyz',
smoothscroll: false, smoothscroll: false,
complete: 'sbh' complete: 'sbh'
}, }),
blacklist: [] blacklist: []
}; };

@ -1,50 +0,0 @@
export type Type = string | number | boolean;
export class Def {
private name0: string;
private description0: string;
private defaultValue0: Type;
constructor(
name: string,
description: string,
defaultValue: Type,
) {
this.name0 = name;
this.description0 = description;
this.defaultValue0 = defaultValue;
}
public get name(): string {
return this.name0;
}
public get defaultValue(): Type {
return this.defaultValue0;
}
public get description(): Type {
return this.description0;
}
public get type(): string {
return typeof this.defaultValue;
}
}
export const defs: Def[] = [
new Def(
'hintchars',
'hint characters on follow mode',
'abcdefghijklmnopqrstuvwxyz'),
new Def(
'smoothscroll',
'smooth scroll',
false),
new Def(
'complete',
'which are completed at the open page',
'sbh'),
];

@ -1,56 +0,0 @@
export type Type = string | number | boolean;
export class Def {
private name0: string;
private description0: string;
private defaultValue0: Type;
constructor(
name: string,
description: string,
defaultValue: Type,
) {
this.name0 = name;
this.description0 = description;
this.defaultValue0 = defaultValue;
}
public get name(): string {
return this.name0;
}
public get defaultValue(): Type {
return this.defaultValue0;
}
public get description(): Type {
return this.description0;
}
public get type(): string {
return typeof this.defaultValue;
}
}
export const defs: Def[] = [
new Def(
'hintchars',
'hint characters on follow mode',
'abcdefghijklmnopqrstuvwxyz'),
new Def(
'smoothscroll',
'smooth scroll',
false),
new Def(
'complete',
'which are completed at the open page',
'sbh'),
];
export const defaultValues = {
hintchars: 'abcdefghijklmnopqrstuvwxyz',
smoothscroll: false,
complete: 'sbh',
};

@ -0,0 +1,110 @@
export type PropertiesJSON = {
hintchars?: string;
smoothscroll?: boolean;
complete?: string;
};
export type PropertyTypes = {
hintchars: string;
smoothscroll: string;
complete: string;
};
type PropertyName = 'hintchars' | 'smoothscroll' | 'complete';
type PropertyDef = {
name: PropertyName;
description: string;
defaultValue: string | number | boolean;
type: 'string' | 'number' | 'boolean';
};
const defs: PropertyDef[] = [
{
name: 'hintchars',
description: 'hint characters on follow mode',
defaultValue: 'abcdefghijklmnopqrstuvwxyz',
type: 'string',
}, {
name: 'smoothscroll',
description: 'smooth scroll',
defaultValue: false,
type: 'boolean',
}, {
name: 'complete',
description: 'which are completed at the open page',
defaultValue: 'sbh',
type: 'string',
}
];
const defaultValues = {
hintchars: 'abcdefghijklmnopqrstuvwxyz',
smoothscroll: false,
complete: 'sbh',
};
export default class Properties {
public hintchars: string;
public smoothscroll: boolean;
public complete: string;
constructor({
hintchars,
smoothscroll,
complete,
}: {
hintchars?: string;
smoothscroll?: boolean;
complete?: string;
}) {
this.hintchars = hintchars || defaultValues.hintchars;
this.smoothscroll = smoothscroll || defaultValues.smoothscroll;
this.complete = complete || defaultValues.complete;
}
static fromJSON(json: any): Properties {
let defNames: Set<string> = new Set(defs.map(def => def.name));
let unknownName = Object.keys(json).find(name => !defNames.has(name));
if (unknownName) {
throw new TypeError(`Unknown property name: "${unknownName}"`);
}
for (let def of defs) {
if (!Object.prototype.hasOwnProperty.call(json, def.name)) {
continue;
}
if (typeof json[def.name] !== def.type) {
throw new TypeError(
`property "${def.name}" is not ${def.type}`);
}
}
return new Properties(json);
}
static types(): PropertyTypes {
return {
hintchars: 'string',
smoothscroll: 'boolean',
complete: 'string',
};
}
static def(name: string): PropertyDef | undefined {
return defs.find(p => p.name === name);
}
static defs(): PropertyDef[] {
return defs;
}
toJSON(): PropertiesJSON {
return {
hintchars: this.hintchars,
smoothscroll: this.smoothscroll,
complete: this.complete,
};
}
}

@ -13,14 +13,14 @@ describe("settings/form/PropertiesForm", () => {
mybool: 'boolean', mybool: 'boolean',
empty: 'string', empty: 'string',
} }
let value = { let values = {
mystr: 'abc', mystr: 'abc',
mynum: 123, mynum: 123,
mybool: true, mybool: true,
}; };
let root = ReactTestRenderer.create( let root = ReactTestRenderer.create(
<PropertiesForm types={types} value={value} />, <PropertiesForm types={types} value={values} />,
).root ).root
let input = root.findByProps({ name: 'mystr' }); let input = root.findByProps({ name: 'mystr' });

@ -5,6 +5,7 @@ import Settings from '../../src/shared/Settings';
import { expect } from 'chai'; import { expect } from 'chai';
import Keymaps from '../../src/shared/settings/Keymaps'; import Keymaps from '../../src/shared/settings/Keymaps';
import Search from '../../src/shared/settings/Search'; import Search from '../../src/shared/settings/Search';
import Properties from '../../src/shared/settings/Properties';
describe('shared/SettingData', () => { describe('shared/SettingData', () => {
describe('FormKeymaps', () => { describe('FormKeymaps', () => {
@ -62,7 +63,7 @@ describe('shared/SettingData', () => {
expect({ expect({
keymaps: settings.keymaps.toJSON(), keymaps: settings.keymaps.toJSON(),
search: settings.search.toJSON(), search: settings.search.toJSON(),
properties: settings.properties, properties: settings.properties.toJSON(),
blacklist: settings.blacklist, blacklist: settings.blacklist,
}).to.deep.equal(JSON.parse(o)); }).to.deep.equal(JSON.parse(o));
}); });
@ -78,11 +79,11 @@ describe('shared/SettingData', () => {
google: "https://google.com/search?q={}", google: "https://google.com/search?q={}",
}, },
}), }),
properties: { properties: Properties.fromJSON({
hintchars: "abcdefghijklmnopqrstuvwxyz", hintchars: "abcdefghijklmnopqrstuvwxyz",
smoothscroll: false, smoothscroll: false,
complete: "sbh" complete: "sbh"
}, }),
blacklist: [], blacklist: [],
}; };
@ -90,7 +91,7 @@ describe('shared/SettingData', () => {
expect(JSON.parse(json)).to.deep.equal({ expect(JSON.parse(json)).to.deep.equal({
keymaps: o.keymaps.toJSON(), keymaps: o.keymaps.toJSON(),
search: o.search.toJSON(), search: o.search.toJSON(),
properties: o.properties, properties: o.properties.toJSON(),
blacklist: o.blacklist, blacklist: o.blacklist,
}); });
}); });
@ -123,7 +124,7 @@ describe('shared/SettingData', () => {
expect({ expect({
keymaps: settings.keymaps.toJSON(), keymaps: settings.keymaps.toJSON(),
search: settings.search.toJSON(), search: settings.search.toJSON(),
properties: settings.properties, properties: settings.properties.toJSON(),
blacklist: settings.blacklist, blacklist: settings.blacklist,
}).to.deep.equal({ }).to.deep.equal({
keymaps: { keymaps: {
@ -159,11 +160,11 @@ describe('shared/SettingData', () => {
"google": "https://google.com/search?q={}" "google": "https://google.com/search?q={}"
} }
}), }),
properties: { properties: Properties.fromJSON({
hintchars: "abcdefghijklmnopqrstuvwxyz", hintchars: "abcdefghijklmnopqrstuvwxyz",
smoothscroll: false, smoothscroll: false,
complete: "sbh" complete: "sbh"
}, }),
blacklist: [] blacklist: []
}; };

@ -1,33 +1,8 @@
import * as settings from '../../src/shared/Settings'; import * as settings from '../../src/shared/Settings';
import { expect } from 'chai'; import {expect} from 'chai';
describe('Settings', () => { describe('Settings', () => {
describe('#propertiesValueOf', () => {
it('returns with default properties by empty settings', () => {
let props = settings.propertiesValueOf({});
expect(props).to.deep.equal({
hintchars: "abcdefghijklmnopqrstuvwxyz",
smoothscroll: false,
complete: "sbh"
})
});
it('returns properties by valid settings', () => {
let props = settings.propertiesValueOf({
hintchars: "abcdefgh",
smoothscroll: false,
complete: "sbh"
});
expect(props).to.deep.equal({
hintchars: "abcdefgh",
smoothscroll: false,
complete: "sbh"
});
});
});
describe('#blacklistValueOf', () => { describe('#blacklistValueOf', () => {
it('returns empty array by empty settings', () => { it('returns empty array by empty settings', () => {
let blacklist = settings.blacklistValueOf([]); let blacklist = settings.blacklistValueOf([]);
@ -70,7 +45,7 @@ describe('Settings', () => {
expect({ expect({
keymaps: x.keymaps.toJSON(), keymaps: x.keymaps.toJSON(),
search: x.search.toJSON(), search: x.search.toJSON(),
properties: x.properties, properties: x.properties.toJSON(),
blacklist: x.blacklist, blacklist: x.blacklist,
}).to.deep.equal({ }).to.deep.equal({
keymaps: {}, keymaps: {},
@ -92,7 +67,7 @@ describe('Settings', () => {
it('sets default settings', () => { it('sets default settings', () => {
let value = settings.valueOf({}); let value = settings.valueOf({});
expect(value.keymaps.toJSON()).to.not.be.empty; expect(value.keymaps.toJSON()).to.not.be.empty;
expect(value.properties).to.not.be.empty; expect(value.properties.toJSON()).to.not.be.empty;
expect(value.search.defaultEngine).to.be.a('string'); expect(value.search.defaultEngine).to.be.a('string');
expect(value.search.engines).to.be.an('object'); expect(value.search.engines).to.be.an('object');
expect(value.blacklist).to.be.empty; expect(value.blacklist).to.be.empty;

@ -1,18 +0,0 @@
import * as settings from 'shared/settings';
describe('properties', () => {
describe('Def class', () => {
it('returns property definitions', () => {
let def = new proerties.Def(
'smoothscroll',
'smooth scroll',
false);
expect(def.name).to.equal('smoothscroll');
expect(def.describe).to.equal('smooth scroll');
expect(def.defaultValue).to.equal(false);
expect(def.type).to.equal('boolean');
});
});
});

@ -1,18 +0,0 @@
import * as settings from 'shared/settings';
describe('properties', () => {
describe('Def class', () => {
it('returns property definitions', () => {
let def = new proerties.Def(
'smoothscroll',
'smooth scroll',
false);
expect(def.name).to.equal('smoothscroll');
expect(def.describe).to.equal('smooth scroll');
expect(def.defaultValue).to.equal(false);
expect(def.type).to.equal('boolean');
});
});
});

@ -0,0 +1,30 @@
import Properties from '../../../src/shared/settings/Properties';
import { expect } from 'chai';
describe('Properties', () => {
describe('#propertiesValueOf', () => {
it('returns with default properties by empty settings', () => {
let props = Properties.fromJSON({});
expect(props).to.deep.equal({
hintchars: "abcdefghijklmnopqrstuvwxyz",
smoothscroll: false,
complete: "sbh"
})
});
it('returns properties by valid settings', () => {
let props = Properties.fromJSON({
hintchars: "abcdefgh",
smoothscroll: false,
complete: "sbh"
});
expect(props).to.deep.equal({
hintchars: "abcdefgh",
smoothscroll: false,
complete: "sbh"
});
});
});
});