Skip to content

Commit

Permalink
Add Android Java Support (#130)
Browse files Browse the repository at this point in the history
Adds support for `analytics-android` to Typewriter
  • Loading branch information
Austin McBee authored Apr 14, 2020
1 parent f15d036 commit 8e35dbc
Show file tree
Hide file tree
Showing 98 changed files with 6,696 additions and 10 deletions.
50 changes: 50 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,52 @@ jobs:
cd mock && yarn dev &
sleep 5
make test-ios-swift-prod
test-android-dev:
docker: # run the steps with Docker
- image: circleci/android:api-29-node
environment:
JVM_OPTS: -Xmx3200m
steps: # a collection of executable commands
- attach_workspace: { at: . }
- restore_cache:
key: jars-{{ checksum "tests/e2e/android-java/build.gradle" }}-{{ checksum "tests/e2e/android-java/app/build.gradle" }}
- run:
name: Download Dependencies
command: cd tests/e2e/android-java && ./gradlew androidDependencies && cd ../../..
- run:
name: Run Android Java Tests
command: |
git clone https://github.com/segmentio/mock.git && cd mock && yarn && cd ..
cd mock && yarn dev &
sleep 5
make test-android-dev
- save_cache:
paths:
- ~/typewriter/tests/e2e/android-java/.gradle
key: jars-{{ checksum "tests/e2e/android-java/build.gradle" }}-{{ checksum "tests/e2e/android-java/app/build.gradle" }}
test-android-prod:
docker: # run the steps with Docker
- image: circleci/android:api-29-node
environment:
JVM_OPTS: -Xmx3200m
steps: # a collection of executable commands
- attach_workspace: { at: . }
- restore_cache:
key: jars-{{ checksum "tests/e2e/android-java/build.gradle" }}-{{ checksum "tests/e2e/android-java/app/build.gradle" }}
- run:
name: Download Dependencies
command: cd tests/e2e/android-java && ./gradlew androidDependencies && cd ../../..
- run:
name: Run Android Java Tests
command: |
git clone https://github.com/segmentio/mock.git && cd mock && yarn && cd ..
cd mock && yarn dev &
sleep 5
make test-android-prod
- save_cache:
paths:
- ~/typewriter/tests/e2e/android-java/.gradle
key: jars-{{ checksum "tests/e2e/android-java/build.gradle" }}-{{ checksum "tests/e2e/android-java/app/build.gradle" }}

workflows:
version: 2
Expand Down Expand Up @@ -273,3 +319,7 @@ workflows:
requires: [setup]
- test-ios-swift-prod:
requires: [setup]
- test-android-dev:
requires: [setup]
- test-android-prod:
requires: [setup]
26 changes: 25 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ e2e:
@make test-ios-swift

@### Android
@# TODO
@make test-android-java

.PHONY: lint
lint:
Expand Down Expand Up @@ -204,6 +204,30 @@ test-ios-swift:
@cd tests/e2e/ios-swift && pod install
@make test-ios-swift-dev test-ios-swift-prod

.PHONY: test-android-java
test-android-java:
@cd tests/e2e/android-java
@make test-android-dev test-android-prod

.PHONY: test-android-dev test-android-prod test-android-java-runner test-android-runner
test-android-dev: IS_DEVELOPMENT=true
test-android-dev: TYPEWRITER_COMMAND=build
test-android-dev: test-android-java-runner

test-android-prod: IS_DEVELOPMENT=false
test-android-prod: TYPEWRITER_COMMAND=prod
test-android-prod: test-android-java-runner

test-android-java-runner: LANGUAGE=java
test-android-java-runner: test-android-runner

test-android-runner:
@echo "\n>>> 🏃 Running Android client test suite ($(TYPEWRITER_COMMAND), $(LANGUAGE))...\n"
@make clear-mock
@yarn run -s dev $(TYPEWRITER_COMMAND) --config=./tests/e2e/android-java
@cd tests/e2e/android-java && ./gradlew testDebugUnitTest
@SDK=analytics-android LANGUAGE=$(LANGUAGE) IS_DEVELOPMENT=$(IS_DEVELOPMENT) yarn run -s jest ./tests/e2e/suite.test.ts

.PHONY: test-ios-objc-dev test-ios-objc-prod test-ios-objc-runner test-ios-swift-dev test-ios-swift-prod test-ios-swift-runner test-ios-runner
test-ios-objc-dev: IS_DEVELOPMENT=true
test-ios-objc-dev: TYPEWRITER_COMMAND=build
Expand Down
3 changes: 3 additions & 0 deletions src/cli/commands/init.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ const SDKPrompt: React.FC<SDKPromptProps> = ({ step, sdk, onSubmit }) => {
{ label: 'Web (analytics.js)', value: SDK.WEB },
{ label: 'Node.js (analytics-node)', value: SDK.NODE },
{ label: 'iOS (analytics-ios)', value: SDK.IOS },
{ label: 'Android (analytics-android)', value: SDK.ANDROID },
]
const initialIndex = items.findIndex(i => i.value === sdk)

Expand Down Expand Up @@ -239,12 +240,14 @@ const LanguagePrompt: React.FC<LanguagePromptProps> = ({ step, sdk, language, on
{ label: 'TypeScript', value: Language.TYPESCRIPT },
{ label: 'Objective-C', value: Language.OBJECTIVE_C },
{ label: 'Swift', value: Language.SWIFT },
{ label: 'Java', value: Language.JAVA },
].filter(item => {
// Filter out items that aren't relevant, given the selected SDK.
const supportedLanguages = {
[SDK.WEB]: [Language.JAVASCRIPT, Language.TYPESCRIPT],
[SDK.NODE]: [Language.JAVASCRIPT, Language.TYPESCRIPT],
[SDK.IOS]: [Language.OBJECTIVE_C, Language.SWIFT],
[SDK.ANDROID]: [Language.JAVA],
}

return supportedLanguages[sdk].includes(item.value)
Expand Down
269 changes: 269 additions & 0 deletions src/generators/android/android.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
import { camelCase, upperFirst } from 'lodash'
import { Type, Schema, getPropertiesSchema } from '../ast'
import * as Handlebars from 'handlebars'
import { Generator, GeneratorClient, BasePropertyContext } from '../gen'

// These contexts are what will be passed to Handlebars to perform rendering.
// Everything in these contexts should be properly sanitized.

interface AndroidObjectContext {
// The formatted name for this object, ex: "ProductClicked"
name: string
}

interface AndroidPropertyContext {
// The formatted name for this property, ex: "numAvocados".
name: string
// The type of this property. ex: "String".
type: string
// Stringified property modifiers. ex: "final, @Nonnull".
modifiers: Modifier
// Whether the property is nullable (@Nonnull vs @Nullable modifier).
isVariableNullable: boolean
// Whether runtime error should be thrown for null payload value
shouldThrowRuntimeError: boolean | undefined
}

interface AndroidTrackCallContext {
// The formatted function name, ex: "orderCompleted".
functionName: string
propsParam: boolean
}

enum JavaType {
String = 'String',
Long = 'Long',
Double = 'Double',
Boolean = 'Boolean',
Object = 'Object',
}

export const android: Generator<
{},
AndroidTrackCallContext,
AndroidObjectContext,
AndroidPropertyContext
> = {
generatePropertiesObject: true,
namer: {
// See: https://github.com/AnanthaRajuCprojects/Reserved-Key-Words-list-of-various-programming-languages/blob/master/Java%20Keywords%20List.md
// prettier-ignore
reservedWords: [
"abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const",
"continue", "default", "do", "double", "else", "enum", "extends", "final", "finally", "float",
"for", "goto", "if", "implement", "imports", "instanceof", "int", "interface", "long", "native",
"new", "package", "private", "protected", "public", "return", "short", "static", "strictfp", "super",
"switch", "synchronized", "this", "throw", "throws", "transient", "try", "void", "volatile", "while"
],
quoteChar: '"',
allowedIdentifierStartingChars: 'A-Za-z_',
allowedIdentifierChars: 'A-Za-z0-9_',
},
setup: async () => {
Handlebars.registerHelper('trackCallFunctionSignature', generateFunctionSignature)
Handlebars.registerHelper('trackCallFunctionExecution', generateFunctionExecution)
Handlebars.registerHelper('builderFunctionBody', generateBuilderFunctionBody)
return {}
},
generatePrimitive: async (client, schema, parentPath) => {
let type = JavaType.Object

if (schema.type === Type.STRING) {
type = JavaType.String
} else if (schema.type === Type.BOOLEAN) {
type = JavaType.Boolean
} else if (schema.type === Type.INTEGER) {
type = JavaType.Long
} else if (schema.type === Type.NUMBER) {
type = JavaType.Double
}

return defaultPropertyContext(client, schema, type, parentPath)
},
generateArray: async (client, schema, items, parentPath) => {
return defaultPropertyContext(client, schema, `List<${items.type}>`, parentPath)
},
generateObject: async (client, schema, properties, parentPath) => {
const property = defaultPropertyContext(client, schema, JavaType.Object, parentPath)
let object: AndroidObjectContext | undefined

if (properties.length > 0) {
const className = client.namer.register(schema.name, 'class', {
transform: (name: string) => {
return upperFirst(camelCase(name))
},
})

property.type = className
object = {
name: className,
}
}

return { property, object }
},
generateUnion: async (client, schema, _, parentPath) => {
// TODO: support unions
return defaultPropertyContext(client, schema, 'Object', parentPath)
},
generateTrackCall: async (client, schema) => {
const { properties } = getPropertiesSchema(schema)
return {
class: schema.name.replace(/\s/g, ''),
functionName: client.namer.register(schema.name, 'function->track', {
transform: camelCase,
}),
propsParam: !!properties.length,
}
},
generateRoot: async (client, context) => {
await Promise.all([
client.generateFile(
'TypewriterAnalytics.java',
'generators/android/templates/analytics.java.hbs',
context
),
client.generateFile(
'TypewriterUtils.java',
'generators/android/templates/typewriterUtils.java.hbs',
context
),
client.generateFile(
'SerializableProperties.java',
'generators/android/templates/abstractSerializableClass.java.hbs',
context
),
...context.objects.map(o =>
client.generateFile(`${o.name}.java`, 'generators/android/templates/class.java.hbs', o)
),
])
},
}

interface Param {
hasParam: boolean
name: string
type: string
}

interface Arg {
inUse: boolean
execution: string
fallback?: string
}

enum Modifier {
FinalNullable = 'final @Nullable',
FinalNonNullable = 'final @NonNull',
}

enum Separator {
Comma = ', ',
Indent = ' ',
NewLineIndent = ' ',
}

enum Properties {
ToProperties = 'props.toProperties()',
Create = 'new Properties()',
}

enum Options {
MergeOptions = 'TypewriterUtils.addTypewriterContext(options)',
Create = 'TypewriterUtils.addTypewriterContext()',
}

function defaultPropertyContext(
client: GeneratorClient,
schema: Schema,
type: string,
namespace: string
): AndroidPropertyContext {
return {
name: client.namer.register(schema.name, namespace, {
transform: camelCase,
}),
type,
modifiers:
!schema.isRequired || !!schema.isNullable
? Modifier.FinalNullable
: Modifier.FinalNonNullable,
isVariableNullable: !schema.isRequired || !!schema.isNullable,
shouldThrowRuntimeError: schema.isRequired && !schema.isNullable,
}
}

function generateBuilderFunctionBody(name: string, rawName: string, type: string): string {
const isArrayType = type.match(/List\<(.*)\>/)
const isSerializable = Object.values(JavaType).every(t => t !== type)

const defaultHandler = (raw = rawName, n = name, indentFirstLine = true) =>
`${indentFirstLine ? Separator.Indent : ''}properties.putValue("${raw}", ${n});\n` +
`${Separator.NewLineIndent}return this;`

const serializeObject =
`${Separator.Indent}if(${name} != null){\n` +
`${Separator.NewLineIndent}${
Separator.Indent
}properties.putValue("${rawName}", ${name}.toProperties());\n` +
`${Separator.NewLineIndent}}else{\n` +
`${Separator.NewLineIndent}${Separator.Indent}properties.putValue("${rawName}", ${name});\n` +
`${Separator.NewLineIndent}}\n` +
`${Separator.NewLineIndent}return this;`

const serializeArray =
`${Separator.Indent}List<?> p = TypewriterUtils.serialize(${name});\n` +
`${Separator.NewLineIndent}${defaultHandler(rawName, 'p', false)}`

return isArrayType && isArrayType[1] !== 'Properties'
? serializeArray
: isSerializable
? serializeObject
: defaultHandler()
}

const getValidParams = (potentialParams: Param[]) =>
potentialParams.reduce((acc: string[], { hasParam, name, type }) => {
if (hasParam) {
acc.push(`${Modifier.FinalNullable} ${type} ${name}`)
}
return acc
}, [])

const getValidArgs = (potentialArgs: Arg[], defaultArg?: string[]) =>
potentialArgs.reduce((acc: string[], { inUse, execution, fallback }) => {
if (inUse) {
acc.push(execution)
} else if (fallback) {
acc.push(fallback)
}
return acc
}, defaultArg || [])

function generateFunctionSignature(
{ functionName, propsParam }: { functionName: string; propsParam: boolean },
withOptions: boolean
): string {
const params = getValidParams([
{ hasParam: propsParam, name: 'props', type: upperFirst(functionName) },
{ hasParam: withOptions, name: 'options', type: 'Options' },
])

return `public void ${functionName}(${params.join(Separator.Comma)})`
}

function generateFunctionExecution(
{ rawEventName, propsParam }: { rawEventName: string; propsParam: boolean },
withOptions: boolean
): string {
const args = getValidArgs(
[
{ inUse: propsParam, execution: Properties.ToProperties, fallback: Properties.Create },
{ inUse: withOptions, execution: Options.MergeOptions, fallback: Options.Create },
],
[`"${rawEventName}"`]
)
return `{
this.analytics.track(${args.join(Separator.Comma)});
}`
}
1 change: 1 addition & 0 deletions src/generators/android/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { android } from './android'
Loading

0 comments on commit 8e35dbc

Please # to comment.