diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 0813f52e..5588b213 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -7,6 +7,7 @@ order. === Release 1.0.0 (In progress) +* https://github.com/spring-io/initializr/issues/83[#83]: add dependency scope support. * https://github.com/spring-io/initializr/issues/81[#81]: allow baseDir parameter with sub-directories. * https://github.com/spring-io/initializr/issues/80[#80]: upgrade to Gradle 2.3. * https://github.com/spring-io/initializr/issues/62[#62]: add version range support. diff --git a/initializr/src/main/groovy/io/spring/initializr/InitializrMetadata.groovy b/initializr/src/main/groovy/io/spring/initializr/InitializrMetadata.groovy index 1df6b411..79255190 100644 --- a/initializr/src/main/groovy/io/spring/initializr/InitializrMetadata.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/InitializrMetadata.groovy @@ -283,6 +283,12 @@ class InitializrMetadata { @ToString(ignoreNulls = true, includePackage = false) static class Dependency extends IdentifiableElement { + static final String SCOPE_COMPILE = 'compile' + static final String SCOPE_RUNTIME = 'runtime' + static final String SCOPE_PROVIDED = 'provided' + static final String SCOPE_TEST = 'test' + static final List SCOPE_ALL = [SCOPE_COMPILE, SCOPE_RUNTIME, SCOPE_PROVIDED, SCOPE_TEST] + List aliases = [] List facets = [] @@ -293,10 +299,19 @@ class InitializrMetadata { String version + String scope = SCOPE_COMPILE + String description String versionRange + void setScope(String scope) { + if (!SCOPE_ALL.contains(scope)) { + throw new InvalidInitializrMetadataException("Invalid scope $scope must be one of $SCOPE_ALL") + } + this.scope = scope + } + void setVersionRange(String versionRange) { this.versionRange = versionRange ? versionRange.trim() : null } @@ -313,12 +328,13 @@ class InitializrMetadata { * Define this dependency as a standard spring boot starter with the specified name *

