Skip to content

Commit

Permalink
Export schemas (#428)
Browse files Browse the repository at this point in the history
* feat (MainMenu): Add logic for enable/disable model definitions menu options

* style (MainMenu): Lint

* feat (MainMenu): Fix ts error

* Add 'Save model definitions' logic
*Enable/Disable when a realm browser window gains/loses focus
*Export JS
*Export swift

* feat (RealmBrowserContainer): Improve electron import

* feat (schema-export): Remove TS errors and refactor structure

* feat (RealmBrowserContainer): Unify export schema functions

* refactor (MainMenu): Call new exportSchema function

* refactor (MainMenu): Use curly-brackets in getMenuItem

* refactor (MainMenu): Rename enableModelDefinitions to enableExportSchemaOption

* refactor (schemaExporter): Now is typed as abstract

* refactor (schemaExporter): Now is typed as abstract

* refactor (schema-export): Simplify schema-export file structure

* feat (schema-export): Add coverage for JS and Swift

* feat (RealmBrowserContainer): Move electron dialog logic to MainMenu

* feat (WindowsManager): createWindow now can receive callbacks for events

* Update c# expected output

* feat (schema-export): Use models/sample instead of demo-model object (+1 squashed commit)
Squashed commits:
[31bcabb] feat (schema-export): Use models/sample instead of demo-model object

* feat (schema-export): Remove src level in the files hierarchy

* feat (WindowManager): Rename callbackEvent to eventListenerCallbacks and add IEventListenerCallbacks

* refactor (RealmBrowserContainer): Move show dialog logic to RealmBrowserContainer

* add Java schema exporter

* add formatted code

* Update README, merged origin

* add missing file

* refactor (MainMenu): Use exportSchema function for Java

* fixing tests

* automatic formatted code

* Reverted the eventListenerCallbacks

I figured that these should have been in the options object - but it
doesn't feel practical

* Taking a more functional approach on the menu updating

I believe mutating the menu items are generally disincuraged

* Using enum values that are suitable for displaying

* Got a TS error on this line

* Registering the listener on mount + removing it on unmount

* Adding a message + a defaultPath to the export schema save dialog
  • Loading branch information
IvanCoronado authored and kraenhansen committed Nov 23, 2017
1 parent 63734f7 commit f52def5
Show file tree
Hide file tree
Showing 29 changed files with 1,727 additions and 8 deletions.
21 changes: 20 additions & 1 deletion src/main/Application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,25 @@ export class Application {
WindowType.RealmBrowser,
options,
);

window.on('blur', () => {
this.mainMenu.update({
enableExportSchema: false,
});
});

window.on('focus', () => {
this.mainMenu.update({
enableExportSchema: true,
});
});

window.on('closed', () => {
this.mainMenu.update({
enableExportSchema: false,
});
});

window.show();
window.webContents.once('did-finish-load', () => {
resolve();
Expand Down Expand Up @@ -175,7 +194,7 @@ export class Application {
}

private onReady = () => {
this.mainMenu.set();
this.mainMenu.update();
// this.showOpenLocalRealm();
// this.showConnectToServer();
this.showGreeting();
Expand Down
64 changes: 59 additions & 5 deletions src/main/MainMenu.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,50 @@
import * as electron from 'electron';
import { Language } from '../services/schema-export';
import { Application } from './Application';

const isProduction = process.env.NODE_ENV === 'production';

export class MainMenu {
private menu = electron.Menu.buildFromTemplate(this.menuTemplate());
export interface IMainMenuOptions {
enableExportSchema?: boolean;
}

const DEFAULT_OPTIONS: IMainMenuOptions = {
enableExportSchema: false,
};

public set() {
electron.Menu.setApplicationMenu(this.menu);
export interface IExportSchemaOptions {
language: Language;
}

export class MainMenu {
public update(options: IMainMenuOptions = DEFAULT_OPTIONS) {
const template = this.menuTemplate(options);
const menu = electron.Menu.buildFromTemplate(template);
electron.Menu.setApplicationMenu(menu);
}

private menuTemplate(): Electron.MenuItemConstructorOptions[] {
private getMenuItem = (
menu: Electron.Menu,
name: string,
): Electron.MenuItemConstructorOptions => {
return menu.items.find(
item => (item as Electron.MenuItemConstructorOptions).label === name,
) as Electron.MenuItemConstructorOptions;
};

private exportSchema = (language: Language) => {
const focusedWindow = electron.BrowserWindow.getFocusedWindow();
const options: IExportSchemaOptions = {
language,
};
focusedWindow.webContents.send('export-schema', options);
};

private menuTemplate(
options?: IMainMenuOptions,
): Electron.MenuItemConstructorOptions[] {
const enableExportSchema = (options && options.enableExportSchema) || false;

const template: Electron.MenuItemConstructorOptions[] = [
{
label: 'File',
Expand All @@ -24,6 +58,26 @@ export class MainMenu {
},
{ type: 'separator' },
{ role: 'close' },
{
label: 'Save model definitions',
submenu: [
{
label: 'Swift',
click: () => this.exportSchema(Language.Swift),
enabled: enableExportSchema,
},
{
label: 'JavaScript',
click: () => this.exportSchema(Language.JS),
enabled: enableExportSchema,
},
{
label: 'Java',
click: () => this.exportSchema(Language.Java),
enabled: enableExportSchema,
},
],
},
],
},
{
Expand Down
6 changes: 6 additions & 0 deletions src/main/WindowManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import * as url from 'url';

import { getWindowOptions, WindowType } from '../windows/WindowType';

export interface IEventListenerCallbacks {
blur?: () => void;
focus?: () => void;
closed?: () => void;
}

const isProduction = process.env.NODE_ENV === 'production';

function getRendererHtmlPath() {
Expand Down
20 changes: 20 additions & 0 deletions src/services/schema-export/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Tips
realm-browser-osx/RealmBrowser/Support/RLMModelExporter.m has code to export all models. Use that for inspiration.

/src/tests/models has two js model files used to generate realm files that can be opened by the browser and exported. The language subdirectories contains the expected output to compare to. (currently it's the output generated by the OSX Browser, but it has to be slightly updated, to match the format the new generator outputs). You can use that almost as a spec for what to generate. But do check the docs for the languages.

Models:
linkingObjects and default values are NOT stored in Realm files.


# TODO
- Add comment to generated files that linkingObjects and default values are not represented in the models.
- Add CS generator
- Add ObjC generator
- Add tests for Java generator

Swift
types of an array: can they not be optional? (see https://realm.io/docs/swift/latest/#property-cheatsheet)

Java
MutableRealmInteger https://realm.io/docs/java/latest/#counters are implemented as Long in schema, so we can't extrapolate this type from the schema.
27 changes: 27 additions & 0 deletions src/services/schema-export/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import JavaSchemaExporter from './languages/java';
import JSSchemaExporter from './languages/javascript';
import SwiftSchemaExporter from './languages/swift';

import { ISchemaExporter } from './schemaExporter';

export enum Language {
CS = 'C#',
Java = 'Java',
JS = 'JavaScript',
ObjC = 'Objective-C',
Swift = 'Swift',
TS = 'TypeScript',
}

export const SchemaExporter = (language: Language): ISchemaExporter => {
switch (language) {
case Language.Swift:
return new SwiftSchemaExporter();
case Language.JS:
return new JSSchemaExporter();
case Language.Java:
return new JavaSchemaExporter();
default:
throw new Error('Language not supported');
}
};
181 changes: 181 additions & 0 deletions src/services/schema-export/languages/java.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import * as fsPath from 'path';
import { ISchemaFile, SchemaExporter } from '../schemaExporter';

export default class JavaSchemaExporter extends SchemaExporter {
private static readonly PADDING = ' ';

private fieldsContent = '';
private gettersSetterContent = '';
private realmImports: Set<string>;

constructor() {
super();
this.fieldsContent = '';
this.gettersSetterContent = '';
this.realmImports = new Set<string>();
}

public exportSchema(realm: Realm): ISchemaFile[] {
realm.schema.forEach(schema => {
this.makeSchema(schema);
});

return this.files;
}

public makeSchema(schema: Realm.ObjectSchema) {
this.appendLine(
'// Please note : @LinkingObjects and default values are not represented in the schema and thus will not be part of the generated models',
);
this.appendLine('package your.package.name.here;');
this.appendLine('');

this.realmImports.add('import io.realm.RealmObject;');

this.fieldsContent += `public class ${schema.name} extends RealmObject {\n`;

// Properties
for (const key in schema.properties) {
if (schema.properties.hasOwnProperty(key)) {
const prop: any = schema.properties[key];
// Ignoring 'linkingObjects' https://github.com/realm/realm-js/issues/1519
// happens only tests, when opening a Realm using schema that includes 'linkingObjects'
if (prop.type === 'linkingObjects') {
continue;
}
this.propertyLine(prop, schema.primaryKey);
if (prop.indexed && prop.name !== schema.primaryKey) {
this.realmImports.add('import io.realm.annotations.Index;');
}

if (
!prop.optional &&
this.javaPropertyTypeCanBeMarkedRequired(prop.type)
) {
this.realmImports.add('import io.realm.annotations.Required;');
}
}
}

// Primary key
if (schema.primaryKey) {
this.realmImports.add('import io.realm.annotations.PrimaryKey;');
}

// Add all Realm imports
this.realmImports.forEach(line => {
this.appendLine(line);
});
this.appendLine('');

// Add class body
this.appendLine(this.fieldsContent);

// Add getters and setters
this.appendLine(this.gettersSetterContent);

// End class
this.appendLine('}');

this.addFile(schema.name + '.java', this.content);

// reset content for next Schema
this.content = '';
this.fieldsContent = '';
this.gettersSetterContent = '';
this.realmImports.clear();
}

private propertyLine(prop: any, primaryKey: string | undefined): void {
if (prop.name === primaryKey) {
this.fieldsContent += `${JavaSchemaExporter.PADDING}@PrimaryKey\n`;
} else if (prop.indexed) {
this.fieldsContent += `${JavaSchemaExporter.PADDING}@Index\n`;
}
if (!prop.optional && this.javaPropertyTypeCanBeMarkedRequired(prop.type)) {
this.fieldsContent += `${JavaSchemaExporter.PADDING}@Required\n`;
}

this.fieldsContent += `${JavaSchemaExporter.PADDING}private ${this.javaNameForProperty(
prop,
)} ${prop.name};\n`;

this.gettersSetterContent += `${JavaSchemaExporter.PADDING}public ${this.javaNameForProperty(
prop,
)} ${prop.type === 'bool' ? 'is' : 'get'}${this.capitalizedString(
prop.name,
)}() { return ${prop.name}; }\n\n`;

this.gettersSetterContent += `${JavaSchemaExporter.PADDING}public void set${this.capitalizedString(
prop.name,
)}(${this.javaNameForProperty(
prop,
)} ${prop.name}) { this.${prop.name} = ${prop.name}; }\n\n`;
}

private javaPropertyTypeCanBeMarkedRequired(type: any): boolean {
switch (type) {
case 'bool':
case 'int':
case 'float':
case 'double':
case 'object':
return false;
case 'string':
case 'data':
case 'date':
case 'list':
return true;
}
return false;
}

private javaNameForProperty(property: any): any {
if (property.type === 'list') {
this.realmImports.add('import io.realm.RealmList;');
switch (property.objectType) {
case 'bool':
return 'RealmList<Boolean>';
case 'int':
return 'RealmList<Long>';
case 'float':
return 'RealmList<Float>';
case 'double':
return 'RealmList<Double>';
case 'string':
return 'RealmList<String>';
case 'data':
return 'RealmList<byte[]>';
case 'date':
this.realmImports.add('import java.util.Date;');
return 'RealmList<Date>';
default:
return `RealmList<${property.objectType}>`;
}
}
switch (property.type) {
case 'bool':
return property.optional ? 'Boolean' : 'boolean';
case 'int':
return property.optional ? 'Long' : 'long';
case 'float':
return property.optional ? 'Float' : 'float';
case 'double':
return property.optional ? 'Double' : 'double';
case 'string':
return 'String';
case 'data':
return 'byte[]';
case 'date':
this.realmImports.add('import java.util.Date;');
return 'Date';
case 'object':
return property.objectType;
}
return null;
}

private capitalizedString(name: string): string {
return name.charAt(0).toUpperCase() + name.slice(1);
}
}
Loading

0 comments on commit f52def5

Please sign in to comment.