-
Notifications
You must be signed in to change notification settings - Fork 412
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
Export language-specific format functions #484
Conversation
inferrinizzard
commented
Oct 1, 2022
- moved Format config validation to separate file
- curried language-specific format functions
- update index to export only specific members
I would prefer not to have a separate function for formatting each language, but instead export the This multi-function approach has a downside that it won't work with a custom Formatter class parameter - for that one would still need to use the old I'm even thinking whether it would be good to break backwards-compatibility and no more allow I propose the following API: import { formatSql, postgresql } from 'sql-formatter';
formatSql("SELECT *", { language: postgresql }); That includes:
|
There's one issue though with my proposed API. How to handle the default I see 2 possible approaches:
|
I'm not sure if I love exporting the XFormatter classes since the user could easily just do I left the current classic |
I wouldn't really worry about that. There's nearly always a possibility to shoot yourself in the foot by using some private API. Like, we currently expose the base However, on second thought, there might be further merits on this multiple-functions approach... let me think... |
Thinking out loud... There's currently a design problem with these For start it would be nice to store the configuration of each dialect as just data. For example: export const postgresql: DialectConfig = {
tokenizerConfig: {
reservedClauses,
reservedKeywords,
...
},
formatConfig: {
alwaysDenseOperators: ['::']
},
}; We could then use this to perform an initialization inside a higher order function (like your import { postgresql } from "src/languages/postgresql/postgresql.formatter";
import { sqlite } from "src/languages/postgresql/sqlite.formatter";
export function createFormatter(dialect: DialectConfig): FormatFn {
const tokenizer = new Tokenizer(dialect.tokenizerConfig);
return (sql: string, options: FormatOptions) => {
return new Formatter(tokenizer, dialect.formatConfig).format(sql, options);
};
}
export const formatPostgresql = createFormatter(postgresql);
export const formatSqlite = createFormatter(sqlite); This code is much nicer then our current tokenizer caching logic. However, it still has a problem - these expensive tokenizer instance are created even when the It would work if we would force user to always call import { createFormatter, postgresql } from "sql-formatter";
const format = createFormatter(postgresql);
format("SELECT *"); But with that solution we're simply pushing the burden on the user of the library. He has the option to use it well, but has even more opportunities to shoot himself in the foot. Like he could call in a loop So we really have to perform this caching of tokenizer creation inside the library itself. We could do this by caching the tokenizers inside a static Map object: class TokenizerFactory {
private map = new Map<DialectConfig, Tokenizer>();
create(dialect: DialectConfig) {
if (!this.map.has(dialect)) {
this.map.set(dialect, new Tokenizer(dialect));
}
return this.map.get(dialect);
}
}
const tokenizerFactory = new TokenizerFactory();
export const createFormatter = (dialect: DialectConfig) => (sql: string, options: FormatOptions) => {
return new Formatter(tokenizerFactory.create(dialect)).format(sql, options, dialect.formatConfig);
} But here we're pretty much back at the start with regards to the public API. We can implement it either as separate functions or as one function that takes |
Thinking further over which kind of API would be better to expose... I'm still leaning towards my initial proposal. It seems like a more flexible option to me. Especially when we also decouple these dialect configs from Formatter class (as I described above). Then one can use the same function to accomplish several things:
This added flexibility has of course downsides too:
|
Another concern with the separate function based approach is that it's more inconvenient when trying to parameterize the specific dialect. With language-parameter you simply need to list the dialects inside a mapping object: import { formatSql, sqlite, postgresql, transactsql } from "sql-formatter";
const languageMap = {
sqlite,
postgresql,
transactsql,
};
export function format(code: string, lang: keyof typeof languageMap) {
return format(code, { language: languageMap[lang] });
} With separate functions, you'll have to list name-function pairs, which is a bit more tedious: import { formatSqlite, formatPostgresql, formatTransactsql } from "sql-formatter";
const formatterMap = {
sqlite: formatSqlite,
postgresql: formatPostgresql,
transactsql: formatTransactsql,
};
export function format(code: string, lang: keyof typeof formatterMap) {
return formatterMap[lang](code);
} It's a minor difference though, and me preferring the first version might just be my personal stylistic choice. |
I'll close this, as it's no more relevant after merging of #511 |