If no name is specified, the root 'spring-boot-starter' is assumed. */ - def asSpringBootStarter(String name) { + Dependency asSpringBootStarter(String name) { groupId = 'org.springframework.boot' artifactId = name ? 'spring-boot-starter-' + name : 'spring-boot-starter' if (name) { id = name } + this } /** diff --git a/initializr/src/main/groovy/io/spring/initializr/ProjectGenerator.groovy b/initializr/src/main/groovy/io/spring/initializr/ProjectGenerator.groovy index 6540cd67..1ecfeaa3 100644 --- a/initializr/src/main/groovy/io/spring/initializr/ProjectGenerator.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/ProjectGenerator.groovy @@ -23,6 +23,7 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value import org.springframework.util.Assert +import static io.spring.initializr.InitializrMetadata.Dependency.* import static io.spring.initializr.support.GroovyTemplate.template /** @@ -167,11 +168,17 @@ class ProjectGenerator { request.resolve(metadata) // request resolved so we can log what has been requested - def dependencies = request.resolvedDependencies.collect { it.id } - log.info("Processing request{type=$request.type, dependencies=$dependencies}") + def dependencies = request.resolvedDependencies + def dependencyIds = dependencies.collect { it.id } + log.info("Processing request{type=$request.type, dependencies=$dependencyIds}") request.properties.each { model[it.key] = it.value } + model['compileDependencies'] = filterDependencies(dependencies, SCOPE_COMPILE) + model['runtimeDependencies'] = filterDependencies(dependencies, SCOPE_RUNTIME) + model['providedDependencies'] = filterDependencies(dependencies, SCOPE_PROVIDED) + model['testDependencies'] = filterDependencies(dependencies, SCOPE_TEST) + // @SpringBootApplication available as from 1.2.0.RC1 model['useSpringBootApplication'] = VERSION_1_2_0_RC1 .compareTo(Version.safeParse(request.bootVersion)) <= 0 @@ -212,4 +219,8 @@ class ProjectGenerator { content << file } + private static def filterDependencies(def dependencies, String scope) { + dependencies.findAll { dep -> scope.equals(dep.scope) }.sort { a, b -> a.id <=> b.id } + } + } diff --git a/initializr/src/main/resources/templates/starter-build.gradle b/initializr/src/main/resources/templates/starter-build.gradle index 0d9990c8..255af5e8 100644 --- a/initializr/src/main/resources/templates/starter-build.gradle +++ b/initializr/src/main/resources/templates/starter-build.gradle @@ -39,11 +39,14 @@ repositories { providedRuntime } <% } %> -dependencies {<% resolvedDependencies.each { %> +dependencies {<% compileDependencies.each { %> compile("${it.groupId}:${it.artifactId}")<% } %><% if (language=='groovy') { %> - compile("org.codehaus.groovy:groovy")<% } %><% if (packaging=='war') { %> - providedRuntime("org.springframework.boot:spring-boot-starter-tomcat")<% } %> - testCompile("org.springframework.boot:spring-boot-starter-test") + compile("org.codehaus.groovy:groovy")<% } %><% runtimeDependencies.each { %> + runtime("${it.groupId}:${it.artifactId}")<% } %><% if (packaging=='war') { %> + providedRuntime("org.springframework.boot:spring-boot-starter-tomcat")<% } %><% providedDependencies.each { %> + providedRuntime("${it.groupId}:${it.artifactId}")<% } %> + testCompile("org.springframework.boot:spring-boot-starter-test") <% testDependencies.each { %> + testCompile("${it.groupId}:${it.artifactId}")<% } %> } eclipse { diff --git a/initializr/src/main/resources/templates/starter-pom.xml b/initializr/src/main/resources/templates/starter-pom.xml index a1b1d8db..34cf9890 100644 --- a/initializr/src/main/resources/templates/starter-pom.xml +++ b/initializr/src/main/resources/templates/starter-pom.xml @@ -24,7 +24,7 @@ ${javaVersion} - <% resolvedDependencies.each { %> + <% compileDependencies.each { %> ${it.groupId} ${it.artifactId}<% if (it.version != null) { %> @@ -33,17 +33,36 @@ org.codehaus.groovy groovy + <% } %> + <% runtimeDependencies.each { %> + + ${it.groupId} + ${it.artifactId}<% if (it.version != null) { %> + ${it.version}<% } %> + runtime <% } %><% if (packaging=='war') { %> org.springframework.boot spring-boot-starter-tomcat provided + <% } %><% providedDependencies.each { %> + + ${it.groupId} + ${it.artifactId}<% if (it.version != null) { %> + ${it.version}<% } %> + provided <% } %> org.springframework.boot spring-boot-starter-test test - + <% testDependencies.each { %> + + ${it.groupId} + ${it.artifactId}<% if (it.version != null) { %> + ${it.version}<% } %> + test + <% } %> diff --git a/initializr/src/test/groovy/io/spring/initializr/InitializrMetadataTests.groovy b/initializr/src/test/groovy/io/spring/initializr/InitializrMetadataTests.groovy index 9deef4ee..044ec5ca 100644 --- a/initializr/src/test/groovy/io/spring/initializr/InitializrMetadataTests.groovy +++ b/initializr/src/test/groovy/io/spring/initializr/InitializrMetadataTests.groovy @@ -97,6 +97,14 @@ class InitializrMetadataTests { metadata.validateDependency(new InitializrMetadata.Dependency()) } + @Test + void invalidDependencyScope() { + def dependency = createDependency('web') + + thrown.expect(InvalidInitializrMetadataException) + dependency.setScope('whatever') + } + @Test void invalidSpringBootRange() { def dependency = createDependency('web') diff --git a/initializr/src/test/groovy/io/spring/initializr/ProjectGeneratorTests.groovy b/initializr/src/test/groovy/io/spring/initializr/ProjectGeneratorTests.groovy index b9272d7c..8c302611 100644 --- a/initializr/src/test/groovy/io/spring/initializr/ProjectGeneratorTests.groovy +++ b/initializr/src/test/groovy/io/spring/initializr/ProjectGeneratorTests.groovy @@ -130,9 +130,9 @@ class ProjectGeneratorTests { def request = createProjectRequest('thymeleaf') request.packaging = 'war' generateMavenPom(request).hasStartClass('demo.DemoApplication') - .hasSpringBootStarterDependency('tomcat') + .hasSpringBootStarterTomcat() .hasDependency('org.foo', 'thymeleaf') // This is tagged as web facet so it brings the web one - .hasSpringBootStarterDependency('test') + .hasSpringBootStarterTest() .hasDependenciesCount(3) } @@ -141,10 +141,10 @@ class ProjectGeneratorTests { def request = createProjectRequest('data-jpa') request.packaging = 'war' generateMavenPom(request).hasStartClass('demo.DemoApplication') - .hasSpringBootStarterDependency('tomcat') + .hasSpringBootStarterTomcat() .hasSpringBootStarterDependency('data-jpa') .hasSpringBootStarterDependency('web') // Added by war packaging - .hasSpringBootStarterDependency('test') + .hasSpringBootStarterTest() .hasDependenciesCount(4) } @@ -223,7 +223,7 @@ class ProjectGeneratorTests { @Test void groovyWithMavenUsesJavaDir() { def request = createProjectRequest('web') - request.type = 'maven-project' + request.type = 'maven-project' request.language = 'groovy' generateProject(request).isMavenProject().isGroovyProject() } @@ -231,11 +231,52 @@ class ProjectGeneratorTests { @Test void groovyWithGradleUsesGroovyDir() { def request = createProjectRequest('web') - request.type = 'gradle-project' + request.type = 'gradle-project' request.language = 'groovy' generateProject(request).isGradleProject().isGroovyProject() } + @Test + void mavenPomWithCustomScope() { + def h2 = new InitializrMetadata.Dependency(id: 'h2', groupId: 'org.h2', artifactId: 'h2', scope: 'runtime') + def hamcrest = new InitializrMetadata.Dependency(id: 'hamcrest', groupId: 'org.hamcrest', + artifactId: 'hamcrest', scope: 'test') + def servlet = new InitializrMetadata.Dependency(id: 'servlet-api', groupId: 'javax.servlet', + artifactId: 'servlet-api', scope: 'provided') + def metadata = InitializrMetadataBuilder.withDefaults() + .addDependencyGroup('core', 'web', 'security', 'data-jpa') + .addDependencyGroup('database', h2) + .addDependencyGroup('container', servlet) + .addDependencyGroup('test', hamcrest).validateAndGet() + projectGenerator.metadata = metadata + def request = createProjectRequest('hamcrest', 'h2', 'servlet-api', 'data-jpa', 'web') + generateMavenPom(request).hasDependency(h2).hasDependency(hamcrest).hasDependency(servlet) + .hasSpringBootStarterDependency('data-jpa') + .hasSpringBootStarterDependency('web') + } + + @Test + void gradleBuildWithCustomScope() { + def h2 = new InitializrMetadata.Dependency(id: 'h2', groupId: 'org.h2', artifactId: 'h2', scope: 'runtime') + def hamcrest = new InitializrMetadata.Dependency(id: 'hamcrest', groupId: 'org.hamcrest', + artifactId: 'hamcrest', scope: 'test') + def servlet = new InitializrMetadata.Dependency(id: 'servlet-api', groupId: 'javax.servlet', + artifactId: 'servlet-api', scope: 'provided') + def metadata = InitializrMetadataBuilder.withDefaults() + .addDependencyGroup('core', 'web', 'security', 'data-jpa') + .addDependencyGroup('database', h2) + .addDependencyGroup('container', servlet) + .addDependencyGroup('test', hamcrest).validateAndGet() + projectGenerator.metadata = metadata + def request = createProjectRequest('hamcrest', 'h2', 'servlet-api', 'data-jpa', 'web') + generateGradleBuild(request) + .contains("compile(\"org.springframework.boot:spring-boot-starter-web\")") + .contains("compile(\"org.springframework.boot:spring-boot-starter-data-jpa\")") + .contains("runtime(\"org.h2:h2\")") + .contains("providedRuntime(\"javax.servlet:servlet-api\")") + .contains("testCompile(\"org.hamcrest:hamcrest\")") + } + PomAssert generateMavenPom(ProjectRequest request) { def content = new String(projectGenerator.generateMavenPom(request)) new PomAssert(content).validateProjectRequest(request) diff --git a/initializr/src/test/groovy/io/spring/initializr/test/GradleBuildAssert.groovy b/initializr/src/test/groovy/io/spring/initializr/test/GradleBuildAssert.groovy index e7b62209..46b72bca 100644 --- a/initializr/src/test/groovy/io/spring/initializr/test/GradleBuildAssert.groovy +++ b/initializr/src/test/groovy/io/spring/initializr/test/GradleBuildAssert.groovy @@ -60,7 +60,7 @@ class GradleBuildAssert { } GradleBuildAssert contains(String expression) { - assertTrue "$expression has not been found in gradle build", content.contains(expression) + assertTrue "$expression has not been found in gradle build $content", content.contains(expression) this } } diff --git a/initializr/src/test/groovy/io/spring/initializr/test/PomAssert.groovy b/initializr/src/test/groovy/io/spring/initializr/test/PomAssert.groovy index dd2b4f3d..4df5827a 100644 --- a/initializr/src/test/groovy/io/spring/initializr/test/PomAssert.groovy +++ b/initializr/src/test/groovy/io/spring/initializr/test/PomAssert.groovy @@ -25,6 +25,7 @@ import org.w3c.dom.Document import org.w3c.dom.Element import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertFalse import static org.junit.Assert.assertNotNull /** @@ -109,6 +110,14 @@ class PomAssert { this } + PomAssert hasSpringBootStarterTomcat() { + hasDependency(new InitializrMetadata.Dependency(id: 'tomcat', scope: 'provided').asSpringBootStarter('tomcat')) + } + + PomAssert hasSpringBootStarterTest() { + hasDependency(new InitializrMetadata.Dependency(id: 'test', scope: 'test').asSpringBootStarter('test')) + } + PomAssert hasSpringBootStarterDependency(String dependency) { hasDependency('org.springframework.boot', "spring-boot-starter-$dependency") } @@ -122,11 +131,18 @@ class PomAssert { } PomAssert hasDependency(String groupId, String artifactId, String version) { - def id = generateId(groupId, artifactId) + hasDependency(new InitializrMetadata.Dependency(groupId: groupId, artifactId: artifactId, version: version)) + } + + PomAssert hasDependency(InitializrMetadata.Dependency expected) { + def id = generateId(expected.groupId, expected.artifactId) def dependency = dependencies[id] assertNotNull "No dependency found with '$id' --> ${dependencies.keySet()}", dependency - if (version) { - assertEquals "Wrong version for $dependency", version, dependency.version + if (expected.version) { + assertEquals "Wrong version for $dependency", expected.version, dependency.version + } + if (expected.scope) { + assertEquals "Wrong scope for $dependency", expected.scope, dependency.scope } this } @@ -189,7 +205,13 @@ class PomAssert { if (version.length > 0) { dependency.version = version.item(0).textContent } - dependencies[dependency.generateId()] = dependency + def scope = element.getElementsByTagName('scope') + if (scope.length > 0) { + dependency.scope = scope.item(0).textContent + } + def id = dependency.generateId() + assertFalse("Duplicate dependency with id $id", dependencies.containsKey(id)) + dependencies[id] = dependency } } } diff --git a/initializr/src/test/groovy/io/spring/initializr/web/CommandLineExampleIntegrationTests.groovy b/initializr/src/test/groovy/io/spring/initializr/web/CommandLineExampleIntegrationTests.groovy index 7ff85381..7c7e322e 100644 --- a/initializr/src/test/groovy/io/spring/initializr/web/CommandLineExampleIntegrationTests.groovy +++ b/initializr/src/test/groovy/io/spring/initializr/web/CommandLineExampleIntegrationTests.groovy @@ -38,7 +38,7 @@ class CommandLineExampleIntegrationTests extends AbstractInitializrControllerInt downloadZip('/starter.zip').isJavaProject() .isMavenProject().hasStaticAndTemplatesResources(false).pomAssert() .hasSpringBootStarterRootDependency() - .hasSpringBootStarterDependency('test') + .hasSpringBootStarterTest() .hasDependenciesCount(2) } @@ -48,7 +48,7 @@ class CommandLineExampleIntegrationTests extends AbstractInitializrControllerInt .isMavenProject().hasStaticAndTemplatesResources(true).pomAssert() .hasJavaVersion('1.8') .hasSpringBootStarterDependency('web') - .hasSpringBootStarterDependency('test') + .hasSpringBootStarterTest() .hasDependenciesCount(2) } diff --git a/initializr/src/test/groovy/io/spring/initializr/web/MainControllerEnvIntegrationTests.groovy b/initializr/src/test/groovy/io/spring/initializr/web/MainControllerEnvIntegrationTests.groovy index 75379db7..d8ef7fdf 100644 --- a/initializr/src/test/groovy/io/spring/initializr/web/MainControllerEnvIntegrationTests.groovy +++ b/initializr/src/test/groovy/io/spring/initializr/web/MainControllerEnvIntegrationTests.groovy @@ -52,7 +52,7 @@ class MainControllerEnvIntegrationTests extends AbstractInitializrControllerInte .hasStaticAndTemplatesResources(false).pomAssert() .hasDependenciesCount(2) .hasSpringBootStarterDependency('data-jpa') - .hasSpringBootStarterDependency('test') + .hasSpringBootStarterTest() } } diff --git a/initializr/src/test/groovy/io/spring/initializr/web/MainControllerIntegrationTests.groovy b/initializr/src/test/groovy/io/spring/initializr/web/MainControllerIntegrationTests.groovy index 21e2f009..8c78da0c 100644 --- a/initializr/src/test/groovy/io/spring/initializr/web/MainControllerIntegrationTests.groovy +++ b/initializr/src/test/groovy/io/spring/initializr/web/MainControllerIntegrationTests.groovy @@ -19,6 +19,7 @@ package io.spring.initializr.web import java.nio.charset.Charset import groovy.json.JsonSlurper +import io.spring.initializr.InitializrMetadata import io.spring.initializr.InitializrMetadataVersion import org.json.JSONObject import org.junit.Ignore @@ -56,7 +57,7 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra .hasDependenciesCount(3) .hasSpringBootStarterDependency('web') .hasSpringBootStarterDependency('data-jpa') // alias jpa -> data-jpa - .hasSpringBootStarterDependency('test') + .hasSpringBootStarterTest() } @Test @@ -69,10 +70,12 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra @Test void dependencyInRange() { + def biz = new InitializrMetadata.Dependency(id: 'biz', groupId: 'org.acme', + artifactId: 'biz', version: '1.3.5', scope: 'runtime') downloadTgz('/starter.tgz?style=org.acme:biz&bootVersion=1.2.1.RELEASE').isJavaProject().isMavenProject() .hasStaticAndTemplatesResources(false).pomAssert() .hasDependenciesCount(2) - .hasDependency('org.acme', 'biz', '1.3.5') + .hasDependency(biz) } @Test @@ -90,7 +93,7 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra .hasStaticAndTemplatesResources(false).pomAssert() .hasDependenciesCount(2) .hasSpringBootStarterRootDependency() // the root dep is added if none is specified - .hasSpringBootStarterDependency('test') + .hasSpringBootStarterTest() } @Test @@ -100,7 +103,7 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra .hasDependenciesCount(3) .hasSpringBootStarterDependency('web') .hasSpringBootStarterDependency('data-jpa') // alias jpa -> data-jpa - .hasSpringBootStarterDependency('test') + .hasSpringBootStarterTest() } @Test @@ -110,7 +113,7 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra .hasDependenciesCount(3) .hasSpringBootStarterDependency('web') .hasSpringBootStarterDependency('data-jpa') // alias jpa -> data-jpa - .hasSpringBootStarterDependency('test') + .hasSpringBootStarterTest() } @Test diff --git a/initializr/src/test/groovy/io/spring/initializr/web/ProjectGenerationSmokeTests.groovy b/initializr/src/test/groovy/io/spring/initializr/web/ProjectGenerationSmokeTests.groovy index 1127dd8f..9018a751 100644 --- a/initializr/src/test/groovy/io/spring/initializr/web/ProjectGenerationSmokeTests.groovy +++ b/initializr/src/test/groovy/io/spring/initializr/web/ProjectGenerationSmokeTests.groovy @@ -74,7 +74,7 @@ class ProjectGenerationSmokeTests extends AbstractInitializrControllerIntegratio projectAssert.hasBaseDir("demo").isMavenProject().isJavaProject() .hasStaticAndTemplatesResources(false) .pomAssert().hasDependenciesCount(2) - .hasSpringBootStarterRootDependency().hasSpringBootStarterDependency('test') + .hasSpringBootStarterRootDependency().hasSpringBootStarterTest() } } @@ -88,7 +88,7 @@ class ProjectGenerationSmokeTests extends AbstractInitializrControllerIntegratio projectAssert.hasBaseDir('demo').isMavenProject().isGroovyProject() .hasStaticAndTemplatesResources(false) .pomAssert().hasDependenciesCount(3) - .hasSpringBootStarterRootDependency().hasSpringBootStarterDependency('test') + .hasSpringBootStarterRootDependency().hasSpringBootStarterTest() .hasDependency('org.codehaus.groovy', 'groovy') } } @@ -113,7 +113,7 @@ class ProjectGenerationSmokeTests extends AbstractInitializrControllerIntegratio .hasName('My project').hasDescription('A description for my project') .hasSpringBootStarterDependency('web') .hasSpringBootStarterDependency('data-jpa') - .hasSpringBootStarterDependency('test') + .hasSpringBootStarterTest() } } @@ -137,7 +137,7 @@ class ProjectGenerationSmokeTests extends AbstractInitializrControllerIntegratio .hasName('My Groovy project').hasDescription('A description for my Groovy project') .hasSpringBootStarterDependency('web') .hasSpringBootStarterDependency('data-jpa') - .hasSpringBootStarterDependency('test') + .hasSpringBootStarterTest() .hasDependency('org.codehaus.groovy', 'groovy') } } @@ -167,8 +167,8 @@ class ProjectGenerationSmokeTests extends AbstractInitializrControllerIntegratio .isJavaWarProject() .pomAssert().hasPackaging('war').hasDependenciesCount(3) .hasSpringBootStarterDependency('web') // Added with war packaging - .hasSpringBootStarterDependency('tomcat') - .hasSpringBootStarterDependency('test') + .hasSpringBootStarterTomcat() + .hasSpringBootStarterTest() } } diff --git a/initializr/src/test/resources/application-test-default.yml b/initializr/src/test/resources/application-test-default.yml index d99df2d3..00158959 100644 --- a/initializr/src/test/resources/application-test-default.yml +++ b/initializr/src/test/resources/application-test-default.yml @@ -30,12 +30,19 @@ initializr: - name: Biz groupId: org.acme artifactId: biz + scope: runtime version: 1.3.5 versionRange: 1.2.0.BUILD-SNAPSHOT - name: Bur id: org.acme:bur version: 2.1.0 + scope: test versionRange: "[1.1.4.RELEASE,1.2.0.BUILD-SNAPSHOT)" + - name: My API + id : my-api + groupId: org.acme + artifactId: my-api + scope: provided types: - name: Maven POM id: maven-build diff --git a/initializr/src/test/resources/metadata/test-default-2.0.0.json b/initializr/src/test/resources/metadata/test-default-2.0.0.json index ce6c6924..8ed875ee 100644 --- a/initializr/src/test/resources/metadata/test-default-2.0.0.json +++ b/initializr/src/test/resources/metadata/test-default-2.0.0.json @@ -48,6 +48,10 @@ { "id": "org.acme:bar", "name": "Bar" + }, + { + "id": "my-api", + "name": "My API" } ] } diff --git a/initializr/src/test/resources/metadata/test-default-2.1.0.json b/initializr/src/test/resources/metadata/test-default-2.1.0.json index 63cd4565..365fea47 100644 --- a/initializr/src/test/resources/metadata/test-default-2.1.0.json +++ b/initializr/src/test/resources/metadata/test-default-2.1.0.json @@ -58,6 +58,10 @@ "id": "org.acme:bur", "name": "Bur", "versionRange": "[1.1.4.RELEASE,1.2.0.BUILD-SNAPSHOT)" + }, + { + "id": "my-api", + "name": "My API" } ] }