├── .java-version
├── client-tests
├── ruby
│ └── clients
│ │ └── .exists
├── play_2_2
│ ├── app
│ │ └── models
│ │ │ └── .exists
│ ├── project
│ │ ├── build.properties
│ │ └── plugins.sbt
│ └── build.sbt
├── play_2_3
│ ├── app
│ │ └── models
│ │ │ └── .exists
│ ├── project
│ │ ├── build.properties
│ │ └── plugins.sbt
│ └── build.sbt
├── play_2_4
│ ├── app
│ │ └── models
│ │ │ └── .exists
│ ├── project
│ │ ├── build.properties
│ │ └── plugins.sbt
│ └── build.sbt
├── ning_1_8_scala_2_10
│ ├── project
│ │ └── build.properties
│ └── build.sbt
├── ning_1_8_scala_2_11
│ ├── project
│ │ └── build.properties
│ └── build.sbt
└── ning_1_9_scala_2_11
│ ├── project
│ └── build.properties
│ └── build.sbt
├── project
├── build.properties
└── plugins.sbt
├── app
├── public
│ ├── icons
│ │ ├── README.md
│ │ ├── eye-2x.png
│ │ ├── globe-2x.png
│ │ ├── people-2x.png
│ │ ├── person-2x.png
│ │ ├── lock-locked-2x.png
│ │ ├── sort-ascending-2x.png
│ │ └── sort-descending-2x.png
│ ├── logo
│ │ ├── blue.png
│ │ ├── blue.psd
│ │ ├── grey.png
│ │ ├── grey.psd
│ │ ├── primary.png
│ │ └── primary.psd
│ └── images
│ │ └── favicon.ico
├── .gitignore
├── app
│ ├── views
│ │ ├── icon.scala.html
│ │ ├── doc
│ │ │ ├── exampleInfo.scala.html
│ │ │ ├── description.scala.html
│ │ │ ├── maximumInfo.scala.html
│ │ │ ├── minimumInfo.scala.html
│ │ │ ├── typeInfo.scala.html
│ │ │ ├── examples.scala.html
│ │ │ ├── types.scala.html
│ │ │ └── attributes.scala.html
│ │ ├── healthchecks
│ │ │ └── index.scala.html
│ │ ├── versions
│ │ │ ├── deprecation.scala.html
│ │ │ ├── datatype.scala.html
│ │ │ ├── imports.scala.html
│ │ │ ├── interfaces.scala.html
│ │ │ ├── exampleJson.scala.html
│ │ │ ├── body.scala.html
│ │ │ ├── values.scala.html
│ │ │ ├── headers.scala.html
│ │ │ ├── parameters.scala.html
│ │ │ └── unionTypes.scala.html
│ │ ├── types
│ │ │ └── resolve.scala.html
│ │ ├── logged_out.scala.html
│ │ ├── tokens
│ │ │ ├── cleartext.scala.html
│ │ │ ├── show.scala.html
│ │ │ └── create.scala.html
│ │ ├── account
│ │ │ └── profile
│ │ │ │ ├── index.scala.html
│ │ │ │ └── edit.scala.html
│ │ ├── organizations
│ │ │ ├── create.scala.html
│ │ │ ├── edit.scala.html
│ │ │ ├── show.todo
│ │ │ └── details.scala.html
│ │ ├── generators
│ │ │ ├── create.scala.html
│ │ │ ├── generatorForm.scala.html
│ │ │ ├── index.scala.html
│ │ │ ├── show.scala.html
│ │ │ ├── service.scala.html
│ │ │ └── generators.scala.html
│ │ ├── attributes
│ │ │ ├── create.scala.html
│ │ │ ├── attributeForm.scala.html
│ │ │ ├── index.scala.html
│ │ │ └── show.scala.html
│ │ ├── login
│ │ │ ├── forgotPasswordConfirmation.scala.html
│ │ │ ├── forgotPassword.scala.html
│ │ │ ├── index.scala.html
│ │ │ └── resetPassword.scala.html
│ │ ├── mainHead.scala.html
│ │ ├── subscriptions
│ │ │ └── index.scala.html
│ │ ├── domains
│ │ │ ├── index.scala.html
│ │ │ └── form.scala.html
│ │ ├── application_settings
│ │ │ ├── move_form.scala.html
│ │ │ ├── show.scala.html
│ │ │ └── form.scala.html
│ │ ├── organization_attributes
│ │ │ ├── index.scala.html
│ │ │ └── edit.scala.html
│ │ ├── code
│ │ │ └── index.scala.html
│ │ ├── search
│ │ │ └── index.scala.html
│ │ └── members
│ │ │ └── add.scala.html
│ ├── lib
│ │ ├── Pagination.scala
│ │ ├── Markdown.scala
│ │ ├── AppOrderHelper.scala
│ │ ├── Zipfile.scala
│ │ ├── Config.scala
│ │ ├── Github.scala
│ │ ├── TarballFile.scala
│ │ └── MemberDownload.scala
│ └── controllers
│ │ ├── LogoutController.scala
│ │ ├── AccountController.scala
│ │ ├── Healthchecks.scala
│ │ ├── EmailVerifications.scala
│ │ ├── MembershipRequestReviews.scala
│ │ └── SearchController.scala
├── conf
│ ├── application.production.conf
│ ├── application.conf
│ └── base.conf
├── test
│ └── lib
│ │ ├── TestApplication.scala
│ │ └── UtilSpec.scala
└── Dockerfile
├── CONTRIBUTING.md
├── core
├── test
│ ├── resources
│ │ ├── apidoc
│ │ │ └── plugin.properties
│ │ ├── avro
│ │ │ ├── circular.avsc
│ │ │ ├── circular.json
│ │ │ └── example.json
│ │ ├── generators
│ │ │ └── play-2-json-spec-readers-quality-plan-writers.txt
│ │ ├── simple-without-array.json
│ │ └── simple-w-array.json
│ ├── helpers
│ │ ├── RandomHelpers.scala
│ │ └── ValidatedTestHelpers.scala
│ └── core
│ │ ├── FileServiceFetcher.scala
│ │ ├── MockServiceFetcher.scala
│ │ ├── builder
│ │ └── api_json
│ │ │ ├── upgrades
│ │ │ └── RemoveApiDocElementSpec.scala
│ │ │ └── InternalDatatypeSpec.scala
│ │ ├── ServiceMethodsSpec.scala
│ │ ├── DuplicateFieldValidatorSpec.scala
│ │ ├── ServiceHeadersSpec.scala
│ │ ├── ServiceCommonReturnTypeSpec.scala
│ │ ├── ImportedResourcePathsSpec.scala
│ │ └── ImportServiceApiJsonSpec.scala
└── app
│ ├── ServiceFetcher.scala
│ ├── VersionMigration.scala
│ ├── builder
│ ├── api_json
│ │ ├── upgrades
│ │ │ ├── InterfacesToSupportResources.scala
│ │ │ ├── RemoveApiDocElement.scala
│ │ │ └── Upgrader.scala
│ │ ├── templates
│ │ │ ├── OptionHelpers.scala
│ │ │ ├── MapMerge.scala
│ │ │ ├── AttributeMerge.scala
│ │ │ ├── ArrayMerge.scala
│ │ │ └── HeaderMerge.scala
│ │ └── ServiceJsonServiceValidator.scala
│ └── DuplicateErrorMessage.scala
│ ├── DuplicateJsonParser.scala
│ └── Importer.scala
├── api
├── conf
│ ├── application.test.conf
│ ├── application.conf
│ ├── devandtest.conf
│ └── application.production.conf
├── app
│ ├── views
│ │ └── emails
│ │ │ ├── membershipRequestDeclined.scala.html
│ │ │ ├── passwordResetRequestCreated.scala.html
│ │ │ ├── emailVerificationCreated.scala.html
│ │ │ ├── membershipRequestAccepted.scala.html
│ │ │ ├── membershipCreated.scala.html
│ │ │ ├── membershipRequestCreated.scala.html
│ │ │ ├── applicationCreated.scala.html
│ │ │ ├── invariants.scala.html
│ │ │ └── versionUpserted.scala.html
│ ├── lib
│ │ ├── Constants.scala
│ │ ├── TokenGenerator.scala
│ │ ├── ServiceUri.scala
│ │ ├── RequestAuthenticationUtil.scala
│ │ ├── Misc.scala
│ │ └── DatabaseServiceFetcher.scala
│ ├── util
│ │ ├── SessionIdGenerator.scala
│ │ ├── QueryFilter.scala
│ │ ├── Conversions.scala
│ │ ├── SessionHelper.scala
│ │ ├── UserAgent.scala
│ │ ├── BasicAuthorization.scala
│ │ ├── LockUtil.scala
│ │ └── Tables.scala
│ ├── invariants
│ │ ├── Invariants.scala
│ │ ├── GeneratorInvariants.scala
│ │ ├── TaskInvariants.scala
│ │ └── PurgeInvariants.scala
│ ├── actors
│ │ ├── Bindings.scala
│ │ └── TaskActor.scala
│ ├── db
│ │ ├── Filters.scala
│ │ └── AuditsDao.scala
│ ├── modules
│ │ ├── ProductionClientModule.scala
│ │ └── clients
│ │ │ └── ProductionGeneratorClientFactory.scala
│ ├── models
│ │ ├── DomainsModel.scala
│ │ ├── OriginalsModel.scala
│ │ ├── GeneratorServicesModel.scala
│ │ ├── AttributesModel.scala
│ │ ├── UsersModel.scala
│ │ ├── TokensModel.scala
│ │ ├── GeneratorWithServiceModel.scala
│ │ └── MembershipsModel.scala
│ ├── processor
│ │ ├── TaskDispatchActorCompanion.scala
│ │ ├── MigrateVersionProcessor.scala
│ │ ├── UserCreatedProcessor.scala
│ │ └── ScheduleSyncGeneratorServicesProcessor.scala
│ ├── controllers
│ │ ├── Healthchecks.scala
│ │ ├── BatchDownloadApplications.scala
│ │ ├── Items.scala
│ │ ├── Changes.scala
│ │ ├── Authentications.scala
│ │ └── PasswordResetRequests.scala
│ └── play
│ │ └── LoggingFilter.scala
├── test
│ ├── db
│ │ ├── DbUtils.scala
│ │ ├── InternalTokensDaoSpec.scala
│ │ └── InternalSubscriptionsDaoSpec.scala
│ ├── helpers
│ │ ├── AsyncHelpers.scala
│ │ ├── OrganizationHelpers.scala
│ │ └── BatchDownloadApplicationHelpers.scala
│ ├── processor
│ │ ├── CheckInvariantsProcessorSpec.scala
│ │ └── ProductionTaskProcessorsSpec.scala
│ ├── actors
│ │ └── TaskActorSpec.scala
│ ├── modules
│ │ └── TestClientModule.scala
│ ├── lib
│ │ ├── TokenGeneratorSpec.scala
│ │ └── TestHelper.scala
│ ├── util
│ │ ├── SessionIdGeneratorSpec.scala
│ │ ├── RandomPortFinder.scala
│ │ ├── SessionHelperSpec.scala
│ │ ├── UserAgentSpec.scala
│ │ ├── GeneratorServiceUtilSpec.scala
│ │ └── BasicAuthorizationSpec.scala
│ ├── controllers
│ │ ├── VersionsSpec.scala
│ │ ├── BatchDownloadApplicationsSpec.scala
│ │ └── ApplicationMetadataSpec.scala
│ └── services
│ │ └── BatchDownloadApplicationsServiceSpec.scala
└── Dockerfile
├── deploy
├── apibuilder-api
│ ├── Chart.yaml
│ └── requirements.yaml
└── apibuilder-app
│ ├── Chart.yaml
│ └── requirements.yaml
├── lib
└── src
│ ├── main
│ └── scala
│ │ ├── ServiceValidator.scala
│ │ ├── Methods.scala
│ │ ├── FileUtils.scala
│ │ ├── Review.scala
│ │ ├── VersionedName.scala
│ │ ├── Bytes.scala
│ │ ├── ServiceConfiguration.scala
│ │ ├── Pager.scala
│ │ ├── ValidatedHelpers.scala
│ │ └── TextDatatype.scala
│ └── test
│ └── scala
│ ├── helpers
│ ├── ServiceConfigurationHelpers.scala
│ └── ValidatedTestHelpers.scala
│ ├── MethodsSpec.scala
│ ├── ReviewSpec.scala
│ └── VersionedNameSpec.scala
├── .gitignore
├── README.md
├── .scalafmt.conf
├── .dockerignore
├── swagger
└── src
│ ├── main
│ └── scala
│ │ └── io
│ │ └── apibuilder
│ │ └── swagger
│ │ ├── translators
│ │ ├── BaseUrl.scala
│ │ ├── ExternalDoc.scala
│ │ ├── Body.scala
│ │ └── Response.scala
│ │ └── SwaggerServiceValidator.scala
│ └── test
│ ├── scala
│ └── io
│ │ └── apibuilder
│ │ └── swagger
│ │ ├── translators
│ │ └── BaseUrlSpec.scala
│ │ └── SwaggerDataSpec.scala
│ └── resources
│ └── no-resources.json
├── avro
├── src
│ ├── main
│ │ └── scala
│ │ │ └── io
│ │ │ └── apibuilder
│ │ │ └── avro
│ │ │ ├── Util.scala
│ │ │ ├── AvroIdlServiceValidator.scala
│ │ │ └── SchemaType.scala
│ └── test
│ │ └── scala
│ │ └── io
│ │ └── apibuilder
│ │ └── avro
│ │ └── AvroIdlServiceValidatorSpec.scala
└── example.avdl
├── .delta
├── script
└── lib
│ ├── ask.rb
│ └── tag.rb
├── SETUP.md
├── .github
└── workflows
│ └── ci.yml
├── DEPLOY.md
├── dao
└── run.rb
├── .apibuilder
├── config
└── .tracked_files
├── LICENSE
└── examples
└── example-interface.json
/.java-version:
--------------------------------------------------------------------------------
1 | 17
2 |
--------------------------------------------------------------------------------
/client-tests/ruby/clients/.exists:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client-tests/play_2_2/app/models/.exists:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client-tests/play_2_3/app/models/.exists:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client-tests/play_2_4/app/models/.exists:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.11.6
2 |
--------------------------------------------------------------------------------
/client-tests/play_2_2/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.9
2 |
--------------------------------------------------------------------------------
/client-tests/play_2_3/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.9
2 |
--------------------------------------------------------------------------------
/client-tests/play_2_4/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.9
2 |
--------------------------------------------------------------------------------
/app/public/icons/README.md:
--------------------------------------------------------------------------------
1 | See https://useiconic.com/open/ for source of icons
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | See https://github.com/gilt/standards/blob/1.0.0/CONTRIBUTIONS.md
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | logs/*
2 | project/project/*
3 | project/target/*
4 | target/*
5 |
--------------------------------------------------------------------------------
/client-tests/ning_1_8_scala_2_10/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.9
2 |
--------------------------------------------------------------------------------
/client-tests/ning_1_8_scala_2_11/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.9
2 |
--------------------------------------------------------------------------------
/client-tests/ning_1_9_scala_2_11/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.9
2 |
--------------------------------------------------------------------------------
/app/public/logo/blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apicollective/apibuilder/HEAD/app/public/logo/blue.png
--------------------------------------------------------------------------------
/app/public/logo/blue.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apicollective/apibuilder/HEAD/app/public/logo/blue.psd
--------------------------------------------------------------------------------
/app/public/logo/grey.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apicollective/apibuilder/HEAD/app/public/logo/grey.png
--------------------------------------------------------------------------------
/app/public/logo/grey.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apicollective/apibuilder/HEAD/app/public/logo/grey.psd
--------------------------------------------------------------------------------
/core/test/resources/apidoc/plugin.properties:
--------------------------------------------------------------------------------
1 | pluginKey=core.plugin.SomePlugin, core.plugin.OtherPlugin
2 |
--------------------------------------------------------------------------------
/api/conf/application.test.conf:
--------------------------------------------------------------------------------
1 | include "devandtest.conf"
2 |
3 | play.modules.enabled += "modules.TestClientModule"
--------------------------------------------------------------------------------
/app/public/icons/eye-2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apicollective/apibuilder/HEAD/app/public/icons/eye-2x.png
--------------------------------------------------------------------------------
/app/public/icons/globe-2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apicollective/apibuilder/HEAD/app/public/icons/globe-2x.png
--------------------------------------------------------------------------------
/app/public/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apicollective/apibuilder/HEAD/app/public/images/favicon.ico
--------------------------------------------------------------------------------
/app/public/logo/primary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apicollective/apibuilder/HEAD/app/public/logo/primary.png
--------------------------------------------------------------------------------
/app/public/logo/primary.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apicollective/apibuilder/HEAD/app/public/logo/primary.psd
--------------------------------------------------------------------------------
/api/conf/application.conf:
--------------------------------------------------------------------------------
1 | include "devandtest.conf"
2 |
3 | play.modules.enabled += "modules.ProductionClientModule"
4 |
--------------------------------------------------------------------------------
/app/public/icons/people-2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apicollective/apibuilder/HEAD/app/public/icons/people-2x.png
--------------------------------------------------------------------------------
/app/public/icons/person-2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apicollective/apibuilder/HEAD/app/public/icons/person-2x.png
--------------------------------------------------------------------------------
/deploy/apibuilder-api/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | appVersion: Manual Deploy
3 | name: apibuilder-api
4 | version: 0.0.1
5 |
--------------------------------------------------------------------------------
/deploy/apibuilder-app/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | appVersion: Manual Deploy
3 | name: apibuilder-app
4 | version: 0.0.1
5 |
--------------------------------------------------------------------------------
/app/public/icons/lock-locked-2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apicollective/apibuilder/HEAD/app/public/icons/lock-locked-2x.png
--------------------------------------------------------------------------------
/app/public/icons/sort-ascending-2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apicollective/apibuilder/HEAD/app/public/icons/sort-ascending-2x.png
--------------------------------------------------------------------------------
/app/public/icons/sort-descending-2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apicollective/apibuilder/HEAD/app/public/icons/sort-descending-2x.png
--------------------------------------------------------------------------------
/app/app/views/icon.scala.html:
--------------------------------------------------------------------------------
1 | @(
2 | path: String,
3 | label: String
4 | )
5 |
6 |
--------------------------------------------------------------------------------
/app/app/views/doc/exampleInfo.scala.html:
--------------------------------------------------------------------------------
1 | @(field: String)
2 |
3 | optional - an example value for this @field used only in the produced documentation
4 |
--------------------------------------------------------------------------------
/client-tests/play_2_2/build.sbt:
--------------------------------------------------------------------------------
1 | name := "play_2_2"
2 |
3 | version := "0.0.1"
4 |
5 | scalaVersion := "2.11.7"
6 |
7 | play.Project.playScalaSettings
8 |
--------------------------------------------------------------------------------
/client-tests/play_2_4/build.sbt:
--------------------------------------------------------------------------------
1 | name := "play_2_4"
2 |
3 | version := "0.0.1"
4 |
5 | scalaVersion := "2.11.7"
6 |
7 | play.Project.playScalaSettings
8 |
--------------------------------------------------------------------------------
/app/app/views/healthchecks/index.scala.html:
--------------------------------------------------------------------------------
1 | @(tpl: models.MainTemplate)(implicit flash: Flash, messages: Messages)
2 |
3 | @main(tpl) {
4 | healthy
5 | }
6 |
7 |
--------------------------------------------------------------------------------
/deploy/apibuilder-api/requirements.yaml:
--------------------------------------------------------------------------------
1 | dependencies:
2 | - name: flow-generic
3 | version: ^1.0.0
4 | repository: https://flow.jfrog.io/artifactory/api/helm/generic-charts-helm
5 |
--------------------------------------------------------------------------------
/deploy/apibuilder-app/requirements.yaml:
--------------------------------------------------------------------------------
1 | dependencies:
2 | - name: flow-generic
3 | version: ^1.0.0
4 | repository: https://flow.jfrog.io/artifactory/api/helm/generic-charts-helm
5 |
--------------------------------------------------------------------------------
/client-tests/play_2_2/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/"
2 |
3 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.2.3")
4 |
--------------------------------------------------------------------------------
/client-tests/play_2_3/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/"
2 |
3 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.3.8")
4 |
--------------------------------------------------------------------------------
/client-tests/play_2_4/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/"
2 |
3 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.4.2")
4 |
--------------------------------------------------------------------------------
/core/app/ServiceFetcher.scala:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import io.apibuilder.spec.v0.models.Service
4 |
5 | trait ServiceFetcher {
6 |
7 | def fetch(uri: String): Service
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/app/app/views/versions/deprecation.scala.html:
--------------------------------------------------------------------------------
1 | @(deprecation: io.apibuilder.spec.v0.models.Deprecation)
2 |
3 |
4 | deprecated:
5 | @Html(lib.Markdown(deprecation.description))
6 |
7 |
--------------------------------------------------------------------------------
/core/app/VersionMigration.scala:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | case class VersionMigration(
4 | internal: Boolean
5 | ) {
6 |
7 | def makeFieldsWithDefaultsRequired(): Boolean = internal
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/lib/src/main/scala/ServiceValidator.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import cats.data.ValidatedNec
4 |
5 | trait ServiceValidator[T] {
6 |
7 | def validate(rawInput: String): ValidatedNec[String, T]
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/api/app/views/emails/membershipRequestDeclined.scala.html:
--------------------------------------------------------------------------------
1 | @(
2 | org: db.InternalOrganization,
3 | user: db.InternalUser
4 | )
5 |
6 |
7 |
8 | Your request to join @org.name has been declined.
9 |
10 |
11 |
--------------------------------------------------------------------------------
/api/app/views/emails/passwordResetRequestCreated.scala.html:
--------------------------------------------------------------------------------
1 | @(
2 | appConfig: lib.AppConfig,
3 | token: String
4 | )
5 |
6 |
7 | Reset your password
8 |
9 |
--------------------------------------------------------------------------------
/client-tests/ning_1_8_scala_2_10/build.sbt:
--------------------------------------------------------------------------------
1 | name := "apidoc-ning"
2 |
3 | scalaVersion := "2.10.5"
4 |
5 | libraryDependencies ++= Seq(
6 | "com.typesafe.play" %% "play-json" % "2.3.9",
7 | "com.ning" % "async-http-client" % "1.8.16"
8 | )
9 |
--------------------------------------------------------------------------------
/client-tests/ning_1_9_scala_2_11/build.sbt:
--------------------------------------------------------------------------------
1 | name := "apidoc-ning"
2 |
3 | scalaVersion := "2.11.7"
4 |
5 | libraryDependencies ++= Seq(
6 | "com.typesafe.play" %% "play-json" % "2.4.6",
7 | "com.ning" % "async-http-client" % "1.9.31"
8 | )
9 |
--------------------------------------------------------------------------------
/client-tests/ning_1_8_scala_2_11/build.sbt:
--------------------------------------------------------------------------------
1 | name := "apidoc-ning"
2 |
3 | scalaVersion := "2.11.7"
4 |
5 | libraryDependencies ++= Seq(
6 | "com.typesafe.play" %% "play-json" % "2.3.8",
7 | "com.ning" % "async-http-client" % "1.8.15"
8 | )
9 |
--------------------------------------------------------------------------------
/app/app/views/doc/description.scala.html:
--------------------------------------------------------------------------------
1 | @(field: String)
2 |
3 | description optional description for what this @field
4 | provides. Supports GFM .
5 |
6 |
--------------------------------------------------------------------------------
/client-tests/play_2_3/build.sbt:
--------------------------------------------------------------------------------
1 | name := "play_2_3"
2 |
3 | version := "0.0.1"
4 |
5 | scalaVersion := "2.11.7"
6 |
7 | libraryDependencies ++= Seq(
8 | ws
9 | )
10 |
11 | lazy val root = (project in file(".")).enablePlugins(PlayScala)
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dao/psql
2 | dao/scala
3 | run
4 | target/
5 | logs/
6 | .ivy2
7 | *.downloaded.scala
8 | *.downloaded.rb
9 | *.swp
10 | *.swo
11 | ning_1_8_scala_2_10/src
12 | ning_1_8_scala_2_11/src
13 | .DS_STORE
14 | .bsp
15 | .idea/
16 | .idea_modules/
17 |
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/apicollective/apibuilder)
2 |
3 | API Builder
4 | ==========
5 |
6 | Simple, Comprehensive Tooling for Modern APIs.
7 |
8 | See https://www.apibuilder.io
9 |
--------------------------------------------------------------------------------
/api/app/views/emails/emailVerificationCreated.scala.html:
--------------------------------------------------------------------------------
1 | @(
2 | appConfig: lib.AppConfig,
3 | verification: db.InternalEmailVerification
4 | )
5 |
6 |
7 | Verify your email address
8 |
9 |
--------------------------------------------------------------------------------
/app/app/views/doc/maximumInfo.scala.html:
--------------------------------------------------------------------------------
1 | @(field: String)
2 |
3 | optional - For a string, refers to the maximum length. For an array,
4 | the maximum number of elements in the array. For example, a value of 1
5 | for an array would indicate the array must have at most 1 element.
6 |
--------------------------------------------------------------------------------
/app/app/views/doc/minimumInfo.scala.html:
--------------------------------------------------------------------------------
1 | @(field: String)
2 |
3 | optional - For a string, refers to the minimum length. For an array,
4 | the minimum number of elements in the array. For example, a value of 1
5 | for an array would indicate the array must have at least 1 element.
6 |
--------------------------------------------------------------------------------
/lib/src/main/scala/Methods.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | object Methods {
4 |
5 | val MethodsNotAcceptingBodies: Set[String] = Set("GET")
6 |
7 | def supportsBody(verb: String): Boolean = {
8 | !MethodsNotAcceptingBodies.contains(verb.toUpperCase)
9 | }
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/.scalafmt.conf:
--------------------------------------------------------------------------------
1 | version = 3.5.9
2 | runner.dialect=scala213
3 | maxColumn = 120
4 | continuationIndent.callSite = 2
5 | continuationIndent.defnSite = 2
6 | continuationIndent.ctorSite = 2
7 | continuationIndent.extendSite = 2
8 | align.preset = none
9 | project.excludePaths = [ "glob:**/generated/**" ]
10 |
--------------------------------------------------------------------------------
/app/app/views/versions/datatype.scala.html:
--------------------------------------------------------------------------------
1 | @(org: io.apibuilder.api.v0.models.Organization,
2 | app: io.apibuilder.api.v0.models.Application,
3 | version: String,
4 | service: io.apibuilder.spec.v0.models.Service,
5 | typeName: String)
6 |
7 | @Html(lib.TypeLabel(org, app, version, service, typeName).link)
8 |
--------------------------------------------------------------------------------
/core/test/resources/avro/circular.avsc:
--------------------------------------------------------------------------------
1 | {
2 | "type": "record",
3 | "name": "category",
4 | "namespace": "apidoc",
5 | "fields": [
6 | {"name": "uuid", "type": {"namespace": "java.util", "type": "fixed", "size": 16, "name": "UUID"}},
7 | {"name": "parent", "type": "category"}
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/core/test/resources/avro/circular.json:
--------------------------------------------------------------------------------
1 | {
2 | "base_url": "http://localhost:9000",
3 | "name": "API Builder",
4 | "models": {
5 | "category": {
6 | "fields": [
7 | { "name": "uuid", "type": "uuid" },
8 | { "name": "parent", "type": "category"}
9 | ]
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/app/views/types/resolve.scala.html:
--------------------------------------------------------------------------------
1 | @(tpl: models.MainTemplate,
2 | typeName: String,
3 | errorMessages: Seq[String] = Nil
4 | )(implicit flash: Flash, messages: Messages)
5 |
6 | @main(tpl, errorMessages = errorMessages) {
7 |
8 |
9 | @typeName is not a recognized type.
10 |
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/app/app/views/logged_out.scala.html:
--------------------------------------------------------------------------------
1 | @(tpl: models.MainTemplate)(implicit flash: Flash, messages: Messages)
2 |
3 | @main(tpl.copy(title = Some("You are now logged out"))) {
4 |
5 | You are now logged out
6 |
7 |
8 | Log back in
9 |
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/core/app/builder/api_json/upgrades/InterfacesToSupportResources.scala:
--------------------------------------------------------------------------------
1 | package builder.api_json.upgrades
2 |
3 | import play.api.libs.json.JsObject
4 |
5 | object InterfacesToSupportResources extends Upgrader {
6 | def apply(js: JsObject): JsObject = {
7 | // TODO: placeholder for future work
8 | js
9 | }
10 | }
--------------------------------------------------------------------------------
/app/conf/application.production.conf:
--------------------------------------------------------------------------------
1 | include "base.conf"
2 |
3 | play.http.secret.key=${?CONF_PLAY_CRYPTO_SECRET}
4 | apibuilder.api.host=${?CONF_APIBUILDER_API_HOST}
5 | apibuilder.api.token=${?CONF_APIBUILDER_TOKEN}
6 | apibuilder.app.host=${?CONF_APIBUILDER_APP_HOST}
7 | apibuilder.github.oauth.client.id=0accc1cc393e9f5e46a7
8 |
9 |
--------------------------------------------------------------------------------
/api/app/lib/Constants.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import java.util.UUID
4 |
5 | object Constants {
6 |
7 | val DefaultUserGuid: UUID = UUID.fromString("f3973f60-be9f-11e3-b1b6-0800200c9a66")
8 |
9 | val AdminUserEmails: Seq[String] = Seq("admin@apibuilder.io")
10 |
11 | val AdminUserGuid: UUID = DefaultUserGuid
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/core/test/helpers/RandomHelpers.scala:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import java.util.UUID
4 |
5 | trait RandomHelpers {
6 |
7 | def randomString(): String = {
8 | UUID.randomUUID.toString
9 | }
10 |
11 | def createRandomName(suffix: String): String = {
12 | s"z-test-$suffix-" + UUID.randomUUID.toString
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/api/app/util/SessionIdGenerator.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | object SessionIdGenerator {
4 |
5 | private val Prefix = "A51"
6 | private val RandomLength = 64 - Prefix.length
7 | private val random = new Random()
8 |
9 | def generate(): String = {
10 | "%s%s".format(Prefix, random.alphaNumeric(RandomLength))
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/api/app/invariants/Invariants.scala:
--------------------------------------------------------------------------------
1 | package invariants
2 |
3 | import io.flow.postgresql.Query
4 |
5 | import javax.inject.Inject
6 |
7 | case class Invariant(name: String, query: Query)
8 |
9 | class Invariants @Inject() () {
10 | val all: Seq[Invariant] =
11 | TaskInvariants.all ++ PurgeInvariants.all ++ GeneratorInvariants.all
12 | }
13 |
--------------------------------------------------------------------------------
/api/conf/devandtest.conf:
--------------------------------------------------------------------------------
1 | include "base.conf"
2 |
3 | db.default.url="jdbc:postgresql://localhost/apibuilderdb"
4 | db.default.url=${?CONF_DB_DEFAULT_URL}
5 |
6 | apibuilder.app.host="http://localhost:9000"
7 |
8 | play.http.secret.key="development:uauTKwTxIpP4dWJA53s1ekGwpPdVfUmdCmSMgxa4"
9 |
10 | mail.localDeliveryDir="/tmp/email.apibuilder"
11 |
12 |
--------------------------------------------------------------------------------
/app/app/views/versions/imports.scala.html:
--------------------------------------------------------------------------------
1 | @(imports: Seq[io.apibuilder.spec.v0.models.Import])
2 |
3 |
4 |
5 | @imports.map { imp =>
6 |
7 | @imp.uri
8 |
9 | }
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/conf/application.conf:
--------------------------------------------------------------------------------
1 | include "base.conf"
2 |
3 | play.http.secret.key="development:uauTKwTxIpP4dWJA53s1ekGwpPdVfUmdCmSMgxa4"
4 | apibuilder.api.host="http://localhost:9001"
5 | apibuilder.api.token="ZdRD61ODVPspeV8Wf18EmNuKNxUfjfROyJXtNJXj9GMMwrAxqi8I4aUtNAT6"
6 | apibuilder.app.host="http://localhost:9000"
7 | apibuilder.github.oauth.client.id=60aafb7d025ff883bcc2
8 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | client-tests/
2 | target/
3 | logs/
4 | generated/target/
5 | core/target/
6 | core/logs/
7 | avro/target/
8 | avro/logs/
9 | swagger/target/
10 | swagger/logs/
11 | lib/target/
12 | lib/logs/
13 | api/target/
14 | api/logs/
15 | app/target/
16 | app/logs/
17 | project/target/
18 | .git
19 | .ivy2
20 | *.swp
21 | *.swo
22 | .DS_STORE
23 | *.md
24 | !README.md
--------------------------------------------------------------------------------
/app/app/views/tokens/cleartext.scala.html:
--------------------------------------------------------------------------------
1 | @(tpl: models.MainTemplate,
2 | guid: java.util.UUID,
3 | cleartext: io.apibuilder.api.v0.models.CleartextToken
4 | )(implicit flash: Flash, messages: Messages)
5 |
6 | @main(tpl) {
7 |
8 | @cleartext.token
9 |
10 |
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/swagger/src/main/scala/io/apibuilder/swagger/translators/BaseUrl.scala:
--------------------------------------------------------------------------------
1 | package io.apibuilder.swagger.translators
2 |
3 | object BaseUrl {
4 |
5 | def apply(
6 | schemes: Seq[String],
7 | host: String,
8 | path: Option[String]
9 | ): Seq[String] = {
10 | schemes.map(_.toLowerCase).map { scheme => s"$scheme://$host${path.getOrElse("")}" }
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/api/app/views/emails/membershipRequestAccepted.scala.html:
--------------------------------------------------------------------------------
1 | @import io.apibuilder.common.v0.models.MembershipRole
2 | @(
3 | org: db.InternalOrganization,
4 | user: db.InternalUser,
5 | role: MembershipRole
6 | )
7 |
8 |
9 |
10 | @if(role == MembershipRole.Admin) {
11 | You are now an administrator of @org.name.
12 | } else {
13 | Your request to join @org.name has been accepted!
14 | }
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/app/views/versions/interfaces.scala.html:
--------------------------------------------------------------------------------
1 | @(org: io.apibuilder.api.v0.models.Organization,
2 | app: io.apibuilder.api.v0.models.Application,
3 | version: String,
4 | service: io.apibuilder.spec.v0.models.Service,
5 | interfaces: Iterable[String]
6 | )
7 |
8 |
9 | Interfaces:
10 | @if(interfaces.isEmpty) {
11 | None
12 | } else {
13 | @interfaces.mkString(", ")
14 | }
15 |
16 |
--------------------------------------------------------------------------------
/core/test/resources/generators/play-2-json-spec-readers-quality-plan-writers.txt:
--------------------------------------------------------------------------------
1 | implicit def jsonWritesQualityPlan: play.api.libs.json.Writes[Plan] = {
2 | (
3 | (__ \ "id").write[Long] and
4 | (__ \ "incident_id").write[Long] and
5 | (__ \ "body").write[String] and
6 | (__ \ "grade").write[scala.Option[Int]] and
7 | (__ \ "created_at").write[org.joda.time.DateTime]
8 | )(unlift(Plan.unapply _))
9 | }
10 |
--------------------------------------------------------------------------------
/api/app/views/emails/membershipCreated.scala.html:
--------------------------------------------------------------------------------
1 | @(
2 | appConfig: lib.AppConfig,
3 | membership: io.apibuilder.api.v0.models.Membership
4 | )
5 |
6 |
7 | @membership.user.name <@membership.user.email> has joined
8 | @membership.organization.name as @membership.role.
9 |
10 |
11 |
12 | View all members
13 |
14 |
15 |
--------------------------------------------------------------------------------
/api/app/util/QueryFilter.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import io.flow.postgresql.Query
4 |
5 | abstract class QueryFilter {
6 | def filter(q: Query): Query
7 | }
8 |
9 | abstract class OptionalQueryFilter[T](value: Option[T]) extends QueryFilter {
10 | final def filter(q: Query): Query =
11 | value match {
12 | case None => q
13 | case Some(v) => filter(q, v)
14 | }
15 | def filter(q: Query, value: T): Query
16 | }
17 |
--------------------------------------------------------------------------------
/lib/src/main/scala/FileUtils.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import java.io.File
4 |
5 | object FileUtils {
6 |
7 | def readToString(path: String): String = readToString(new File(path))
8 |
9 | def readToString(file: File): String = {
10 | val source = scala.io.Source.fromFile(file, "UTF-8")
11 | try {
12 | source.getLines().mkString("\n").trim
13 | } finally {
14 | source.close()
15 | }
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/app/test/lib/TestApplication.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import play.api.ApplicationLoader.Context
4 | import play.api.{ApplicationLoader, Environment, Mode, Play}
5 |
6 | trait TestApplication {
7 |
8 | private val env = Environment(new java.io.File("."), this.getClass.getClassLoader, Mode.Test)
9 | private val context = Context.create(env)
10 | private val app = ApplicationLoader(context).load(context)
11 | Play.start(app)
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/lib/src/main/scala/Review.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | case class Review(key: String, name: String)
4 |
5 | object Review {
6 |
7 | val Accept: Review = Review("accept", "Accept")
8 | val Decline: Review = Review("decline", "Decline")
9 |
10 | val All: Seq[Review] = Seq(Accept, Decline)
11 |
12 | def fromString(key: String): Option[Review] = {
13 | val lowerKey = key.toLowerCase
14 | All.find(_.key == lowerKey)
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/api/test/db/DbUtils.scala:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import io.flow.postgresql.Query
4 | import play.api.Application
5 | import play.api.db.Database
6 |
7 | trait DbUtils {
8 | def app: Application
9 |
10 | def database: Database = app.injector.instanceOf[Database]
11 |
12 | def execute(queries: Query*): Unit = {
13 | database.withConnection { c =>
14 | queries.foreach(_.anormSql().executeUpdate()(using c))
15 | }
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/swagger/src/main/scala/io/apibuilder/swagger/translators/ExternalDoc.scala:
--------------------------------------------------------------------------------
1 | package io.apibuilder.swagger.translators
2 |
3 | import io.apibuilder.swagger.Util
4 | import io.swagger.{ models => swagger }
5 |
6 | object ExternalDoc {
7 |
8 | def apply(docs: Option[swagger.ExternalDocs]): Option[String] = {
9 | docs.flatMap { doc =>
10 | Util.combine(Seq(Option(doc.getDescription), Option(doc.getUrl)), ": ")
11 | }
12 | }
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/api/app/views/emails/membershipRequestCreated.scala.html:
--------------------------------------------------------------------------------
1 | @(
2 | appConfig: lib.AppConfig,
3 | request: io.apibuilder.api.v0.models.MembershipRequest
4 | )
5 |
6 |
7 | @request.user.name <@request.user.email> has submitted a
8 | request to join @request.organization.name as @request.role.
9 |
10 |
11 |
12 | Review membership request
13 |
14 |
--------------------------------------------------------------------------------
/avro/src/main/scala/io/apibuilder/avro/Util.scala:
--------------------------------------------------------------------------------
1 | package io.apibuilder.avro
2 |
3 | import lib.Text
4 |
5 | object Util {
6 |
7 | def formatName(name: String): String = {
8 | //Text.camelCaseToUnderscore(name).toLowerCase
9 | name.trim
10 | }
11 |
12 | def toOption(value: String): Option[String] = {
13 | if (value == null || value.trim.isEmpty) {
14 | None
15 | } else {
16 | Some(value.trim)
17 | }
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/.delta:
--------------------------------------------------------------------------------
1 | builds:
2 | - api:
3 | cluster: k8s
4 | dockerfile: api/Dockerfile
5 | initial.number.instances: 2
6 | instance.type: t3.large
7 | port.container: 9000
8 | port.host: 7071
9 | version: 1.3
10 | - app:
11 | cluster: k8s
12 | dockerfile: app/Dockerfile
13 | initial.number.instances: 2
14 | instance.type: t3.large
15 | port.container: 9000
16 | port.host: 7070
17 | version: 1.3
18 |
--------------------------------------------------------------------------------
/api/test/helpers/AsyncHelpers.scala:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import lib.TestHelper
4 | import org.scalatest.concurrent.Eventually
5 | import org.scalatest.concurrent.PatienceConfiguration.Timeout
6 | import org.scalatest.time.{Seconds, Span}
7 |
8 | trait AsyncHelpers extends TestHelper with Eventually {
9 |
10 | def eventuallyInNSeconds[T](n: Long = 3)(f: => T): T = {
11 | eventually(Timeout(Span(n, Seconds))) {
12 | f
13 | }
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/core/app/builder/api_json/templates/OptionHelpers.scala:
--------------------------------------------------------------------------------
1 | package builder.api_json.templates
2 |
3 | private[templates] object OptionHelpers {
4 |
5 | def flatten[T](a: Option[T], b: Option[T])(
6 | f: (T, T) => T
7 | ): Option[T] = {
8 | (a, b) match {
9 | case (None, None) => None
10 | case (Some(a), None) => Some(a)
11 | case (None, Some(b)) => Some(b)
12 | case (Some(a), Some(b)) => Some(f(a, b))
13 | }
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/core/app/builder/api_json/upgrades/RemoveApiDocElement.scala:
--------------------------------------------------------------------------------
1 | package builder.api_json.upgrades
2 |
3 | import play.api.libs.json.JsObject
4 |
5 | /**
6 | * In Oct 2022 we removed support for the top level 'apidoc' node which allowed
7 | * the user to specify the version of API Builder. This feature was never
8 | * implemented.
9 | */
10 | object RemoveApiDocElement extends Upgrader {
11 | def apply(js: JsObject): JsObject = {
12 | js - "apidoc"
13 | }
14 | }
--------------------------------------------------------------------------------
/api/conf/application.production.conf:
--------------------------------------------------------------------------------
1 | include "base.conf"
2 |
3 | db.default.url=${?CONF_DB_DEFAULT_URL}
4 | db.default.password=${?CONF_DB_DEFAULT_PASS}
5 |
6 | apibuilder.app.host=${?CONF_APIBUILDER_APP_HOST}
7 |
8 | play.http.secret.key=${?CONF_PLAY_CRYPTO_SECRET}
9 |
10 | play.modules.enabled += "modules.ProductionClientModule"
11 |
12 | sendgrid.apiKey=${?CONF_SENDGRID_API_KEY}
13 |
14 | rollbar.accessToken=${?CONF_ROLLBAR_ACCESS_TOKEN}
15 | rollbar.enabled=false
16 |
--------------------------------------------------------------------------------
/api/test/processor/CheckInvariantsProcessorSpec.scala:
--------------------------------------------------------------------------------
1 | package processor
2 |
3 | import org.scalatestplus.play.PlaySpec
4 | import org.scalatestplus.play.guice.GuiceOneAppPerSuite
5 |
6 | class CheckInvariantsProcessorSpec extends PlaySpec with GuiceOneAppPerSuite with db.Helpers {
7 |
8 | private def processor: CheckInvariantsProcessor = injector.instanceOf[CheckInvariantsProcessor]
9 |
10 | "process" in {
11 | processor.processRecord(randomString())
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/lib/src/test/scala/helpers/ServiceConfigurationHelpers.scala:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import java.util.UUID
4 | import lib.ServiceConfiguration
5 |
6 | trait ServiceConfigurationHelpers {
7 |
8 | def makeServiceConfiguration(
9 | orgNamespace: String = UUID.randomUUID.toString,
10 | ): ServiceConfiguration = {
11 | ServiceConfiguration(
12 | orgKey = "apidoc",
13 | orgNamespace = orgNamespace,
14 | version = "1.0"
15 | )
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/api/app/views/emails/applicationCreated.scala.html:
--------------------------------------------------------------------------------
1 | @(
2 | appConfig: lib.AppConfig,
3 | org: db.InternalOrganization,
4 | application: db.InternalApplication
5 | )
6 |
7 |
8 | New application named @application.name created for @org.name with visibility @application.visibility.
9 |
10 |
11 | @application.description.map { desc =>
12 |
13 | @desc
14 |
15 | }
16 |
17 |
18 | View application
19 |
20 |
--------------------------------------------------------------------------------
/api/test/actors/TaskActorSpec.scala:
--------------------------------------------------------------------------------
1 | package actors
2 |
3 | import db.Authorization
4 | import helpers.AsyncHelpers
5 | import org.scalatestplus.play.PlaySpec
6 | import org.scalatestplus.play.guice.GuiceOneAppPerSuite
7 |
8 | class TaskActorSpec extends PlaySpec with GuiceOneAppPerSuite with AsyncHelpers with db.Helpers {
9 |
10 | "run" in {
11 | val app = createApplication()
12 | eventuallyInNSeconds(10) {
13 | itemsDao.findByGuid(Authorization.All, app.guid).value
14 | }
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/app/app/lib/Pagination.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | object Pagination {
4 |
5 | val DefaultLimit = 50
6 |
7 | }
8 |
9 | case class PaginatedCollection[T](page: Int, allItems: Seq[T], limit: Int = Pagination.DefaultLimit) {
10 |
11 | def items = allItems.take(limit)
12 |
13 | lazy val hasPrevious: Boolean = {
14 | page > 0
15 | }
16 |
17 | lazy val hasNext: Boolean = {
18 | allItems.length > limit
19 | }
20 |
21 | lazy val isEmpty: Boolean = {
22 | allItems.isEmpty
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/app/app/views/account/profile/index.scala.html:
--------------------------------------------------------------------------------
1 | @(tpl: models.MainTemplate,
2 | user: io.apibuilder.api.v0.models.User
3 | )(implicit flash: Flash, messages: Messages)
4 |
5 | @main(tpl) {
6 |
7 |
10 |
11 |
12 | Email: @user.email
13 | Nickname: @user.nickname
14 | Name: @user.name
15 |
16 |
17 | }
18 |
19 |
--------------------------------------------------------------------------------
/app/app/views/versions/exampleJson.scala.html:
--------------------------------------------------------------------------------
1 | @(org: io.apibuilder.api.v0.models.Organization,
2 | app: io.apibuilder.api.v0.models.Application,
3 | version: String,
4 | typeName: String
5 | )
6 |
7 | Example Json:
8 | Minimal |
9 | Full
10 |
--------------------------------------------------------------------------------
/api/test/modules/TestClientModule.scala:
--------------------------------------------------------------------------------
1 | package modules
2 |
3 | import modules.clients._
4 | import play.api.{Configuration, Environment, Mode}
5 | import play.api.inject.Module
6 |
7 | class TestClientModule extends Module {
8 |
9 | def bindings(env: Environment, conf: Configuration) = {
10 | assert(env.mode == Mode.Test, s"Mode expected to be '${Mode.Test}' and not '${env.mode}' for class[${getClass.getName}]")
11 | Seq(
12 | bind[GeneratorClientFactory].to[TestGeneratorClientFactory]
13 | )
14 | }
15 | }
--------------------------------------------------------------------------------
/api/app/actors/Bindings.scala:
--------------------------------------------------------------------------------
1 | package actors
2 |
3 | import com.google.inject.AbstractModule
4 | import play.api.libs.concurrent.PekkoGuiceSupport
5 |
6 | class ActorsModule extends AbstractModule with PekkoGuiceSupport {
7 | override def configure(): Unit = {
8 | bindActor[ScheduleTasksActor]("ScheduleTasksActor")
9 | bindActor[TaskDispatchActor](
10 | "TaskDispatchActor",
11 | _.withDispatcher("task-context-dispatcher")
12 | )
13 | bindActorFactory[TaskActor, actors.TaskActor.Factory]
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/app/views/organizations/create.scala.html:
--------------------------------------------------------------------------------
1 | @(tpl: models.MainTemplate,
2 | form: Form[controllers.Organizations.OrgData],
3 | errorMessages: Seq[String] = Nil
4 | )(implicit flash: Flash, messages: Messages)
5 |
6 | @main(tpl.copy(title = Some("Create Organization")), errorMessages = errorMessages) {
7 |
8 |
9 |
10 | @helper.form(action = routes.Organizations.createPost) {
11 |
12 | @orgForm(form, routes.ApplicationController.index())
13 |
14 | }
15 |
16 |
17 |
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/api/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM flowdocker/play_builder:latest-java17 as builder
2 | ADD . /opt/play
3 | WORKDIR /opt/play
4 | RUN sbt 'project api' clean stage
5 |
6 | FROM flowdocker/play:latest-java17
7 | COPY --from=builder /opt/play /opt/play
8 | WORKDIR /opt/play/api/target/universal/stage
9 | ENTRYPOINT ["java", "-jar", "/root/environment-provider.jar", "--service", "play", "apibuilder-api", "bin/apibuilder-api"]
10 | HEALTHCHECK --interval=5s --timeout=5s --retries=10 \
11 | CMD curl -f http://localhost:9000/_internal_/healthcheck || exit 1
12 |
--------------------------------------------------------------------------------
/app/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM flowdocker/play_builder:latest-java17 as builder
2 | ADD . /opt/play
3 | WORKDIR /opt/play
4 | RUN sbt 'project app' clean stage
5 |
6 | FROM flowdocker/play:latest-java17
7 | COPY --from=builder /opt/play /opt/play
8 | WORKDIR /opt/play/app/target/universal/stage
9 | ENTRYPOINT ["java", "-jar", "/root/environment-provider.jar", "--service", "play", "apibuilder-app", "bin/apibuilder-app"]
10 | HEALTHCHECK --interval=5s --timeout=5s --retries=10 \
11 | CMD curl -f http://localhost:9000/_internal_/healthcheck || exit 1
12 |
--------------------------------------------------------------------------------
/app/app/views/generators/create.scala.html:
--------------------------------------------------------------------------------
1 | @(tpl: models.MainTemplate,
2 | form: Form[controllers.Generators.GeneratorServiceCreateFormData],
3 | errorMessages: Seq[String] = Nil
4 | )(implicit flash: Flash, messages: Messages)
5 |
6 | @main(tpl.copy(title = Some("Add Generator")), errorMessages = errorMessages) {
7 |
8 |
9 |
10 | @helper.form(action = routes.Generators.createPost()) {
11 |
12 | @generatorForm(form, routes.Generators.index())
13 |
14 | }
15 |
16 |
17 |
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/api/test/helpers/OrganizationHelpers.scala:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import db.InternalOrganization
4 | import io.apibuilder.api.v0.models.{BatchDownloadApplicationForm, BatchDownloadApplicationsForm, Organization}
5 | import models.OrganizationsModel
6 | import org.scalatestplus.play.PlaySpec
7 |
8 | trait OrganizationHelpers extends db.Helpers {
9 |
10 | private def orgModel: OrganizationsModel = injector.instanceOf[OrganizationsModel]
11 | def toModel(org: InternalOrganization): Organization = {
12 | orgModel.toModel(org)
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/app/app/views/attributes/create.scala.html:
--------------------------------------------------------------------------------
1 | @(tpl: models.MainTemplate,
2 | form: Form[controllers.AttributesController.AttributeFormData],
3 | errorMessages: Seq[String] = Nil
4 | )(implicit flash: Flash, messages: Messages)
5 |
6 | @main(tpl.copy(title = Some("Add Attribute")), errorMessages = errorMessages) {
7 |
8 |
9 |
10 | @helper.form(action = routes.AttributesController.createPost()) {
11 |
12 | @attributeForm(form, routes.AttributesController.index())
13 |
14 | }
15 |
16 |
17 |
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/api/app/db/Filters.scala:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | object Filters {
4 |
5 | def isDeleted(
6 | tableName: String,
7 | value: Boolean
8 | ): String = {
9 | if (value) {
10 | s"$tableName.deleted_at is not null"
11 | } else {
12 | s"$tableName.deleted_at is null"
13 | }
14 | }
15 |
16 | def isExpired(
17 | tableName: String,
18 | value: Boolean
19 | ): String = {
20 | if (value) {
21 | s"$tableName.expires_at < now()"
22 | } else {
23 | s"$tableName.expires_at >= now()"
24 | }
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/api/app/modules/ProductionClientModule.scala:
--------------------------------------------------------------------------------
1 | package modules
2 |
3 | import modules.clients._
4 | import play.api.{Configuration, Environment, Mode}
5 | import play.api.inject.Module
6 |
7 | class ProductionClientModule extends Module {
8 |
9 | def bindings(env: Environment, conf: Configuration) = {
10 | assert(env.mode == Mode.Prod || env.mode == Mode.Dev, s"Mode expected to be '${Mode.Prod}' or '${Mode.Dev}' and not '${env.mode}' for class[${getClass.getName}]")
11 | Seq(
12 | bind[GeneratorClientFactory].to[ProductionGeneratorClientFactory]
13 | )
14 | }
15 | }
--------------------------------------------------------------------------------
/core/test/core/FileServiceFetcher.scala:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import io.apibuilder.spec.v0.models.Service
4 | import io.apibuilder.spec.v0.models.json._
5 | import play.api.libs.json.Json
6 | import java.net.URI
7 |
8 | case class FileServiceFetcher() extends ServiceFetcher {
9 |
10 | override def fetch(uri: String): Service = {
11 | val source = scala.io.Source.fromURI(new URI(uri))
12 | try {
13 | val contents = source.getLines().mkString
14 | Json.parse(contents).as[Service]
15 | } finally {
16 | source.close()
17 | }
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/app/app/views/organizations/edit.scala.html:
--------------------------------------------------------------------------------
1 | @(tpl: models.MainTemplate,
2 | org: io.apibuilder.api.v0.models.Organization,
3 | form: Form[controllers.Organizations.OrgData],
4 | errorMessages: Seq[String] = Nil
5 | )(implicit flash: Flash, messages: Messages)
6 |
7 | @main(tpl.copy(title = Some("Edit Organization")), errorMessages = errorMessages) {
8 |
9 |
10 |
11 | @helper.form(action = routes.Organizations.editPost(org.key)) {
12 |
13 | @orgForm(form, routes.Organizations.show(org.key))
14 |
15 | }
16 |
17 |
18 |
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/api/app/views/emails/invariants.scala.html:
--------------------------------------------------------------------------------
1 | @(
2 | appConfig: lib.AppConfig,
3 | noErrors: Seq[String],
4 | withErrors: Seq[processor.InvariantResult]
5 | )
6 |
7 |
8 | The following invariants resulted in a non zero value
9 |
10 |
11 |
12 | @for(i <- withErrors) {
13 | @{i.invariant.name}: @{i.count}
14 | @{i.invariant.query.interpolate()}
15 |
16 | }
17 |
18 |
19 |
20 | The following invariants were all valid
21 |
22 |
23 |
24 | @for(i <- noErrors) {
25 | @i
26 | }
27 |
28 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | // Comment to get more information during initialization
2 | logLevel := Level.Warn
3 |
4 | // The Typesafe repository
5 | resolvers += "Typesafe repository" at "https://repo.typesafe.com/typesafe/releases/"
6 |
7 | // Use the Play sbt plugin for Play projects
8 | addSbtPlugin("org.playframework" % "sbt-plugin" % "3.0.6")
9 |
10 | addSbtPlugin("org.playframework.twirl" % "sbt-twirl" % "2.0.7")
11 |
12 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.1")
13 |
14 | addSbtPlugin("com.github.sbt" % "sbt-javaagent" % "0.1.8")
15 |
16 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.5")
17 |
--------------------------------------------------------------------------------
/app/app/views/generators/generatorForm.scala.html:
--------------------------------------------------------------------------------
1 | @(form: Form[controllers.Generators.GeneratorServiceCreateFormData],
2 | cancelUrl: play.api.mvc.Call
3 | )(implicit flash: Flash, messages: Messages)
4 |
5 |
6 | @helper.inputText(
7 | form("uri"),
8 | Symbol("_label") -> "Service URI",
9 | Symbol("_help") -> "The full URI to the service providing the code generator.",
10 | Symbol("_error") -> form.error("uri")
11 | )
12 |
13 |
14 | Submit
15 | Cancel
16 |
--------------------------------------------------------------------------------
/script/lib/ask.rb:
--------------------------------------------------------------------------------
1 | class Ask
2 |
3 | TRUE = ['y', 'yes']
4 | FALSE = ['n', 'no']
5 |
6 | def Ask.for_string(msg)
7 | value = ""
8 | while value.empty?
9 | puts msg
10 | value = STDIN.gets
11 | value.strip!
12 | end
13 | value
14 | end
15 |
16 | def Ask.for_boolean(msg)
17 | result = nil
18 | while result.nil?
19 | value = Ask.for_string(msg).downcase
20 | if TRUE.include?(value)
21 | result = true
22 | elsif FALSE.include?(value)
23 | result = false
24 | end
25 | end
26 | result
27 | end
28 |
29 | end
30 |
31 |
--------------------------------------------------------------------------------
/app/app/views/doc/typeInfo.scala.html:
--------------------------------------------------------------------------------
1 | @(field: String)
2 |
3 | specifies the type of this @field. Acceptable values include the name
4 | of either an enum, a model, or a
5 | (primitive type ). To specify
6 | a List, the type name can be wrapped with "[]". For example, to
7 | specify that the type is a collection of strings, use "[string]". To
8 | specify a Map, the type name can be prefixed with "map[type]". For
9 | example, to specify that the type is a Map of string to long, use
10 | "map[long]". Note that for map, the keys must be strings (per the
11 | JSON specification).
12 |
--------------------------------------------------------------------------------
/api/app/invariants/GeneratorInvariants.scala:
--------------------------------------------------------------------------------
1 | package invariants
2 |
3 | import io.flow.postgresql.Query
4 | import org.joda.time.DateTime
5 |
6 | object GeneratorInvariants {
7 | val all: Seq[Invariant] = Seq(
8 | Invariant(
9 | s"deleted_services_have_deleted_generators",
10 | Query(
11 | """
12 | |select count(*)
13 | | from generators.generators g
14 | | join generators.services s on s.guid = g.service_guid
15 | | where s.deleted_at is not null
16 | | and g.deleted_at is null
17 | |""".stripMargin
18 | )
19 | )
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/app/app/controllers/LogoutController.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import models.MainTemplate
4 | import play.api.mvc.{Action, AnyContent}
5 |
6 | import javax.inject.Inject
7 |
8 | class LogoutController @Inject() (
9 | val apiBuilderControllerComponents: ApiBuilderControllerComponents
10 | ) extends ApiBuilderController {
11 |
12 |
13 | def logged_out: Action[AnyContent] = Action { implicit request =>
14 | Ok(views.html.logged_out(MainTemplate(requestPath = request.path)))
15 | }
16 |
17 | def logout: Action[AnyContent] = Action {
18 | Redirect("/logged_out").withNewSession
19 | }
20 |
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/api/app/invariants/TaskInvariants.scala:
--------------------------------------------------------------------------------
1 | package invariants
2 |
3 | import io.flow.postgresql.Query
4 |
5 | object TaskInvariants {
6 | val all: Seq[Invariant] = Seq(
7 | Invariant(
8 | "tasks_not_attempted",
9 | Query("""
10 | |select count(*) from tasks where num_attempts=0 and created_at < now() - interval '1 hour'
11 | |""".stripMargin)
12 | ),
13 | Invariant(
14 | "tasks_not_completed_in_12_hours",
15 | Query("""
16 | |select count(*) from tasks where num_attempts>0 and created_at < now() - interval '12 hour'
17 | |""".stripMargin)
18 | )
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/core/app/DuplicateJsonParser.scala:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import cats.implicits._
4 | import cats.data.ValidatedNec
5 | import io.circe.ParsingFailure
6 | import io.circe.jawn.JawnParser
7 |
8 |
9 | // scala> parser.decode[Map[String, Int]]("""{"a":1,"a":2}""")
10 |
11 | object DuplicateJsonParser {
12 |
13 | def validateDuplicates(value: String): ValidatedNec[String, Unit] = {
14 | val parser = new JawnParser(maxValueSize = None, allowDuplicateKeys = false)
15 |
16 | parser.parse(value) match {
17 | case Left(er: ParsingFailure) => er.message.invalidNec
18 | case _ => ().validNec
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/api/app/models/DomainsModel.scala:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import db.*
4 | import io.apibuilder.api.v0.models.Domain
5 | import io.apibuilder.common.v0.models.{Audit, ReferenceGuid}
6 |
7 | import java.util.UUID
8 | import javax.inject.Inject
9 |
10 | class DomainsModel @Inject()(
11 | domainsDao: InternalOrganizationDomainsDao,
12 | ) {
13 | def toModel(v: InternalOrganizationDomain): Domain = {
14 | toModels(Seq(v)).head
15 | }
16 |
17 | def toModels(domains: Seq[InternalOrganizationDomain]): Seq[Domain] = {
18 | domains.map { dom =>
19 | Domain(
20 | name = dom.db.domain,
21 | )
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/app/app/views/login/forgotPasswordConfirmation.scala.html:
--------------------------------------------------------------------------------
1 | @(tpl: models.MainTemplate, email: String)(implicit flash: Flash, messages: Messages)
2 |
3 | @main(tpl.copy(headTitle = Some("Forgot Password"))) {
4 |
5 |
6 | If there is an account with the email address @email , we
7 | have sent an email message containing a link to reset your
8 | password. If this account does not exist, you will not receive an
9 | email message and you may need to register.
10 |
11 |
12 |
13 | Login
14 |
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/avro/example.avdl:
--------------------------------------------------------------------------------
1 | @namespace("mynamespace")
2 | protocol MyProtocol {
3 |
4 | enum Suit {
5 | SPADES, DIAMONDS, CLUBS, HEARTS
6 | }
7 |
8 | fixed MD5(16);
9 |
10 | record Employee {
11 | string name;
12 | boolean active = true;
13 | long salary;
14 | }
15 |
16 | record Contractor {
17 | string name;
18 | }
19 |
20 | record Members {
21 | string company;
22 | union {Employee, Contractor} user;
23 | }
24 |
25 | error Kaboom {
26 | string explanation;
27 | int result_code = -1;
28 | }
29 |
30 | record Card {
31 | Suit suit;
32 | int number;
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/SETUP.md:
--------------------------------------------------------------------------------
1 | Deploying a schema change
2 | =========================
3 | Log into EC2 instance in security group w/ access to the database
4 | (e.g. an API instance)
5 |
6 | sudo apt-get --force-yes -y install postgresql-client git ruby
7 |
8 | git clone git://github.com/gilt/schema-evolution-manager.git
9 | cd schema-evolution-manager
10 | git checkout 0.9.24
11 | ruby ./configure.rb
12 | sudo ruby ./install.rb
13 |
14 | echo "apidoc2.cqe9ob8rnh0u.us-east-1.rds.amazonaws.com:5432:apidoc:web:PASSWORD" > ~/.pgpass
15 | chmod 0600 ~/.pgpass
16 |
17 | sem-apply --host apidoc2.cqe9ob8rnh0u.us-east-1.rds.amazonaws.com --name apidoc --user web
--------------------------------------------------------------------------------
/core/app/builder/api_json/upgrades/Upgrader.scala:
--------------------------------------------------------------------------------
1 | package builder.api_json.upgrades
2 |
3 | import play.api.libs.json.{JsObject, JsValue}
4 |
5 | trait Upgrader {
6 | final def apply(js: JsValue): JsValue = {
7 | js match {
8 | case o: JsObject => apply(o)
9 | case j => j
10 | }
11 | }
12 |
13 | def apply(js: JsObject): JsObject
14 | }
15 |
16 | object AllUpgrades {
17 |
18 | private val all: Seq[Upgrader] = List(RemoveApiDocElement, InterfacesToSupportResources)
19 |
20 | def apply(js: JsValue): JsValue = {
21 | all.foldLeft(js) { case (j, upgrader) =>
22 | upgrader.apply(j)
23 | }
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/core/test/helpers/ValidatedTestHelpers.scala:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import cats.data.Validated.{Invalid, Valid}
4 | import cats.data.ValidatedNec
5 |
6 | trait ValidatedTestHelpers {
7 |
8 | def expectValid[T, U](r: ValidatedNec[T, U]): U = {
9 | r match {
10 | case Valid(o) => o
11 | case Invalid(errors) => sys.error(s"Expected valid but was invalid: ${errors.toNonEmptyList}")
12 | }
13 | }
14 |
15 | def expectInvalid[T, U](r: ValidatedNec[T, U]): Seq[T] = {
16 | r match {
17 | case Valid(_) => sys.error("Expected invalid but was valid")
18 | case Invalid(errors) => errors.toNonEmptyList.toList
19 | }
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/core/app/builder/api_json/templates/MapMerge.scala:
--------------------------------------------------------------------------------
1 | package builder.api_json.templates
2 |
3 | abstract class MapMerge[T]() {
4 |
5 | def merge(a: T, b: T): T
6 |
7 | final def merge(a: Option[Map[String, T]], b: Option[Map[String, T]]): Option[Map[String, T]] = {
8 | OptionHelpers.flatten(a, b)(merge)
9 | }
10 |
11 | final def merge(a: Map[String, T], b: Map[String, T]): Map[String, T] = {
12 | a.filterNot { case (n, _) => b.contains(n) } ++ b.map { case (name, bInstance) =>
13 | a.get(name) match {
14 | case None => name -> bInstance
15 | case Some(aInstance) => name -> merge(aInstance, bInstance)
16 | }
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/core/test/core/MockServiceFetcher.scala:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import io.apibuilder.spec.v0.models.Service
4 |
5 | import scala.collection.mutable
6 |
7 | trait ServiceFetcher {
8 |
9 | def fetch(uri: String): Service
10 |
11 | }
12 |
13 | case class MockServiceFetcher() extends ServiceFetcher {
14 |
15 | val services: mutable.Map[String, Service] = scala.collection.mutable.Map[String, Service]()
16 |
17 | def add(uri: String, service: Service): Unit = {
18 | services += (uri -> service)
19 | }
20 |
21 | override def fetch(uri: String): Service = {
22 | services.getOrElse(uri, sys.error(s"No mock found for imported service w/ uri[$uri]"))
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/api/app/processor/TaskDispatchActorCompanion.scala:
--------------------------------------------------------------------------------
1 | package processor
2 |
3 | import anorm.SqlParser
4 | import io.apibuilder.task.v0.models.TaskType
5 | import io.flow.postgresql.Query
6 | import play.api.db.Database
7 |
8 | import javax.inject.Inject
9 |
10 | class TaskDispatchActorCompanion @Inject() (
11 | database: Database
12 | ) {
13 | private val TypesQuery = Query(
14 | "select distinct type from tasks where next_attempt_at <= now()"
15 | )
16 |
17 | def typesWithWork: Seq[TaskType] = {
18 | database
19 | .withConnection { c =>
20 | TypesQuery.as(SqlParser.str(1).*)(using c)
21 | }
22 | .flatMap(TaskType.fromString)
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/lib/src/test/scala/helpers/ValidatedTestHelpers.scala:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import cats.data.Validated.{Invalid, Valid}
4 | import cats.data.ValidatedNec
5 |
6 | trait ValidatedTestHelpers {
7 |
8 | def expectValid[S, T](value: ValidatedNec[S, T]): T = {
9 | value match {
10 | case Invalid(e) => sys.error(s"Expected valid but got: ${e.toNonEmptyList.toList.mkString(", ")}")
11 | case Valid(v) => v
12 | }
13 | }
14 |
15 | def expectInvalid[T, U](r: ValidatedNec[T, U]): Seq[T] = {
16 | r match {
17 | case Valid(_) => sys.error("Expected invalid but was valid")
18 | case Invalid(errors) => errors.toNonEmptyList.toList
19 | }
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/api/app/modules/clients/ProductionGeneratorClientFactory.scala:
--------------------------------------------------------------------------------
1 | package modules.clients
2 |
3 | import io.apibuilder.generator.v0.interfaces.{Client => ClientInterface}
4 | import io.apibuilder.generator.v0.{Client => GeneratorClient}
5 | import play.api.libs.ws.WSClient
6 |
7 | import javax.inject.Inject
8 |
9 | trait GeneratorClientFactory {
10 | def instance(baseUrl: String): ClientInterface
11 | }
12 |
13 | class ProductionGeneratorClientFactory @Inject() (
14 | ws: WSClient
15 | ) extends GeneratorClientFactory {
16 |
17 | def instance(baseUrl: String): GeneratorClient = {
18 | new GeneratorClient(
19 | ws = ws,
20 | baseUrl = baseUrl
21 | )
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/api/test/lib/TokenGeneratorSpec.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import org.scalatestplus.play.PlaySpec
4 | import org.scalatestplus.play.guice.GuiceOneAppPerSuite
5 |
6 | class TokenGeneratorSpec extends PlaySpec with GuiceOneAppPerSuite {
7 |
8 | "generates unique tokens" in {
9 | val tokens = (1 to 100).map { _ => TokenGenerator.generate() }
10 | tokens.distinct.sorted must be(tokens.sorted)
11 | }
12 |
13 | "generates tokens that are long" in {
14 | TokenGenerator.generate().length >= 80 mustBe true
15 | TokenGenerator.generate(100).length mustBe 100
16 | }
17 |
18 | "generates tokens that are short" in {
19 | TokenGenerator.generate(5).length mustBe 5
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/app/views/tokens/show.scala.html:
--------------------------------------------------------------------------------
1 | @(tpl: models.MainTemplate,
2 | token: io.apibuilder.api.v0.models.Token
3 | )(implicit flash: Flash, messages: Messages)
4 |
5 | @main(tpl) {
6 |
7 |
8 | Full token: @token.maskedToken (Show full token )
9 | Description: @Html(lib.Markdown.toHtml(token.description.getOrElse("~none~")))
10 | Created: @lib.DateHelper.longDateTime(tpl.timeZone, token.audit.createdAt)
11 |
12 |
13 |
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/core/app/builder/api_json/templates/AttributeMerge.scala:
--------------------------------------------------------------------------------
1 | package builder.api_json.templates
2 |
3 | import io.apibuilder.api.json.v0.models.Attribute
4 |
5 | trait AttributeMerge {
6 | private val merger = new ArrayMerge[Attribute] {
7 | override def uniqueIdentifier(i: Attribute): String = i.name
8 |
9 | override def merge(original: Attribute, tpl: Attribute): Attribute = {
10 | Attribute(
11 | name = original.name,
12 | value = tpl.value ++ original.value
13 | )
14 | }
15 | }
16 |
17 | def mergeAttributes(original: Option[Seq[Attribute]], tpl: Option[Seq[Attribute]]): Option[Seq[Attribute]] = {
18 | merger.merge(original, tpl)
19 | }
20 |
21 | }
--------------------------------------------------------------------------------
/app/app/views/login/forgotPassword.scala.html:
--------------------------------------------------------------------------------
1 | @(tpl: models.MainTemplate,
2 | form: Form[controllers.LoginController.ForgotPasswordData]
3 | )(implicit flash: Flash, messages: Messages)
4 |
5 | @main(tpl.copy(headTitle = Some("Forgot Password"))) {
6 |
7 | @helper.form(action = routes.LoginController.postForgotPassword) {
8 |
9 |
10 |
11 | @helper.inputText(
12 | form("email"),
13 | Symbol("_label") -> "Email address",
14 | Symbol("_error") -> form.error("email")
15 | )
16 |
17 |
18 |
19 | Send Email to Reset Password
20 |
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/app/conf/base.conf:
--------------------------------------------------------------------------------
1 | play.i18n.langs=["en"]
2 |
3 | play.filters.disabled += "play.filters.csrf.CSRFFilter"
4 | play.filters.disabled += "play.filters.hosts.AllowedHostsFilter"
5 | play.filters.disabled += "play.filters.headers.SecurityHeadersFilter"
6 | play.http.parser.maxMemoryBuffer=10M
7 | play.http.requestHandler = "play.http.DefaultHttpRequestHandler"
8 |
9 | apibuilder.supportEmail="mbryzek@gmail.com"
10 | apibuilder.github.oauth.client.secret=${?CONF_APIBUILDER_GITHUB_OAUTH_CLIENT_SECRET}
11 |
12 | pekko {
13 | actor {
14 | default-dispatcher {
15 | fork-join-executor {
16 | parallelism-min = 8
17 | parallelism-max = 128
18 | }
19 | }
20 | }
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/api/app/util/Conversions.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import db.InternalUser
4 | import io.apibuilder.api.v0.models.{Authentication, Session, User}
5 | import models.UsersModel
6 | import javax.inject.Inject
7 |
8 | class Conversions @Inject() (
9 | usersModel: UsersModel
10 | ) {
11 |
12 | private def toSession(db: _root_.db.generated.Session): Session = {
13 | Session(
14 | id = db.id,
15 | expiresAt = db.expiresAt
16 | )
17 | }
18 |
19 | def toAuthentication(dbSession: _root_.db.generated.Session, user: InternalUser): Authentication = {
20 | Authentication(
21 | session = toSession(dbSession),
22 | user = usersModel.toModel(user)
23 | )
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/avro/src/main/scala/io/apibuilder/avro/AvroIdlServiceValidator.scala:
--------------------------------------------------------------------------------
1 | package io.apibuilder.avro
2 |
3 | import cats.implicits._
4 | import cats.data.ValidatedNec
5 | import lib.{ServiceConfiguration, ServiceValidator}
6 | import scala.util.{Failure, Success, Try}
7 | import io.apibuilder.spec.v0.models.Service
8 |
9 | case class AvroIdlServiceValidator(
10 | config: ServiceConfiguration
11 | ) extends ServiceValidator[Service] {
12 |
13 | override def validate(rawInput: String): ValidatedNec[String, Service] = {
14 | Try(Parser(config).parseString(rawInput)) match {
15 | case Failure(ex) => ex.toString.invalidNec
16 | case Success(service) => service.validNec
17 | }
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/api/app/processor/MigrateVersionProcessor.scala:
--------------------------------------------------------------------------------
1 | package processor
2 |
3 | import cats.data.ValidatedNec
4 | import db.InternalVersionsDao
5 | import io.apibuilder.task.v0.models.TaskType
6 |
7 | import java.util.UUID
8 | import javax.inject.Inject
9 |
10 | object MigrateVersion {
11 | val ServiceVersionNumber: String = io.apibuilder.spec.v0.Constants.Version.toLowerCase
12 | }
13 |
14 | class MigrateVersionProcessor @Inject()(
15 | args: TaskProcessorArgs,
16 | versionsDao: InternalVersionsDao
17 | ) extends TaskProcessorWithGuid(args, TaskType.MigrateVersion) {
18 |
19 | override def processRecord(versionGuid: UUID): ValidatedNec[String, Unit] = {
20 | versionsDao.migrateVersionGuid(versionGuid)
21 | }
22 | }
--------------------------------------------------------------------------------
/api/test/db/InternalTokensDaoSpec.scala:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import io.apibuilder.api.v0.models.TokenForm
4 | import org.scalatestplus.play.PlaySpec
5 | import org.scalatestplus.play.guice.GuiceOneAppPerSuite
6 |
7 | class InternalTokensDaoSpec extends PlaySpec with GuiceOneAppPerSuite with db.Helpers {
8 |
9 | "obfuscates token by default" in {
10 | val user = upsertUser()
11 | val token = expectValid {
12 | tokensDao.create(
13 | user,
14 | TokenForm(userGuid = user.guid)
15 | )
16 | }
17 | token.userGuid must be(user.guid)
18 | token.maskedToken must be("XXX-XXX-XXX")
19 |
20 | tokensDao.findByToken(token.db.token).get.guid must be(token.guid)
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/app/app/views/versions/body.scala.html:
--------------------------------------------------------------------------------
1 | @(org: io.apibuilder.api.v0.models.Organization,
2 | app: io.apibuilder.api.v0.models.Application,
3 | version: String,
4 | service: io.apibuilder.spec.v0.models.Service,
5 | body: io.apibuilder.spec.v0.models.Body
6 | )
7 |
8 |
9 |
10 |
11 | Type
12 | Description
13 |
14 |
15 |
16 |
17 | @datatype(org, app, version, service, body.`type`)
18 | @Html(lib.Markdown.toHtml(body.description.getOrElse("N/A")))
19 | @body.deprecation.map(deprecation(_))
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/api/app/invariants/PurgeInvariants.scala:
--------------------------------------------------------------------------------
1 | package invariants
2 |
3 | import io.flow.postgresql.Query
4 | import org.joda.time.DateTime
5 |
6 | object PurgeInvariants {
7 | private case class PurgeTable(name: String, retentionMonths: Int)
8 | private val Tables = Seq(
9 | PurgeTable("organizations", 12),
10 | PurgeTable("applications", 6),
11 | PurgeTable("versions", 6)
12 | )
13 |
14 | val all: Seq[Invariant] = Tables.map { t =>
15 | Invariant(
16 | s"old_deleted_records_purged_from_${t.name}",
17 | Query(
18 | s"select count(*) from ${t.name} where deleted_at < {deleted_at}::timestamptz"
19 | ).bind("deleted_at", DateTime.now.minusMonths(t.retentionMonths+1))
20 | )
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/api/test/util/SessionIdGeneratorSpec.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import org.scalatestplus.play.PlaySpec
4 | import org.scalatestplus.play.guice.GuiceOneAppPerSuite
5 |
6 | class SessionIdGeneratorSpec extends PlaySpec with GuiceOneAppPerSuite {
7 |
8 | "starts with prefix" in {
9 | SessionIdGenerator.generate().startsWith("A51") must be (true)
10 | }
11 |
12 | "64 characters long" in {
13 | SessionIdGenerator.generate().length must be(64)
14 | }
15 |
16 | "generates unique identifiers" in {
17 | val s = collection.mutable.Set[String]()
18 |
19 | 1.to(100000).foreach { _ =>
20 | val tn = SessionIdGenerator.generate()
21 | s(tn) must be (false)
22 | s += tn
23 | }
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/app/app/controllers/AccountController.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import play.api.mvc.{Action, AnyContent}
4 |
5 | import javax.inject.Inject
6 | import scala.concurrent.ExecutionContext
7 |
8 | class AccountController @Inject() (
9 | val apiBuilderControllerComponents: ApiBuilderControllerComponents
10 | ) extends ApiBuilderController {
11 |
12 | private implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global
13 |
14 | def redirect: Action[AnyContent] = Action { implicit request =>
15 | Redirect(routes.AccountController.index())
16 | }
17 |
18 | def index(): Action[AnyContent] = Identified { implicit request =>
19 | Redirect(routes.AccountProfileController.index())
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | push:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | strategy:
15 | matrix:
16 | java: [ '17' ]
17 | scala: [ '3.4.2' ]
18 | steps:
19 | - uses: actions/checkout@v2
20 | - name: Set up JDK
21 | uses: actions/setup-java@v2
22 | with:
23 | java-version: ${{ matrix.java }}
24 | distribution: 'zulu'
25 | - name: Build
26 | run: |
27 | docker run -d -p 127.0.0.1:5432:5432 flowcommerce/apibuilder-postgresql:latest-pg15
28 | CONF_APIBUILDER_API_HOST="http://localhost:9001" sbt ++${{ matrix.scala }} clean test
29 |
--------------------------------------------------------------------------------
/lib/src/main/scala/VersionedName.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | case class VersionedName(
4 | name: String,
5 | version: Option[String] = None
6 | ) extends Ordered[VersionedName] {
7 |
8 | private[lib] val versionTag = version.map(VersionTag(_))
9 |
10 | val label = version match {
11 | case None => name
12 | case Some(v) => s"$name:$v"
13 | }
14 |
15 | def compare(that: VersionedName) = {
16 | if (versionTag.isEmpty && that.versionTag.isEmpty) {
17 | 0
18 | } else if (versionTag.isEmpty && !that.versionTag.isEmpty) {
19 | 1
20 | } else if (!versionTag.isEmpty && that.versionTag.isEmpty) {
21 | -1
22 | } else {
23 | versionTag.get.compare(that.versionTag.get)
24 | }
25 | }
26 |
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/app/app/views/mainHead.scala.html:
--------------------------------------------------------------------------------
1 | @(headerTitle: Option[String])
2 |
3 |
4 |
5 |
6 |
7 |
8 | @headerTitle.getOrElse("API Builder")
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/api/app/lib/TokenGenerator.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import java.security.SecureRandom
4 | import java.util.UUID
5 | import scala.util.Random
6 |
7 | object TokenGenerator {
8 |
9 | private val random = new Random(new SecureRandom())
10 | private val Alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
11 |
12 | def generate(n: Int = 80): String = {
13 | val uuid = UUID.randomUUID().toString.replaceAll("-", "")
14 | val numberRandom = n - uuid.length
15 | if (numberRandom > 0) {
16 | uuid + random(numberRandom)
17 | } else {
18 | random(n)
19 | }
20 | }
21 |
22 | private def random(n: Int): String = {
23 | LazyList.continually(random.nextInt(Alphabet.length)).map(Alphabet).take(n).mkString
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/api/app/models/OriginalsModel.scala:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import builder.api_json.upgrades.ServiceParser
4 | import cats.data.Validated.{Invalid, Valid}
5 | import cats.data.ValidatedNec
6 | import cats.implicits.*
7 | import db.*
8 | import io.apibuilder.api.v0.models.Original
9 | import io.apibuilder.common.v0.models.Reference
10 | import io.apibuilder.spec.v0.models.Service
11 |
12 | import javax.inject.Inject
13 |
14 | class OriginalsModel @Inject()() {
15 |
16 | def toModel(v: InternalOriginal): Original = {
17 | toModels(Seq(v)).head
18 | }
19 |
20 | def toModels(originals: Seq[InternalOriginal]): Seq[Original] = {
21 | originals.map { o =>
22 | Original(
23 | `type` = o.`type`,
24 | data = o.data,
25 | )
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/core/test/core/builder/api_json/upgrades/RemoveApiDocElementSpec.scala:
--------------------------------------------------------------------------------
1 | package builder.api_json.upgrades
2 |
3 | import core.TestHelper
4 | import helpers.ApiJsonHelpers
5 | import org.scalatest.funspec.AnyFunSpec
6 | import org.scalatest.matchers.should.Matchers
7 |
8 | import scala.annotation.nowarn
9 |
10 |
11 | @nowarn("msg=value apidoc in class Service is deprecated")
12 | class RemoveApiDocElementSpec extends AnyFunSpec with Matchers with ApiJsonHelpers{
13 |
14 | it("accepts json with apidoc node") {
15 | setupValidApiJson(
16 | """
17 | |{
18 | | "name": "API Builder",
19 | | "apidoc": {
20 | | "version": "1.0"
21 | | }
22 | |}
23 | |""".stripMargin
24 | ).apidoc shouldBe None
25 | }
26 |
27 | }
--------------------------------------------------------------------------------
/api/test/util/RandomPortFinder.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import java.io.IOException
4 | import java.net.{InetSocketAddress, Socket}
5 |
6 | object RandomPortFinder {
7 |
8 | def getRandomPort: Int = {
9 | LazyList
10 | .continually(randomPort)
11 | .dropWhile(port => !checkIfPortIsFree("localhost", port))
12 | .head
13 | }
14 |
15 | private def randomPort: Int = {
16 | 10000 + scala.util.Random.nextInt(55000)
17 | }
18 |
19 | private def checkIfPortIsFree(host: String, port: Int): Boolean = {
20 | val s = new Socket()
21 | try {
22 | s.connect(new InetSocketAddress(host, port), 1000)
23 | false
24 | } catch {
25 | case _: IOException => true
26 | } finally {
27 | s.close()
28 | }
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/app/app/views/doc/examples.scala.html:
--------------------------------------------------------------------------------
1 | @(
2 | util: lib.Util,
3 | user: Option[io.apibuilder.api.v0.models.User]
4 | )
5 |
6 | @doc.main(routes.DocController.examples.url, user, Some("Examples")) {
7 |
8 |
9 |
10 | One of the best ways to learn how to use API Builder is by looking at
11 | example definitions of existing services.
12 |
13 |
14 |
15 |
16 |
17 |
18 | @lib.Labels.Examples.map { example =>
19 |
20 | @example.label
21 | Online docs
22 | api.json
23 |
24 | }
25 |
26 |
27 |
28 |
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/core/app/builder/DuplicateErrorMessage.scala:
--------------------------------------------------------------------------------
1 | package builder
2 |
3 | import lib.Text
4 |
5 | import cats.implicits._
6 | import cats.data.ValidatedNec
7 |
8 | private[builder] object DuplicateErrorMessage {
9 | def validate(label: String, values: Iterable[String]): ValidatedNec[String, Unit] = {
10 | findDuplicates(values) match {
11 | case Nil => ().validNec
12 | case dups => dups.map { n =>
13 | s"$label[$n] appears more than once".invalidNec
14 | }.sequence.map(_ => ())
15 | }
16 | }
17 |
18 | private def findDuplicates(values: Iterable[String]): List[String] = {
19 | values.groupBy(Text.camelCaseToUnderscore(_).toLowerCase.trim)
20 | .filter {
21 | _._2.size > 1
22 | }
23 | .keys.toList.distinct.sorted
24 | }
25 | }
--------------------------------------------------------------------------------
/app/app/views/versions/values.scala.html:
--------------------------------------------------------------------------------
1 | @(org: io.apibuilder.api.v0.models.Organization,
2 | app: io.apibuilder.api.v0.models.Application,
3 | version: String,
4 | e: io.apibuilder.spec.v0.models.Enum)
5 |
6 |
7 |
8 |
9 | Name
10 | Value
11 | Description
12 |
13 |
14 |
15 | @e.deprecation.map { depr =>
16 |
17 | @depr
18 |
19 | }
20 | @e.values.map { value =>
21 |
22 | @value.name
23 | @value.value.getOrElse(value.name)
24 | @Html(lib.Markdown(value.description))
25 |
26 | }
27 |
28 |
29 |
--------------------------------------------------------------------------------
/core/app/builder/api_json/templates/ArrayMerge.scala:
--------------------------------------------------------------------------------
1 | package builder.api_json.templates
2 |
3 | abstract class ArrayMerge[T]() {
4 | def uniqueIdentifier(i: T): String
5 |
6 | def merge(a: T, b: T): T
7 |
8 | final def merge(a: Option[Seq[T]], b: Option[Seq[T]]): Option[Seq[T]] = {
9 | OptionHelpers.flatten(a, b)(merge)
10 | }
11 |
12 | final def merge(a: Seq[T], b: Seq[T]): Seq[T] = {
13 | val aByLabel = a.map { i => uniqueIdentifier(i) -> i }.toMap
14 | val bByLabel = b.map { i => uniqueIdentifier(i) -> i }.toMap
15 | val all = a.map(uniqueIdentifier) ++ b.map(uniqueIdentifier).filterNot(aByLabel.contains)
16 | all.flatMap { identifier =>
17 | OptionHelpers.flatten(aByLabel.get(identifier), bByLabel.get(identifier))(merge)
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/swagger/src/main/scala/io/apibuilder/swagger/SwaggerServiceValidator.scala:
--------------------------------------------------------------------------------
1 | package io.apibuilder.swagger
2 |
3 | import cats.implicits._
4 | import cats.data.ValidatedNec
5 | import lib.{ServiceConfiguration, ServiceValidator}
6 | import scala.util.{Failure, Success, Try}
7 | import io.apibuilder.spec.v0.models.Service
8 |
9 | case class SwaggerServiceValidator(
10 | config: ServiceConfiguration
11 | ) extends ServiceValidator[Service] {
12 |
13 | override def validate(rawInput: String): ValidatedNec[String, Service] = {
14 | Try(Parser(config).parseString(rawInput)) match {
15 | case Failure(ex) => {
16 | ex.printStackTrace(System.err)
17 | ex.toString.invalidNec
18 | }
19 | case Success(service) => service.validNec
20 | }
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/api/app/actors/TaskActor.scala:
--------------------------------------------------------------------------------
1 | package actors
2 |
3 | import org.apache.pekko.actor.{Actor, ActorLogging}
4 | import com.google.inject.assistedinject.Assisted
5 | import io.apibuilder.task.v0.models.TaskType
6 | import processor.TaskActorCompanion
7 |
8 | import javax.inject.Inject
9 |
10 | object TaskActor {
11 | case object Process
12 | trait Factory {
13 | def apply(
14 | @Assisted("type") `type`: TaskType
15 | ): Actor
16 | }
17 | }
18 |
19 | class TaskActor @Inject() (
20 | @Assisted("type") `type`: TaskType,
21 | companion: TaskActorCompanion
22 | ) extends Actor with ActorLogging with ErrorHandler {
23 |
24 | def receive: Receive = {
25 | case TaskActor.Process => companion.process(`type`)
26 | case m: Any => logUnhandledMessage(m)
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/api/app/controllers/Healthchecks.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import db.{Authorization, InternalOrganizationsDao, InternalVersionsDao}
4 | import play.api.libs.json._
5 | import play.api.mvc._
6 |
7 | import javax.inject.{Inject, Named, Singleton}
8 |
9 | @Singleton
10 | class Healthchecks @Inject() (
11 | val apiBuilderControllerComponents: ApiBuilderControllerComponents,
12 | organizationsDao: InternalOrganizationsDao,
13 | ) extends ApiBuilderController {
14 |
15 | private val Result = Json.toJson(Map("status" -> "healthy"))
16 |
17 | def getHealthcheck(): Action[AnyContent] = Action { _ =>
18 | organizationsDao.findAll(Authorization.PublicOnly, limit = Some(1)).headOption
19 | Ok(Result)
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/lib/src/test/scala/MethodsSpec.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import org.scalatest.funspec.AnyFunSpec
4 | import org.scalatest.matchers.should.Matchers
5 |
6 | class MethodsSpec extends AnyFunSpec with Matchers {
7 |
8 | it("supportsBody") {
9 | Methods.supportsBody("GET") should be(false)
10 | Methods.supportsBody("get") should be(false)
11 | Methods.supportsBody("DELETE") should be(true)
12 | Methods.supportsBody("delete") should be(true)
13 | Methods.supportsBody("POST") should be(true)
14 | Methods.supportsBody("post") should be(true)
15 | Methods.supportsBody("PUT") should be(true)
16 | Methods.supportsBody("put") should be(true)
17 | Methods.supportsBody("PATCH") should be(true)
18 | Methods.supportsBody("patch") should be(true)
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/app/app/views/organizations/show.todo:
--------------------------------------------------------------------------------
1 | Your Role(s)
2 | @role.label
3 |
4 | @if(!requests.isEmpty) {
5 | Membership requests pending approval
6 |
7 |
8 | @requests.map { request =>
9 |
10 | @request.org.name
11 | @request.user.name.getOrElse("")
12 | @request.user.email
13 | @request.role
14 |
15 | Approve |
16 | Decline
17 |
18 |
19 | }
20 |
21 |
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/app/app/lib/Markdown.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import com.vladsch.flexmark.html.HtmlRenderer
4 | import com.vladsch.flexmark.parser.Parser
5 | import com.vladsch.flexmark.util.data.MutableDataSet
6 |
7 | /**
8 | * Wrapper on play config testing for empty strings and standardizing
9 | * error message for required configuration.
10 | */
11 | object Markdown {
12 |
13 | def apply(
14 | value: Option[String],
15 | default: String = ""
16 | ): String = {
17 | value.map { toHtml }.getOrElse(default)
18 | }
19 |
20 | private val options = new MutableDataSet()
21 | private val parser = Parser.builder(options).build
22 | private val renderer = HtmlRenderer.builder(options).build
23 |
24 | def toHtml(value: String): String = {
25 | renderer.render(parser.parse(value))
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/avro/src/test/scala/io/apibuilder/avro/AvroIdlServiceValidatorSpec.scala:
--------------------------------------------------------------------------------
1 | package io.apibuilder.avro
2 |
3 | import helpers.ValidatedTestHelpers
4 | import lib.{FileUtils, ServiceConfiguration}
5 | import org.scalatest.funspec.AnyFunSpec
6 | import org.scalatest.matchers.should.Matchers
7 |
8 | import java.io.File
9 |
10 | class AvroIdlServiceValidatorSpec extends AnyFunSpec with Matchers with ValidatedTestHelpers {
11 |
12 | private val validator: AvroIdlServiceValidator = AvroIdlServiceValidator(
13 | ServiceConfiguration(
14 | orgKey = "gilt",
15 | orgNamespace = "io.apibuilder",
16 | version = "0.0.1-dev"
17 | )
18 | )
19 |
20 | it("parses") {
21 | expectValid {
22 | validator.validate(FileUtils.readToString(new File("avro/example.avdl")))
23 | }
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/lib/src/test/scala/ReviewSpec.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import org.scalatest.funspec.AnyFunSpec
4 | import org.scalatest.matchers.should.Matchers
5 |
6 | class ReviewSpec extends AnyFunSpec with Matchers {
7 |
8 | it("fromString") {
9 | Review.fromString(Review.Accept.key) should be(Some(Review.Accept))
10 | Review.fromString(Review.Accept.key.toUpperCase) should be(Some(Review.Accept))
11 | Review.fromString(Review.Accept.key.toLowerCase) should be(Some(Review.Accept))
12 |
13 | Review.fromString(Review.Decline.key) should be(Some(Review.Decline))
14 | Review.fromString(Review.Decline.key.toUpperCase) should be(Some(Review.Decline))
15 | Review.fromString(Review.Decline.key.toLowerCase) should be(Some(Review.Decline))
16 |
17 | Review.fromString("other") should be(None)
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/api/app/lib/ServiceUri.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | case class ServiceUri(
4 | host: String,
5 | org: String,
6 | app: String,
7 | version: String
8 | )
9 |
10 | /**
11 | * Parses the URI for a service.json file into its component parts
12 | */
13 | object ServiceUri {
14 |
15 | private val Pattern = """^https?:\/\/([^\/]+)/([^\/]+)/([^\/]+)/([^\/]+)\/service.json$""".r
16 |
17 | def parse(uri: String): Option[ServiceUri] = {
18 | uri.toLowerCase.trim match {
19 | case Pattern(host, org, app, version) => {
20 | Some(
21 | ServiceUri(
22 | host = host,
23 | org = org,
24 | app = app,
25 | version = version
26 | )
27 | )
28 | }
29 |
30 | case _ => {
31 | None
32 | }
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/api/app/db/AuditsDao.scala:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | private[db] object AuditsDao {
4 |
5 | def query(tableName: String): String = {
6 | Seq(
7 | queryCreation(tableName),
8 | s"${tableName}.updated_at",
9 | s"${tableName}.updated_by_guid"
10 | ).mkString(",\n ")
11 | }
12 |
13 | private def queryCreation(tableName: String): String = {
14 | Seq(
15 | s"${tableName}.created_at",
16 | s"${tableName}.created_by_guid"
17 | ).mkString(",\n ")
18 | }
19 |
20 | def queryCreationDefaultingUpdatedAt(tableName: String): String = {
21 | Seq(
22 | s"${tableName}.created_at",
23 | s"${tableName}.created_by_guid",
24 | s"${tableName}.created_at as updated_at",
25 | s"${tableName}.created_by_guid as updated_by_guid"
26 | ).mkString(",\n ")
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/api/test/helpers/BatchDownloadApplicationHelpers.scala:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import io.apibuilder.api.v0.models.{BatchDownloadApplicationForm, BatchDownloadApplicationsForm}
4 |
5 | trait BatchDownloadApplicationHelpers extends RandomHelpers {
6 |
7 | def makeBatchDownloadApplicationsForm(
8 | applications: Seq[io.apibuilder.api.v0.models.BatchDownloadApplicationForm],
9 | ): BatchDownloadApplicationsForm = {
10 | BatchDownloadApplicationsForm(
11 | applications = applications,
12 | )
13 | }
14 |
15 | def makeBatchDownloadApplicationForm(
16 | applicationKey: String = randomString(),
17 | version: String = "latest",
18 | ): BatchDownloadApplicationForm = {
19 | BatchDownloadApplicationForm(
20 | applicationKey = applicationKey,
21 | version = version,
22 | )
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/app/app/views/attributes/attributeForm.scala.html:
--------------------------------------------------------------------------------
1 | @(form: Form[controllers.AttributesController.AttributeFormData],
2 | cancelUrl: play.api.mvc.Call
3 | )(implicit flash: Flash, messages: Messages)
4 |
5 |
6 | @helper.inputText(
7 | form("name"),
8 | Symbol("_label") -> "Attribute Name",
9 | Symbol("_help") -> "URL friendly, globally unique attribute name.",
10 | Symbol("_error") -> form.error("name")
11 | )
12 |
13 | @helper.textarea(
14 | form("description"),
15 | Symbol("cols") -> 50,
16 | Symbol("rows") -> 5,
17 | Symbol("_label") -> "Description",
18 | Symbol("_error") -> form.error("description")
19 | )
20 |
21 |
22 | Submit
23 | Cancel
24 |
--------------------------------------------------------------------------------
/api/app/lib/RequestAuthenticationUtil.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import javax.inject.Inject
4 |
5 | import db.{InternalUsersDao, InternalUser}
6 | import io.apibuilder.api.v0.models.User
7 | import play.api.mvc.Headers
8 | import util.BasicAuthorization
9 |
10 | /**
11 | * Helpers to fetch a user from an incoming request header
12 | */
13 | class RequestAuthenticationUtil @Inject() (
14 | usersDao: InternalUsersDao
15 | ) {
16 | private val AuthorizationHeader: String = "Authorization"
17 |
18 | def user(headers: Headers): Option[InternalUser] = {
19 | BasicAuthorization.get(headers.get(AuthorizationHeader)).flatMap {
20 | case BasicAuthorization.Token(t) => usersDao.findByToken(t)
21 | case BasicAuthorization.Session(id) => usersDao.findBySessionId(id)
22 | case _: BasicAuthorization.User => None
23 | }
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/core/test/core/ServiceMethodsSpec.scala:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import helpers.ApiJsonHelpers
4 | import org.scalatest.funspec.AnyFunSpec
5 | import org.scalatest.matchers.should.Matchers
6 |
7 | class ServiceMethodsSpec extends AnyFunSpec with Matchers with ApiJsonHelpers {
8 |
9 | it("missing method") {
10 | val json = """
11 | {
12 | "name": "API Builder",
13 | "apidoc": { "version": "0.9.6" },
14 |
15 | "models": {
16 | "user": {
17 | "fields": [
18 | { "name": "id", "type": "long" }
19 | ]
20 | }
21 | },
22 |
23 | "resources": {
24 | "user": {
25 | "operations": [
26 | {}
27 | ]
28 | }
29 | }
30 |
31 | }
32 | """
33 |
34 | TestHelper.expectSingleError(json) should be("Resource[user] /users Missing method")
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/script/lib/tag.rb:
--------------------------------------------------------------------------------
1 | class Tag
2 | def Tag.ask
3 | assert_sem_installed
4 |
5 | next_standard_tag = `sem-info tag next`.strip
6 | next_tag = replace_hundreds(next_standard_tag)
7 | puts ""
8 | if Ask.for_boolean("Create new tag #{next_tag}?")
9 | run("git tag -a -m #{next_tag} #{next_tag}")
10 | run("git push --tags origin")
11 | end
12 |
13 | `sem-info tag latest`.strip
14 | end
15 |
16 | def Tag.replace_hundreds(tag)
17 | parts = tag.split('.', 3)
18 | if parts.length == 3 && parts.all? { |p| p.to_i.to_s == p }
19 | if parts[2].to_i >= 100
20 | Tag.replace_hundreds("%s.%s.%s" % [parts[0], parts[1].to_i + 1, 0])
21 | elsif parts[1].to_i >= 100
22 | Tag.replace_hundreds("%s.%s.%s" % [parts[0] + 1, 0, 0])
23 | else
24 | tag
25 | end
26 | else
27 | tag
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/core/app/Importer.scala:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import cats.data.Validated.{Invalid, Valid}
4 | import cats.implicits._
5 | import cats.data.ValidatedNec
6 | import io.apibuilder.spec.v0.models.Service
7 |
8 | import scala.util.{Failure, Success, Try}
9 |
10 | case class Importer(fetcher: ServiceFetcher, uri: String) {
11 |
12 | lazy val service: Service = fetched match {
13 | case Invalid(errors) => sys.error(s"Error fetching uri[$uri]: ${errors.toNonEmptyList.toList.mkString(", ")}")
14 | case Valid(service) => service
15 | }
16 |
17 | lazy val validate: ValidatedNec[String, Unit] = fetched.map(_ => ())
18 |
19 | lazy val fetched: ValidatedNec[String, Service] = {
20 | Try(
21 | fetcher.fetch(uri)
22 | ) match {
23 | case Success(service) => service.validNec
24 | case Failure(ex) => s"Error fetching uri[$uri]: ${ex}".invalidNec
25 | }
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/lib/src/test/scala/VersionedNameSpec.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import org.scalatest.funspec.AnyFunSpec
4 | import org.scalatest.matchers.should.Matchers
5 |
6 | class VersionedNameSpec extends AnyFunSpec with Matchers {
7 |
8 | it("label") {
9 | VersionedName("user").label should be("user")
10 | VersionedName("user", Some("1.0.0")).label should be("user:1.0.0")
11 | VersionedName("user", Some("latest")).label should be("user:latest")
12 | }
13 |
14 | it("orders") {
15 | val a = VersionedName("user", Some("latest"))
16 | val b = VersionedName("user", Some("1.0.0"))
17 | val c = VersionedName("user", Some("1.0.1"))
18 | val d = VersionedName("user")
19 | val e = VersionedName("user")
20 |
21 | Seq(a, b, c, d, e).sorted should be(Seq(a, b, c, d, e))
22 | Seq(e, d, c, b, a).sorted should be(Seq(a, b, c, d, e))
23 |
24 | d.compare(e) should be(0)
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/app/app/lib/AppOrderHelper.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import io.apibuilder.api.v0.models.{SortOrder, AppSortBy}
4 |
5 | object AppOrderHelper {
6 | def newOrder(currSort: Option[AppSortBy], currOrd: Option[SortOrder], heading: AppSortBy) = {
7 | if (currSort == Some(heading)) {
8 | currOrd match {
9 | case Some(SortOrder.Desc) => Some(SortOrder.Asc)
10 | case _ => Some(SortOrder.Desc)
11 | }
12 | } else {
13 | Some(SortOrder.Asc)
14 | }
15 | }
16 |
17 | def orderImage(currSort: Option[AppSortBy], currOrd: Option[SortOrder], heading: AppSortBy) = {
18 | if (currSort == Some(heading)) {
19 | currOrd match {
20 | case Some(SortOrder.Desc) => views.html.icon("icons/sort-descending-2x.png", "descending")
21 | case _ => views.html.icon("icons/sort-ascending-2x.png", "ascending")
22 | }
23 | } else {
24 | ""
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/core/app/builder/api_json/templates/HeaderMerge.scala:
--------------------------------------------------------------------------------
1 | package builder.api_json.templates
2 |
3 | import io.apibuilder.api.json.v0.models.Header
4 |
5 | trait HeaderMerge extends AttributeMerge {
6 | private val merger = new ArrayMerge[Header] {
7 | override def uniqueIdentifier(i: Header): String = i.name
8 |
9 | override def merge(original: Header, tpl: Header): Header = {
10 | Header(
11 | name = original.name,
12 | `type` = original.`type`,
13 | required = original.required,
14 | description = original.description.orElse(tpl.description),
15 | attributes = mergeAttributes(original.attributes, tpl.attributes),
16 | deprecation = original.deprecation.orElse(tpl.deprecation),
17 | )
18 | }
19 | }
20 |
21 | def mergeHeaders(original: Option[Seq[Header]], tpl: Option[Seq[Header]]): Option[Seq[Header]] = {
22 | merger.merge(original, tpl)
23 | }
24 |
25 | }
--------------------------------------------------------------------------------
/DEPLOY.md:
--------------------------------------------------------------------------------
1 | Deploying apibuilder
2 | ====================
3 |
4 | - Runs in EC2 on docker images. Database is RDS Postgresql
5 | - yum install docker
6 | - service docker start
7 | - docker pull flowcommerce/apibuilder:0.12.1
8 | - docker run ...
9 |
10 | Installing Docker on mac
11 | ========================
12 |
13 | http://docs.docker.io/installation/mac/
14 |
15 | Building the Docker Image
16 | =========================
17 |
18 | Step 1: Using a small set of scripts that will tag the repo, update
19 | any markup to latest tag, and then create the docker image:
20 |
21 | /web/ionroller-tools/bin/release-and-deploy
22 |
23 | You can also build the docker image directly:
24 |
25 | docker build -t flowcommerce/apibuilder:0.12.1 .
26 |
27 | Database backup
28 | ===============
29 |
30 | pg_dump -Fc -h host -U api -f apibuilderdb.dmp apibuilderdb
31 | pg_restore -U api -c -d apibuilderdb apibuilderdb.dmp
32 |
--------------------------------------------------------------------------------
/app/app/views/login/index.scala.html:
--------------------------------------------------------------------------------
1 | @(tpl: models.MainTemplate,
2 | github: lib.Github,
3 | tab: controllers.LoginController.Tab,
4 | returnUrl: Option[String] = None
5 | )(implicit flash: Flash, messages: Messages)
6 |
7 | @main(tpl.copy(headTitle = Some("Sign in"))) {
8 |
9 |
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/app/app/views/subscriptions/index.scala.html:
--------------------------------------------------------------------------------
1 | @(tpl: models.MainTemplate,
2 | userPublications: Seq[controllers.Subscriptions.UserPublication]
3 | )(implicit flash: Flash, messages: Messages)
4 |
5 | @main(tpl.copy(title = Some("Subscriptions"))) {
6 |
7 | @if(userPublications.isEmpty) {
8 | No publications found
9 | } else {
10 |
11 |
12 | @userPublications.map { up =>
13 |
14 |
15 | @if(up.isSubscribed) {
16 | Subscribed
17 | } else {
18 | Not subscribed
19 | }
20 |
21 | (Toggle )
22 |
23 | @up.label
24 |
25 | }
26 |
27 |
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/app/app/controllers/Healthchecks.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import models.MainTemplate
4 | import play.api.mvc.{Action, AnyContent}
5 |
6 | import javax.inject.Inject
7 | import scala.concurrent.ExecutionContext
8 |
9 | class Healthchecks @Inject() (
10 | val apiBuilderControllerComponents: ApiBuilderControllerComponents
11 | ) extends ApiBuilderController {
12 |
13 | private implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global
14 |
15 | def index(): Action[AnyContent] = Anonymous.async { implicit request =>
16 | for {
17 | orgs <- request.api.organizations.get(key = Some("apicollective"), limit = 1)
18 | } yield {
19 | val tpl = MainTemplate(
20 | requestPath = request.path,
21 | title = Some("Healthcheck"),
22 | org = orgs.headOption
23 | )
24 | Ok(views.html.healthchecks.index(tpl))
25 | }
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/app/app/views/domains/index.scala.html:
--------------------------------------------------------------------------------
1 | @(tpl: models.MainTemplate)(implicit flash: Flash, messages: Messages)
2 |
3 | @main(tpl) {
4 |
5 |
6 | Organizations can provide one or more domains to automate user
7 | signup. When a user registers, if their domain matches a company
8 | domain, that user will be automatically added as a member of the
9 | organization.
10 |
11 |
12 | @tpl.org.map { org =>
13 |
16 |
17 | @if(org.domains.isEmpty) {
18 | No domains
19 |
20 | } else {
21 |
22 | @org.domains.map { domain =>
23 |
26 | }
27 | }
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/app/app/views/versions/headers.scala.html:
--------------------------------------------------------------------------------
1 | @(org: io.apibuilder.api.v0.models.Organization,
2 | app: io.apibuilder.api.v0.models.Application,
3 | version: String,
4 | service: io.apibuilder.spec.v0.models.Service,
5 | headers: Seq[io.apibuilder.spec.v0.models.Header]
6 | )
7 |
8 |
9 |
10 |
11 | Name
12 | Type
13 | Required?
14 | Default
15 | Description
16 |
17 |
18 |
19 | @headers.map { header =>
20 |
21 | @header.name@header.deprecation.map(deprecation(_))
22 | @datatype(org, app, version, service, header.`type`)
23 | @if(header.required) { Yes } else { No }
24 | @header.default.getOrElse("-")
25 | @Html(lib.Markdown(header.description))
26 |
27 | }
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/app/views/generators/index.scala.html:
--------------------------------------------------------------------------------
1 | @(
2 | tpl: models.MainTemplate,
3 | generatorWithServices: lib.PaginatedCollection[io.apibuilder.api.v0.models.GeneratorWithService]
4 | )(implicit flash: Flash, messages: Messages)
5 |
6 | @main(tpl) {
7 |
8 | @tpl.user.map { user =>
9 |
12 | }
13 |
14 | @generators.generators(generatorWithServices)
15 |
16 | @if(generatorWithServices.hasPrevious || generatorWithServices.hasNext) {
17 |
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/api/app/models/GeneratorServicesModel.scala:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import db.Authorization
4 | import db.generators.InternalGeneratorService
5 | import io.apibuilder.api.v0.models.{GeneratorService, GeneratorWithService}
6 | import io.apibuilder.common.v0.models.{Audit, ReferenceGuid}
7 |
8 | import javax.inject.Inject
9 |
10 | class GeneratorServicesModel @Inject()() {
11 | def toModel(generator: InternalGeneratorService): GeneratorService = {
12 | toModels(Seq(generator)).head
13 | }
14 |
15 | def toModels(services: Seq[InternalGeneratorService]): Seq[GeneratorService] = {
16 | services.map { s =>
17 | GeneratorService(
18 | guid = s.guid,
19 | uri = s.uri,
20 | audit = Audit(
21 | createdAt = s.db.createdAt,
22 | createdBy = ReferenceGuid(s.db.createdByGuid),
23 | updatedAt = s.db.updatedAt,
24 | updatedBy = ReferenceGuid(s.db.createdByGuid),
25 | )
26 | )
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/app/views/application_settings/move_form.scala.html:
--------------------------------------------------------------------------------
1 | @(tpl: models.MainTemplate,
2 | form: Form[controllers.ApplicationSettings.MoveOrgData],
3 | errors: Seq[String] = Seq.empty
4 | )(implicit flash: Flash, messages: Messages)
5 |
6 | @main(tpl, errorMessages = errors) {
7 |
8 |
9 | @helper.form(action = routes.ApplicationSettings.postMove(tpl.org.get.key, tpl.application.get.key)) {
10 |
11 |
12 | @helper.inputText(
13 | form("org_key"),
14 | Symbol("_label") -> "New Organization Key",
15 | Symbol("_error") -> form.error("org_key")
16 | )
17 |
18 |
19 |
23 |
24 | }
25 |
26 |
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/api/test/controllers/VersionsSpec.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import io.apibuilder.api.v0.models.OriginalType
4 | import org.scalatestplus.play.guice.GuiceOneServerPerSuite
5 | import org.scalatestplus.play.PlaySpec
6 |
7 | class VersionsSpec extends PlaySpec with MockClient with GuiceOneServerPerSuite {
8 |
9 | private lazy val org = createOrganization()
10 | private lazy val application = createApplication(org)
11 |
12 | "POST /:orgKey/:version stores the original in the proper format" in {
13 | val form = createVersionForm(name = application.name)
14 | val version = createVersionThroughApi(application, Some(form))
15 |
16 | // Now test that we stored the appropriate original
17 | version.original match {
18 | case None => sys.error("No original found")
19 | case Some(original) => {
20 | original.`type` must equal(OriginalType.ApiJson)
21 | original.data must equal(form.originalForm.data)
22 | }
23 | }
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/app/app/lib/Zipfile.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import java.io.FileOutputStream
4 | import java.util.zip.{ZipEntry, ZipOutputStream}
5 | import io.apibuilder.generator.v0.models.File
6 |
7 | object Zipfile {
8 |
9 | private val UTF8 = "UTF-8"
10 |
11 | def create(dirName: String, files: Seq[File]): java.io.File = {
12 | val path = java.io.File.createTempFile(dirName, ".zip")
13 | createForFile(path, files, prefix = dirName + "/")
14 | path
15 | }
16 |
17 | private def createForFile(
18 | zip: java.io.File,
19 | files: Seq[File],
20 | prefix: String
21 | ): Unit = {
22 | val zipOutputStream = new ZipOutputStream(new FileOutputStream(zip))
23 | files.foreach { f =>
24 | val path = prefix + f.dir.fold("")(_ + "/")
25 | zipOutputStream.putNextEntry(new ZipEntry(s"$path${f.name}"))
26 | zipOutputStream.write(f.contents.getBytes(UTF8))
27 | zipOutputStream.closeEntry
28 | }
29 | zipOutputStream.close()
30 | }
31 |
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/app/app/views/application_settings/show.scala.html:
--------------------------------------------------------------------------------
1 | @(
2 | tpl: models.MainTemplate,
3 | application: io.apibuilder.api.v0.models.Application
4 | )(implicit flash: Flash, messages: Messages)
5 |
6 | @main(tpl) {
7 |
8 | @tpl.org.map { org =>
9 | @if(tpl.canEditApplication(application.key)) {
10 |
13 | }
14 |
15 |
16 | Visibility: @application.visibility
17 |
18 |
19 | @if(tpl.canAdminApplication(application.key)) {
20 | Move to new organization |
21 | Delete this application
22 | }
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/app/app/views/generators/show.scala.html:
--------------------------------------------------------------------------------
1 | @(
2 | tpl: models.MainTemplate,
3 | gws: io.apibuilder.api.v0.models.GeneratorWithService
4 | )(implicit flash: Flash, messages: Messages)
5 |
6 | @import io.apibuilder.api.v0.models.Visibility
7 |
8 | @main(tpl) {
9 |
10 |
11 | Service: @gws.service.uri
12 | Key: @gws.generator.key
13 | Name: @gws.generator.name
14 | Language: @gws.generator.language.getOrElse("not specified")
15 | Attributes:
16 | @if(gws.generator.attributes.isEmpty) {
17 | None
18 | } else {
19 | @gws.generator.attributes.map { attr =>
20 | @attr
21 | }
22 | }
23 |
24 | Description:
25 | @Html(lib.Markdown(gws.generator.description))
26 |
27 |
28 |
29 | }
30 |
31 |
32 |
--------------------------------------------------------------------------------
/swagger/src/test/scala/io/apibuilder/swagger/translators/BaseUrlSpec.scala:
--------------------------------------------------------------------------------
1 | package io.apibuilder.swagger.translators
2 |
3 | import lib.ServiceConfiguration
4 | import org.scalatest.funspec.AnyFunSpec
5 | import org.scalatest.matchers.should.Matchers
6 |
7 | class BaseUrlSpec extends AnyFunSpec with Matchers {
8 |
9 | it("apply") {
10 | BaseUrl(Nil, "localhost", None) should be(Nil)
11 | BaseUrl(Seq("http"), "localhost", None) should be(Seq("http://localhost"))
12 | BaseUrl(Seq("HTTP"), "localhost", None) should be(Seq("http://localhost"))
13 | BaseUrl(Seq("https"), "localhost", None) should be(Seq("https://localhost"))
14 | BaseUrl(Seq("https"), "localhost", Some("/api")) should be(Seq("https://localhost/api"))
15 | BaseUrl(Seq("http", "https"), "localhost", Some("/api")) should be(Seq("http://localhost/api", "https://localhost/api"))
16 | BaseUrl(Seq("https", "http"), "localhost", Some("/api")) should be(Seq("https://localhost/api", "http://localhost/api"))
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/lib/src/main/scala/Bytes.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | object Bytes {
4 |
5 | private val OneKb = 1024
6 | private val OneMb = OneKb * OneKb
7 | private val OneGb = OneMb * OneKb
8 |
9 | def label(bytes: Long): String = {
10 | if (bytes < OneKb) {
11 | bytes match {
12 | case 1 => "1 byte"
13 | case n => s"$n bytes"
14 | }
15 | } else if (bytes < OneMb) {
16 | formatLabel(bytes, OneKb, "kb", "mb")
17 | } else if (bytes < OneGb) {
18 | formatLabel(bytes, OneMb, "mb", "gb")
19 | } else {
20 | formatLabel(bytes, OneGb, "gb", "tb")
21 | }
22 | }
23 |
24 | private def formatLabel(bytes: Long, divisor: Long, label: String, nextLabel: String): String = {
25 | val value = bytes / divisor
26 | val remainder = ((bytes % divisor) / (divisor * 1.0))
27 | (remainder * 10).round match {
28 | case 0 => s"$value $label"
29 | case 10 => s"1 $nextLabel"
30 | case remainder => s"$value.$remainder $label"
31 | }
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/api/app/models/AttributesModel.scala:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import db.{Authorization, InternalAttribute, InternalAttributesDao}
4 | import io.apibuilder.api.v0.models.Attribute
5 | import io.apibuilder.common.v0.models.{Audit, ReferenceGuid}
6 |
7 | import java.util.UUID
8 | import javax.inject.Inject
9 |
10 | class AttributesModel @Inject()(
11 | attributesDao: InternalAttributesDao,
12 | ) {
13 | def toModel(v: InternalAttribute): Attribute = {
14 | toModels(Seq(v)).head
15 | }
16 |
17 | def toModels(attributes: Seq[InternalAttribute]): Seq[Attribute] = {
18 | attributes.map { attr =>
19 | Attribute(
20 | guid = attr.guid,
21 | name = attr.name,
22 | description = attr.description,
23 | audit = Audit(
24 | createdAt = attr.db.createdAt,
25 | createdBy = ReferenceGuid(attr.db.createdByGuid),
26 | updatedAt = attr.db.updatedAt,
27 | updatedBy = ReferenceGuid(attr.db.updatedByGuid),
28 | )
29 | )
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/lib/src/main/scala/ServiceConfiguration.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | case class ServiceConfiguration(
4 | orgKey: String,
5 | orgNamespace: String,
6 | version: String
7 | ) {
8 | assert(orgKey.trim == orgKey, s"orgKey[$orgKey] must be trimmed")
9 | assert(orgNamespace.trim == orgNamespace, s"orgNamespace[$orgNamespace] must be trimmed")
10 | assert(version.trim == version, s"version[$version] must be trimmed")
11 |
12 | private val ApplicationNamespaceNonLetterRegexp = """\.([^a-zA-Z])""".r
13 |
14 | /**
15 | * Example: apidocSpec => apidoc.spec.v0
16 | */
17 | def applicationNamespace(key: String): String = {
18 | ApplicationNamespaceNonLetterRegexp.replaceAllIn(
19 | (
20 | Seq(orgNamespace.trim) ++
21 | Text.splitIntoWords(Text.camelCaseToUnderscore(key.trim)).map(_.toLowerCase).map(_.trim) ++
22 | Seq(VersionTag(version).major.map(num => s"v$num").getOrElse(""))
23 | ).filter(!_.isEmpty).mkString("."),
24 | m => m.group(1)
25 | )
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/app/app/views/domains/form.scala.html:
--------------------------------------------------------------------------------
1 | @(tpl: models.MainTemplate,
2 | form: Form[controllers.Domains.DomainData],
3 | errors: Seq[String] = Seq.empty
4 | )(implicit flash: Flash, messages: Messages)
5 |
6 | @main(tpl) {
7 |
8 |
9 | @helper.form(action = routes.Domains.postCreate(tpl.org.get.key)) {
10 |
11 |
12 | @if(!errors.isEmpty) {
13 |
14 | @errors.map { msg => @msg }
15 |
16 | }
17 |
18 | @helper.inputText(
19 | form("name"),
20 | Symbol("_label") -> "Domain name",
21 | Symbol("_error") -> form.error("name")
22 | )
23 |
24 |
25 |
26 |
30 |
31 | }
32 |
33 |
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/dao/run.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | profile = nil
4 | remainder = []
5 | i = 0
6 | while i < ARGV.length
7 | v = ARGV[i]
8 | if v.match(/^\-\-/)
9 | if v == "--profile"
10 | i += 1
11 | profile = ARGV[i]
12 | else
13 | raise "Invalid flag: #{v}"
14 | end
15 | else
16 | remainder << v
17 | end
18 | i += 1
19 | end
20 |
21 | files = if remainder.empty?
22 | Dir.glob("spec/*json")
23 | else
24 | remainder
25 | end
26 |
27 | def run(cmd)
28 | puts cmd
29 | if !system(cmd)
30 | exit 1
31 | end
32 | end
33 |
34 | def setup_dir(name)
35 | if File.directory?(name)
36 | run "rm -rf #{name}/*"
37 | else
38 | run "mkdir #{name}"
39 | end
40 | end
41 |
42 | setup_dir("scala")
43 | setup_dir("psql")
44 |
45 | files.each_with_index do |f, i|
46 | puts "\n"
47 | if i > 0
48 | puts "-" * 80
49 | puts "\n"
50 | end
51 | p = profile ? " --profile #{profile}" : ""
52 | run "~/code/flowcommerce/dao_generator/run.rb#{p} --organization apicollective #{f}"
53 | end
54 |
--------------------------------------------------------------------------------
/app/test/lib/UtilSpec.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import org.scalatestplus.play.PlaySpec
4 | import org.scalatestplus.play.guice.GuiceOneAppPerSuite
5 |
6 | class UtilSpec extends PlaySpec with GuiceOneAppPerSuite {
7 |
8 | private val util = app.injector.instanceOf[Util]
9 |
10 | "validateReturnUrl for invalid domains" in {
11 | util.validateReturnUrl("") must be(Left(Seq("Redirect URL[] must start with / or a known domain")))
12 | util.validateReturnUrl("https://google.com/foo") must be(Left(Seq("Redirect URL[https://google.com/foo] must start with / or a known domain")))
13 | }
14 |
15 | "validateReturnUrl for valid domains" in {
16 | util.validateReturnUrl("/") must be(Right("/"))
17 | util.validateReturnUrl("https://www.apibuilder.io") must be(Right("/"))
18 | util.validateReturnUrl("https://www.apibuilder.io/") must be(Right("/"))
19 | util.validateReturnUrl("https://app.apibuilder.io") must be(Right("/"))
20 | util.validateReturnUrl("https://app.apibuilder.io/") must be(Right("/"))
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/api/app/util/SessionHelper.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import javax.inject.Inject
4 | import db.{InternalUser, InternalUsersDao}
5 | import io.apibuilder.api.v0.models.{Authentication, User}
6 | import org.joda.time.DateTime
7 | import lib.Constants
8 |
9 | class SessionHelper @Inject() (
10 | sessionsDao: db.generated.SessionsDao,
11 | conversions: Conversions
12 | ) {
13 |
14 | private val DefaultSessionExpirationHours = 24 * 30
15 |
16 | def createAuthentication(u: InternalUser): Authentication = {
17 | val id = SessionIdGenerator.generate()
18 | val ts = DateTime.now
19 |
20 | sessionsDao.insert(
21 | Constants.AdminUserGuid,
22 | _root_.db.generated.SessionForm(
23 | id = id,
24 | userGuid = u.guid,
25 | expiresAt = ts.plusHours(DefaultSessionExpirationHours)
26 | )
27 | )
28 |
29 | val dbSession = sessionsDao.findById(id).getOrElse {
30 | sys.error("Failed to create session")
31 | }
32 |
33 | conversions.toAuthentication(dbSession, u)
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/api/test/util/SessionHelperSpec.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import org.joda.time.DateTime
4 | import org.scalatestplus.play.PlaySpec
5 | import org.scalatestplus.play.guice.GuiceOneAppPerSuite
6 |
7 | class SessionHelperSpec extends PlaySpec with GuiceOneAppPerSuite with db.Helpers {
8 |
9 | "createAuthentication" in {
10 | val user = upsertUser("michael@mailinator.com")
11 | val auth = sessionHelper.createAuthentication(user)
12 | auth.user.guid must equal(user.guid)
13 | auth.session.expiresAt.isBefore(DateTime.now.plusWeeks(6)) must be(true)
14 | auth.session.expiresAt.isAfter(DateTime.now.plusWeeks(3)) must be(true)
15 | }
16 |
17 | "can delete session" in {
18 | val user = upsertUser("michael@mailinator.com")
19 | val auth = sessionHelper.createAuthentication(user)
20 |
21 | sessionsDao.findById(auth.session.id).get.deletedAt.isEmpty must be(true)
22 |
23 | sessionsDao.deleteById(user.guid, auth.session.id)
24 | sessionsDao.findById(auth.session.id).get.deletedAt.isEmpty must be(false)
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/app/app/views/organizations/details.scala.html:
--------------------------------------------------------------------------------
1 | @(
2 | tpl: models.MainTemplate,
3 | org: io.apibuilder.api.v0.models.Organization,
4 | haveMembershipRequests: Boolean
5 | )(implicit flash: Flash, messages: Messages)
6 |
7 | @main(tpl.copy(title = Some("Org Details"))) {
8 |
9 |
10 |
11 |
Edit
12 |
13 | @if(tpl.canDeleteOrganization) {
14 |
15 |
Delete
16 | }
17 |
18 |
19 | @if(haveMembershipRequests) {
20 | Review pending membership requests
21 | }
22 |
23 |
24 | Name: @org.name
25 | Key: @org.key
26 | Namespace: @org.namespace
27 | Visibility: @org.visibility.toString
28 |
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/api/app/controllers/BatchDownloadApplications.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import cats.data.Validated.{Invalid, Valid}
4 | import io.apibuilder.api.v0.models.BatchDownloadApplicationsForm
5 | import io.apibuilder.api.v0.models.json.*
6 |
7 | import javax.inject.{Inject, Singleton}
8 | import lib.Validation
9 | import play.api.libs.json.Json
10 | import play.api.mvc.Action
11 | import services.BatchDownloadApplicationsService
12 |
13 | @Singleton
14 | class BatchDownloadApplications @Inject() (
15 | override val apiBuilderControllerComponents: ApiBuilderControllerComponents,
16 | service: BatchDownloadApplicationsService,
17 | ) extends ApiBuilderController {
18 |
19 | def post(orgKey: String): Action[BatchDownloadApplicationsForm] = Anonymous(parse.json[BatchDownloadApplicationsForm]) { request =>
20 | service.process(request.authorization, orgKey, request.body) match {
21 | case Valid(result) => Created(Json.toJson(result))
22 | case Invalid(errors) => Conflict(Json.toJson(Validation.errors(errors)))
23 | }
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/api/app/controllers/Items.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import db.ItemsDao
4 | import io.apibuilder.api.v0.models.json._
5 | import play.api.mvc._
6 | import play.api.libs.json._
7 | import java.util.UUID
8 | import javax.inject.{Inject, Singleton}
9 |
10 | @Singleton
11 | class Items @Inject() (
12 | val apiBuilderControllerComponents: ApiBuilderControllerComponents,
13 | itemsDao: ItemsDao
14 | ) extends ApiBuilderController {
15 |
16 | def get(
17 | q: Option[String],
18 | limit: Long = 25,
19 | offset: Long = 0
20 | ): Action[AnyContent] = Anonymous { request =>
21 | val items = itemsDao.findAll(
22 | request.authorization,
23 | q = q,
24 | limit = limit,
25 | offset = offset
26 | )
27 | Ok(Json.toJson(items))
28 | }
29 |
30 | def getByGuid(
31 | guid: UUID
32 | ): Action[AnyContent] = Anonymous { request =>
33 | itemsDao.findByGuid(request.authorization, guid) match {
34 | case None => NotFound
35 | case Some(item) => Ok(Json.toJson(item))
36 | }
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/api/test/lib/TestHelper.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import builder.OriginalValidator
4 | import db.Authorization
5 | import helpers.ValidatedTestHelpers
6 | import io.apibuilder.api.v0.models.OriginalType
7 | import io.apibuilder.api.v0.models.{Original, OriginalType}
8 | import io.apibuilder.spec.v0.models.Service
9 | import play.api.Application
10 |
11 | import java.io.File
12 |
13 | trait TestHelper extends ValidatedTestHelpers {
14 |
15 | def app: Application
16 |
17 | def readFile(path: String): String = FileUtils.readToString(new File(path))
18 |
19 | def readService(path: String): Service = {
20 | val config = ServiceConfiguration(
21 | orgKey = "gilt",
22 | orgNamespace = "io.apibuilder",
23 | version = "0.9.10"
24 | )
25 |
26 | val validator = OriginalValidator(
27 | config,
28 | OriginalType.ApiJson,
29 | app.injector.instanceOf[DatabaseServiceFetcher].instance(Authorization.All)
30 | )
31 | expectValid {
32 | validator.validate(readFile(path))
33 | }
34 | }
35 |
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/swagger/src/main/scala/io/apibuilder/swagger/translators/Body.scala:
--------------------------------------------------------------------------------
1 | package io.apibuilder.swagger.translators
2 |
3 | import io.apibuilder.spec.v0.{ models => apidoc }
4 | import io.swagger.models.{ArrayModel, ModelImpl, RefModel}
5 | import io.swagger.models.{ parameters => swaggerParams }
6 | import io.apibuilder.swagger.Util
7 |
8 | object Body {
9 |
10 | def apply(
11 | resolver: Resolver,
12 | param: swaggerParams.BodyParameter
13 | ): apidoc.Body = {
14 | val bodyType = param.getSchema match {
15 | case a: ArrayModel => s"[${Field(resolver, param.getName, "", a.getItems).`type`}]"
16 | case m: ModelImpl => m.getType
17 | case m: RefModel => resolver.resolveWithError(m).name
18 | case _ => sys.error("Unsupported body type: " + param.getSchema.getClass)
19 | }
20 |
21 | apidoc.Body(
22 | `type` = bodyType,
23 | description = Option(param.getDescription),
24 | deprecation = None,
25 | attributes = Util.vendorExtensionsToAttributes(param.getVendorExtensions)
26 | )
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/app/app/views/organization_attributes/index.scala.html:
--------------------------------------------------------------------------------
1 | @(
2 | tpl: models.MainTemplate,
3 | values: Seq[io.apibuilder.api.v0.models.AttributeValue],
4 | otherAttributes: Seq[io.apibuilder.api.v0.models.Attribute]
5 | )(implicit flash: Flash, messages: Messages)
6 |
7 | @main(tpl) {
8 |
9 |
10 |
11 | @values.map { value =>
12 |
13 | edit
14 | @value.attribute.name
15 | @value.value
16 |
17 | }
18 | @otherAttributes.map { attr =>
19 |
20 | Edit
21 | @attr.name
22 | -
23 |
24 | }
25 |
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/app/app/views/tokens/create.scala.html:
--------------------------------------------------------------------------------
1 | @(tpl: models.MainTemplate,
2 | form: Form[controllers.TokensController.TokenData],
3 | errorMessages: Seq[String] = Nil
4 | )(implicit flash: Flash, messages: Messages)
5 |
6 | @main(tpl.copy(title = Some("Create Token")), errorMessages = errorMessages) {
7 |
8 |
9 |
10 | @helper.form(action = routes.TokensController.postCreate) {
11 |
12 |
13 |
14 |
15 | @helper.inputText(
16 | form("description"),
17 | Symbol("_label") -> "Description",
18 | Symbol("_help") -> "Optional description to help you remember where this token is used",
19 | Symbol("_error") -> form.error("description")
20 | )
21 |
22 |
23 |
Submit
24 |
Cancel
25 |
26 |
27 |
28 | }
29 |
30 |
31 |
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/lib/src/main/scala/Pager.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | object Pager {
4 |
5 | /**
6 | * Iterator that takes two functions:
7 | * pagerFunction: Method to return a page of results
8 | * perObjectFunction: Function to call on each element
9 | *
10 | * Example:
11 | * Pager.eachPage[Subscription] { offset =>
12 | * SubscriptionsDao.findAll(
13 | * Authorization.All,
14 | * organization = Some(organization),
15 | * publication = Some(publication),
16 | * limit = 100,
17 | * offset = offset
18 | * )
19 | * } { subscription =>
20 | * println(subscription)
21 | * }
22 | */
23 | def eachPage[T](
24 | pagerFunction: Int => Seq[T]
25 | )(
26 | perObjectFunction: T => Unit
27 | ): Unit = {
28 | var offset = 0
29 | var haveMore = true
30 |
31 | while (haveMore) {
32 | val objects = pagerFunction(offset)
33 | haveMore = objects.nonEmpty
34 | offset += objects.size
35 | objects.foreach { perObjectFunction(_) }
36 | }
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/api/test/controllers/BatchDownloadApplicationsSpec.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import org.scalatestplus.play.guice.GuiceOneServerPerSuite
4 | import org.scalatestplus.play.PlaySpec
5 |
6 | class BatchDownloadApplicationsSpec extends PlaySpec with MockClient with GuiceOneServerPerSuite
7 | with helpers.BatchDownloadApplicationHelpers
8 | {
9 |
10 | implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global
11 |
12 | private lazy val version = createVersion(createApplication(createOrganization()))
13 |
14 | "postApplications" must {
15 | def post(key: String) = {
16 | client.batchDownloadApplications.post(
17 | version.organization.key,
18 | makeBatchDownloadApplicationsForm(
19 | applications = Seq(
20 | makeBatchDownloadApplicationForm(key)
21 | )
22 | )
23 | )
24 | }
25 |
26 | "2xx" in {
27 | await { post(version.application.key) }
28 | }
29 |
30 | "409" in {
31 | expectErrors { post(randomString()) }
32 | }
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/app/app/lib/Config.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import javax.inject.{Inject, Singleton}
4 |
5 | import play.api.{Configuration, Logger}
6 |
7 | /**
8 | * Wrapper on play config testing for empty strings and standardizing
9 | * error message for required configuration.
10 | */
11 | @Singleton
12 | class Config @Inject() (
13 | configuration: Configuration
14 | ) {
15 |
16 | private val logger: Logger = Logger(this.getClass)
17 |
18 | def requiredString(name: String): String = {
19 | optionalString(name).getOrElse {
20 | val msg = s"configuration parameter[$name] is required"
21 | logger.error(msg)
22 | sys.error(msg)
23 | }
24 | }
25 |
26 | def optionalString(name: String): Option[String] = {
27 | configuration.getOptional[String](name).map { value =>
28 | value.trim match {
29 | case "" => {
30 | val msg = s"Value for configuration parameter[$name], if specified, cannot be blank"
31 | logger.error(msg)
32 | sys.error(msg)
33 | }
34 | case v => v
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/app/lib/Github.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import java.net.URLEncoder
4 | import javax.inject.Inject
5 |
6 | class Github @Inject() (
7 | config: Config,
8 | util: Util
9 | ) {
10 |
11 | lazy val clientId: String = config.requiredString("apibuilder.github.oauth.client.id")
12 | lazy val clientSecret: String = config.requiredString("apibuilder.github.oauth.client.secret")
13 | private lazy val baseUrl = util.fullUrl("/login/github/callback")
14 |
15 | private val Scopes = Seq("user:email")
16 |
17 | private val OauthUrl = "https://github.com/login/oauth/authorize"
18 |
19 | def oauthUrl(returnUrl: Option[String]): String = {
20 | val finalUrl = URLEncoder.encode(
21 | util.fullUrl(returnUrl.getOrElse("/")),
22 | "UTF-8"
23 | )
24 |
25 | OauthUrl + "?" + Seq(
26 | "scope" -> Scopes.mkString(","),
27 | "client_id" -> clientId,
28 | "redirect_uri" -> s"$baseUrl?return_url=$finalUrl"
29 | ).map { case (key, value) =>
30 | s"$key=" + URLEncoder.encode(value, "UTF-8")
31 | }.mkString("&")
32 | }
33 |
34 | }
35 |
36 |
--------------------------------------------------------------------------------
/app/app/views/organization_attributes/edit.scala.html:
--------------------------------------------------------------------------------
1 | @(
2 | tpl: models.MainTemplate,
3 | attribute: io.apibuilder.api.v0.models.Attribute,
4 | form: Form[controllers.OrganizationAttributesController.FormData],
5 | errorMessages: Seq[String] = Nil
6 | )(implicit flash: Flash, messages: Messages)
7 |
8 | @main(tpl.copy(title = Some(s"Edit ${attribute.name}")), errorMessages) {
9 |
10 |
11 |
12 | @helper.form(action = routes.OrganizationAttributesController.editPost(tpl.org.get.key, attribute.name)) {
13 |
14 |
15 |
16 | @helper.inputText(
17 | form("value"),
18 | Symbol("_label") -> "Value",
19 | Symbol("_error") -> form.error("value")
20 | )
21 |
22 |
23 |
Submit
24 |
Cancel
25 |
26 | }
27 |
28 |
29 | @Html(lib.Markdown.toHtml(attribute.description.getOrElse("")))
30 |
31 |
32 |
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/core/test/resources/simple-without-array.json:
--------------------------------------------------------------------------------
1 | {
2 | "swagger": "2.0",
3 | "info": {
4 | "version": "0.0.0",
5 | "title": "Simple API"
6 | },
7 | "paths": {
8 | "/": {
9 | "get": {
10 | "responses": {
11 | "200": {
12 | "description": "OK",
13 | "schema": {
14 | "$ref": "#/definitions/test"
15 | }
16 | }
17 | }
18 | }
19 | }
20 | },
21 | "definitions": {
22 | "test": {
23 | "required": [
24 | "id",
25 | "name"
26 | ],
27 | "properties": {
28 | "id": {
29 | "type": "integer"
30 | },
31 | "name": {
32 | "type": "string"
33 | },
34 | "tag": {
35 | "type": "string"
36 | }
37 | }
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/app/app/controllers/EmailVerifications.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import javax.inject.Inject
4 | import io.apibuilder.api.v0.models.EmailVerificationConfirmationForm
5 | import play.api.mvc.{Action, AnyContent}
6 |
7 | import scala.concurrent.ExecutionContext
8 |
9 | class EmailVerifications @Inject() (
10 | val apiBuilderControllerComponents: ApiBuilderControllerComponents
11 | ) extends ApiBuilderController {
12 |
13 | private implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global
14 |
15 | def get(token: String): Action[AnyContent] = Anonymous.async { implicit request =>
16 | request.api.emailVerificationConfirmationForms.post(
17 | EmailVerificationConfirmationForm(token = token)
18 | ).map { _ =>
19 | Redirect(routes.ApplicationController.index()).flashing("success" -> "Email confirmed")
20 | }.recover {
21 | case r: io.apibuilder.api.v0.errors.ErrorsResponse => {
22 | Redirect(routes.ApplicationController.index()).flashing("warning" -> r.errors.map(_.message).mkString(", "))
23 | }
24 | }
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/api/app/lib/Misc.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import cats.data.ValidatedNec
4 | import cats.implicits.catsSyntaxValidatedIdBinCompat0
5 | import io.apibuilder.api.v0.models.Error
6 |
7 | object Misc {
8 |
9 | def validateEmail(email: String): ValidatedNec[Error, String] = {
10 | def err(msg: String) = Validation.singleError(msg).invalidNec
11 | val trimmed = email.trim
12 |
13 | if (!trimmed.contains("@")) {
14 | err("Email must have an '@' symbol")
15 |
16 | } else if (trimmed == "@") {
17 | err("Invalid Email: missing username and domain")
18 |
19 | } else if (trimmed.startsWith("@")) {
20 | err("Invalid Email: missing username")
21 |
22 | } else if (trimmed.endsWith("@")) {
23 | err("Invalid Email: missing domain")
24 |
25 | } else {
26 | trimmed.validNec
27 | }
28 | }
29 |
30 | def emailDomain(email: String): Option[String] = {
31 | email.trim.split("@").toList match {
32 | case _ :: domain :: Nil => {
33 | Some(domain.toLowerCase.trim).filter(_.nonEmpty)
34 | }
35 | case _ => None
36 | }
37 | }
38 |
39 | }
--------------------------------------------------------------------------------
/.apibuilder/config:
--------------------------------------------------------------------------------
1 | code:
2 | apicollective:
3 | apibuilder-api:
4 | version: latest
5 | generators:
6 | play_2_9_scala_3_client: generated/app
7 | play_2_x_routes: api/conf/routes
8 | apibuilder-api-json:
9 | version: latest
10 | generators:
11 | play_2_x_scala_3_json: core/app/generated
12 | apibuilder-spec:
13 | version: latest
14 | generators:
15 | play_2_x_standalone_json: lib/src/main/scala/generated
16 | play_2_9_scala_3_client: generated/app
17 | play_2_8_mock_client: generated/app
18 | apibuilder-common:
19 | version: latest
20 | generators:
21 | play_2_9_scala_3_client: generated/app
22 | play_2_8_mock_client: generated/app
23 | apibuilder-generator:
24 | version: latest
25 | generators:
26 | play_2_9_scala_3_client: generated/app
27 | play_2_8_mock_client: generated/app
28 | apibuilder-task:
29 | version: latest
30 | generators:
31 | play_2_9_scala_3_client: generated/app
32 | play_2_8_mock_client: generated/app
33 |
34 |
--------------------------------------------------------------------------------
/api/app/models/UsersModel.scala:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import db.{Authorization, InternalUser, InternalUsersDao}
4 | import io.apibuilder.api.v0.models.User
5 | import io.apibuilder.common.v0.models.{Audit, ReferenceGuid}
6 |
7 | import java.util.UUID
8 | import javax.inject.Inject
9 |
10 | class UsersModel @Inject()(
11 | usersDao: InternalUsersDao
12 | ) {
13 |
14 | def toModel(user: InternalUser): User = {
15 | toModels(Seq(user)).head
16 | }
17 |
18 | def toModelByGuids(guids: Seq[UUID]): Seq[User] = {
19 | toModels(usersDao.findAllByGuids(guids.distinct))
20 | }
21 |
22 | def toModels(users: Seq[InternalUser]): Seq[User] = {
23 | users.map { user =>
24 | User(
25 | guid = user.guid,
26 | email = user.email,
27 | name = user.name,
28 | nickname = user.nickname,
29 | audit = Audit(
30 | createdAt = user.db.createdAt,
31 | createdBy = ReferenceGuid(user.db.createdByGuid),
32 | updatedAt = user.db.updatedAt,
33 | updatedBy = ReferenceGuid(user.db.updatedByGuid),
34 | )
35 | )
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/lib/src/main/scala/ValidatedHelpers.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import cats.data.Validated.{Invalid, Valid}
4 | import cats.implicits._
5 | import cats.data.{NonEmptyChain, ValidatedNec}
6 |
7 | trait ValidatedHelpers {
8 | def sequenceUnique(all: Iterable[ValidatedNec[String, Any]]): ValidatedNec[String, Unit] = {
9 | all.toSeq.sequence.map(_ => ()) match {
10 | case Invalid(errors) => errors.toNonEmptyList.distinct.map(_.invalidNec).sequence.map(_ => ())
11 | case Valid(t) => t.validNec
12 | }
13 | }
14 |
15 | def formatErrors(value: ValidatedNec[String, Any]): String = {
16 | value match {
17 | case Invalid(errors) => formatErrors(errors)
18 | case Valid(_) => ""
19 | }
20 | }
21 |
22 | def formatErrors(errors: NonEmptyChain[String]): String = {
23 | errors.toNonEmptyList.toList.mkString(", ")
24 | }
25 |
26 | def addPrefixToError[T](prefix: String, value: ValidatedNec[String, T]): ValidatedNec[String, T] = {
27 | value match {
28 | case Invalid(errors) => s"$prefix: ${formatErrors(errors)}".invalidNec
29 | case Valid(t) => Valid(t)
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/api/test/processor/ProductionTaskProcessorsSpec.scala:
--------------------------------------------------------------------------------
1 | package processor
2 |
3 | import io.apibuilder.task.v0.models.TaskType
4 | import org.scalatestplus.play.PlaySpec
5 | import org.scalatestplus.play.guice.GuiceOneAppPerSuite
6 |
7 | final class ProductionTaskProcessorsSpec extends PlaySpec with GuiceOneAppPerSuite {
8 |
9 | private def companion = app.injector.instanceOf[TaskActorCompanion]
10 |
11 | "each task type is assigned a processor" in {
12 | val missing = TaskType.all.filterNot(companion.all.contains)
13 | if (missing.nonEmpty) {
14 | sys.error(
15 | s"TaskActorCompanion: Missing processor for task type(s): ${missing.map(_.toString).mkString(", ")}",
16 | )
17 | }
18 | }
19 |
20 | "each task type is assigned to at most one processor" in {
21 | val dups = companion.all.values.toSeq.groupBy(_.typ).filter { case (_, v) => v.length > 1 }.map { case (t, all) =>
22 | s"Task type $t is assigned to more than 1 processor: " + all.map(_.getClass.getName).mkString(", ")
23 | }
24 | if (dups.nonEmpty) {
25 | sys.error(dups.mkString(", "))
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/app/lib/TarballFile.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import java.io.{BufferedOutputStream, FileOutputStream}
4 |
5 | import io.apibuilder.generator.v0.models.File
6 | import org.apache.commons.compress.archivers.tar.{TarArchiveEntry, TarArchiveOutputStream}
7 | import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream
8 |
9 | case object TarballFile {
10 |
11 | def create(prefix: String, files: Seq[File]): java.io.File = {
12 | val tmpFilePath = java.io.File.createTempFile(prefix, ".tar.gz")
13 | val tar: TarArchiveOutputStream = new TarArchiveOutputStream(new GzipCompressorOutputStream(new BufferedOutputStream(new FileOutputStream(tmpFilePath))))
14 |
15 | files.foreach { file =>
16 | val path = prefix + "/" + file.dir.fold("")(_ + "/")
17 |
18 | val entry = new TarArchiveEntry(new java.io.File(file.name), s"$path${file.name}")
19 | entry.setSize(file.contents.getBytes.length)
20 | tar.putArchiveEntry(entry)
21 | tar.write(file.contents.getBytes("UTF-8"))
22 | tar.closeArchiveEntry()
23 | }
24 | tar.close()
25 |
26 | tmpFilePath
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/app/app/views/code/index.scala.html:
--------------------------------------------------------------------------------
1 | @(tpl: models.MainTemplate,
2 | orgKey: String,
3 | applicationKey: String,
4 | version: String,
5 | generatorKey: String,
6 | files: Seq[io.apibuilder.generator.v0.models.File]
7 | )(implicit flash: Flash, messages: Messages)
8 |
9 | @main(tpl) {
10 |
11 |
17 |
18 |
27 |
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/swagger/src/test/resources/no-resources.json:
--------------------------------------------------------------------------------
1 | {
2 | "swagger": "2.0",
3 | "info": {
4 | "version": "1.0.0",
5 | "title": "Example with no models in the responses (just type string)",
6 | "description": "Example with no models in the responses (just type string)"
7 | },
8 | "paths": {
9 | "/return-string": {
10 | "get": {
11 | "description": "Returns a string",
12 | "responses": {
13 | "200": {
14 | "description": "Just a random string",
15 | "schema": {
16 | "type": "string"
17 | }
18 | }
19 | }
20 | }
21 | },
22 | "/return-array-of-strings": {
23 | "get": {
24 | "description": "Returns an array of strings",
25 | "responses": {
26 | "200": {
27 | "description": "Just an array of random strings",
28 | "schema": {
29 | "type": "array",
30 | "items": {
31 | "type": "string"
32 | }
33 | }
34 | }
35 | }
36 | }
37 | }
38 | },
39 | "definitions": {}
40 | }
--------------------------------------------------------------------------------
/app/app/views/doc/types.scala.html:
--------------------------------------------------------------------------------
1 | @(
2 | util: lib.Util,
3 | user: Option[io.apibuilder.api.v0.models.User]
4 | )
5 |
6 | @doc.main(routes.DocController.types.url, user, Some("Types")) {
7 |
8 |
9 |
10 | API Builder supports types that are common on the web - and useful for
11 | many web applications. For example, many applications work with
12 | dates and date times - by explicitly supporting these types, we
13 | can simplify working with these types in all web applications, and
14 | for type safe languages, we can map to the appropriate classes in
15 | each language.
16 |
17 |
18 |
19 |
20 | @lib.PrimitiveMetadata.All.sortWith(_.primitive.toString < _.primitive.toString).map { md =>
21 |
22 |
@md.primitive.toString
23 |
24 | @Html(lib.Markdown.toHtml(md.description))
25 | @if(!md.examples.isEmpty) {
26 | Examples:
27 | @Html(md.examples.mkString(""))
28 |
29 | }
30 |
31 | }
32 |
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/api/app/util/UserAgent.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import javax.inject.Inject
4 | import lib.AppConfig
5 |
6 | /**
7 | * Generates safe user agents
8 | */
9 | class UserAgent @Inject() (
10 | appConfig: AppConfig
11 | ) {
12 |
13 | private val Prefixes: Seq[String] = Seq("http://", "https://")
14 |
15 | def generate(
16 | orgKey: String,
17 | applicationKey: String,
18 | versionName: String,
19 | generatorKey: Option[String]
20 | ): String = {
21 | Seq(
22 | "apibuilder",
23 | Seq(
24 | Some("https://" + appConfig.apibuilderWwwHost),
25 | Some(orgKey),
26 | Some(applicationKey),
27 | Some(versionName),
28 | generatorKey
29 | ).flatten.map(format).mkString("/")
30 | ).map(format).mkString(" ")
31 | }
32 |
33 | def format(value: String): String = {
34 | Prefixes.foldLeft(value) { case (v, prefix) =>
35 | val lower = v.toLowerCase()
36 | if (lower.startsWith(prefix)) {
37 | format(v.substring(prefix.length))
38 | } else {
39 | v
40 | }
41 | }.replaceAll(":", " ").replaceAll("\\s+", " ").trim
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/core/test/core/DuplicateFieldValidatorSpec.scala:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import helpers.ApiJsonHelpers
4 | import org.scalatest.funspec.AnyFunSpec
5 | import org.scalatest.matchers.should.Matchers
6 |
7 | class DuplicateFieldValidatorSpec extends AnyFunSpec with Matchers with ApiJsonHelpers {
8 |
9 | it("detects duplicate fields") {
10 | def setup(name1: String, name2: String) = {
11 | TestHelper.serviceValidatorFromApiJson(
12 | s"""
13 | |{
14 | | "name": "duplicate-test",
15 | | "models": {
16 | | "$name1": {
17 | | "fields": [{"name": "placeholder", "type": "string"}]
18 | | },
19 | | "$name2": {
20 | | "fields": [{"name": "placeholder", "type": "string"}]
21 | | }
22 | | }
23 | |}
24 | |""".stripMargin
25 | )
26 | }
27 |
28 | expectValid {
29 | setup("user1", "user2")
30 | }
31 | expectInvalid {
32 | setup("user", "user")
33 | } shouldBe Seq("Invalid json, duplicate key name found: user")
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014-2015 Gilt Groupe, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/api/app/models/TokensModel.scala:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import db.{InternalToken, InternalUsersDao}
4 | import io.apibuilder.api.v0.models.Token
5 | import io.apibuilder.common.v0.models.{Audit, ReferenceGuid}
6 |
7 | import javax.inject.Inject
8 |
9 | class TokensModel @Inject()(usersModel: UsersModel) {
10 |
11 | def toModel(mr: InternalToken): Option[Token] = {
12 | toModels(Seq(mr)).headOption
13 | }
14 |
15 | def toModels(tokens: Seq[InternalToken]): Seq[Token] = {
16 | val users = usersModel.toModelByGuids(tokens.map(_.userGuid)).map { u => u.guid -> u }.toMap
17 |
18 | tokens.flatMap { t =>
19 | users.get(t.userGuid).map { user =>
20 | Token(
21 | guid = t.guid,
22 | maskedToken = t.maskedToken,
23 | description = t.description,
24 | user = user,
25 | audit = Audit(
26 | createdAt = t.db.createdAt,
27 | createdBy = ReferenceGuid(t.db.createdByGuid),
28 | updatedAt = t.db.createdAt,
29 | updatedBy = ReferenceGuid(t.db.createdByGuid),
30 | )
31 | )
32 | }
33 | }
34 | } }
--------------------------------------------------------------------------------
/api/app/controllers/Changes.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import io.apibuilder.api.v0.models.json.*
4 | import db.InternalChangesDao
5 | import models.ChangesModel
6 |
7 | import javax.inject.{Inject, Singleton}
8 | import play.api.mvc.*
9 | import play.api.libs.json.*
10 |
11 | @Singleton
12 | class Changes @Inject() (
13 | val apiBuilderControllerComponents: ApiBuilderControllerComponents,
14 | changesDao: InternalChangesDao,
15 | model: ChangesModel
16 | ) extends ApiBuilderController {
17 |
18 | def get(
19 | orgKey: Option[String],
20 | applicationKey: Option[String],
21 | from: Option[String],
22 | to: Option[String],
23 | `type`: Option[String],
24 | limit: Long = 25,
25 | offset: Long = 0
26 | ): Action[AnyContent] = Anonymous { request =>
27 | val changes = changesDao.findAll(
28 | request.authorization,
29 | organizationKey = orgKey,
30 | applicationKey = applicationKey,
31 | fromVersion = from,
32 | toVersion = to,
33 | `type` = `type`,
34 | limit = Some(limit),
35 | offset = offset
36 | )
37 | Ok(Json.toJson(model.toModels(changes)))
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/swagger/src/main/scala/io/apibuilder/swagger/translators/Response.scala:
--------------------------------------------------------------------------------
1 | package io.apibuilder.swagger.translators
2 |
3 | import lib.Primitives
4 | import io.apibuilder.spec.v0.{models => apidoc}
5 | import io.apibuilder.swagger.Util
6 | import io.swagger.{models => swagger}
7 |
8 | import scala.annotation.nowarn
9 |
10 | object Response {
11 |
12 | @nowarn
13 | def apply(
14 | resolver: Resolver,
15 | code: String,
16 | response: swagger.Response
17 | ): apidoc.Response = {
18 | val responseCode = if (code == "default") {
19 | apidoc.ResponseCodeOption.Default
20 | } else {
21 | apidoc.ResponseCodeInt(code.toInt)
22 | }
23 |
24 | // getExamples
25 | // getHeaders
26 |
27 | apidoc.Response(
28 | code = responseCode,
29 | `type` = Option(response.getSchema) match {
30 | case None => Primitives.Unit.toString
31 | case Some(schema) => resolver.schemaType(schema)
32 | },
33 | description = Option(response.getDescription),
34 | deprecation = None,
35 | attributes = Util.vendorExtensionsToAttributesOpt(response.getVendorExtensions)
36 | )
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/app/app/views/login/resetPassword.scala.html:
--------------------------------------------------------------------------------
1 | @(tpl: models.MainTemplate,
2 | token: String,
3 | form: Form[controllers.LoginController.ResetPasswordData],
4 | errorMessage: Option[String] = None
5 | )(implicit flash: Flash, messages: Messages)
6 |
7 | @main(tpl.copy(headTitle = Some("Reset Password"))) {
8 |
9 | @errorMessage.map { msg =>
10 | @msg
11 | }
12 |
13 | @helper.form(action = routes.LoginController.postResetPassword(token)) {
14 |
15 |
16 |
17 | @helper.inputPassword(
18 | form("password"),
19 | Symbol("_label") -> "Password",
20 | Symbol("_error") -> form.error("password")
21 | )
22 |
23 | @helper.inputPassword(
24 | form("password_verify"),
25 | Symbol("_label") -> "Verify Password",
26 | Symbol("_error") -> form.error("password_verify")
27 | )
28 |
29 |
30 |
31 | Reset Password
32 |
33 |
34 | Go to login form
35 |
36 |
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/lib/src/main/scala/TextDatatype.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | sealed trait TextDatatype
4 |
5 | object TextDatatype {
6 |
7 | case object List extends TextDatatype
8 | case object Map extends TextDatatype
9 | case class Singleton(name: String) extends TextDatatype
10 |
11 | private val ListRx = "^\\[(.*)\\]$".r
12 | private val MapRx = "^map\\[(.*)\\]$".r
13 | private val MapDefaultRx = "^map$".r
14 |
15 | def parse(value: String): Seq[TextDatatype] = {
16 | value match {
17 | case ListRx(t) => Seq(TextDatatype.List) ++ parse(t)
18 | case MapRx(t) => Seq(TextDatatype.Map) ++ parse(t)
19 | case MapDefaultRx() => Seq(TextDatatype.Map, TextDatatype.Singleton(Primitives.String.toString))
20 | case _ => Seq(TextDatatype.Singleton(value))
21 | }
22 | }
23 |
24 | def label(types: Seq[TextDatatype]): String = {
25 | types.toList match {
26 | case Nil => ""
27 | case one :: rest => {
28 | one match {
29 | case List => "[" + label(rest) + "]"
30 | case Map => "map[" + label(rest) + "]"
31 | case Singleton(n) => n + label(rest)
32 | }
33 | }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/app/views/search/index.scala.html:
--------------------------------------------------------------------------------
1 | @(tpl: models.MainTemplate,
2 | util: lib.Util,
3 | q: Option[String],
4 | org: Option[String],
5 | items: lib.PaginatedCollection[io.apibuilder.api.v0.models.Item]
6 | )(implicit flash: Flash, messages: Messages)
7 |
8 | @main(tpl) {
9 |
10 | @if(items.isEmpty) {
11 | No applications found
12 |
13 | } else {
14 |
15 |
16 | @items.items.map { item =>
17 |
18 |
19 | @item.label
20 | @item.description
21 |
22 |
23 | }
24 |
25 |
26 |
27 | @if(items.hasPrevious || items.hasNext) {
28 |
36 | }
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/api/app/controllers/Authentications.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import io.apibuilder.api.v0.models.json._
4 | import util.Conversions
5 | import db.InternalUsersDao
6 | import db.generated.SessionsDao
7 | import javax.inject.{Inject, Singleton}
8 | import play.api.libs.json.Json
9 | import play.api.mvc._
10 |
11 | @Singleton
12 | class Authentications @Inject() (
13 | val apiBuilderControllerComponents: ApiBuilderControllerComponents,
14 | sessionsDao: SessionsDao,
15 | usersDao: InternalUsersDao,
16 | conversions: Conversions
17 | ) extends ApiBuilderController {
18 |
19 | def getSessionById(sessionId: String): Action[AnyContent] = Anonymous { _ =>
20 | sessionsDao.findById(sessionId) match {
21 | case None => NotFound
22 | case Some(session) => {
23 | if (session.deletedAt.isDefined) {
24 | NotFound
25 | } else {
26 | usersDao.findByGuid(session.userGuid) match {
27 | case None => NotFound
28 | case Some(user) => {
29 | Ok(Json.toJson(conversions.toAuthentication(session, user)))
30 | }
31 | }
32 | }
33 | }
34 | }
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/core/test/resources/simple-w-array.json:
--------------------------------------------------------------------------------
1 | {
2 | "swagger": "2.0",
3 | "info": {
4 | "version": "0.0.0",
5 | "title": "Simple API"
6 | },
7 | "paths": {
8 | "/": {
9 | "get": {
10 | "responses": {
11 | "200": {
12 | "description": "OK",
13 | "schema": {
14 | "$ref": "#/definitions/test"
15 | }
16 | }
17 | }
18 | }
19 | }
20 | },
21 | "definitions": {
22 | "test": {
23 | "required": [
24 | "id",
25 | "name"
26 | ],
27 | "properties": {
28 | "id": {
29 | "type": "integer"
30 | },
31 | "name": {
32 | "type": "string"
33 | },
34 | "tag": {
35 | "items": {
36 | "type": "string"
37 | },
38 | "type": "array"
39 | }
40 | }
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/api/test/util/UserAgentSpec.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import org.scalatestplus.play.PlaySpec
4 | import org.scalatestplus.play.guice.GuiceOneAppPerSuite
5 |
6 | class UserAgentSpec extends PlaySpec with GuiceOneAppPerSuite {
7 |
8 | private val userAgent = app.injector.instanceOf[UserAgent]
9 |
10 | "user agent generates valid strings" in {
11 | userAgent.generate(
12 | orgKey = "apicollective",
13 | applicationKey = "apibuilder",
14 | versionName = "1.2.3",
15 | generatorKey = Some("play_client")
16 | ) must fullyMatch regex("apibuilder localhost 9000/apicollective/apibuilder/1\\.2\\.3/play_client")
17 |
18 | userAgent.generate(
19 | orgKey = "apicollective",
20 | applicationKey = "apibuilder",
21 | versionName = "1:0",
22 | generatorKey = Some("play_client")
23 | ) must fullyMatch regex("apibuilder localhost 9000/apicollective/apibuilder/1 0/play_client")
24 |
25 | userAgent.generate(
26 | orgKey = "apicollective",
27 | applicationKey = "apibuilder",
28 | versionName = "1:0",
29 | generatorKey = None
30 | ) must fullyMatch regex("apibuilder localhost 9000/apicollective/apibuilder/1 0")
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/api/app/views/emails/versionUpserted.scala.html:
--------------------------------------------------------------------------------
1 | @(
2 | appConfig: lib.AppConfig,
3 | org: io.apibuilder.api.v0.models.Organization,
4 | application: db.InternalApplication,
5 | version: io.apibuilder.api.v0.models.Version,
6 | breakingDiffs: Seq[io.apibuilder.api.v0.models.Diff],
7 | nonBreakingDiffs: Seq[io.apibuilder.api.v0.models.Diff],
8 | )
9 |
10 | Breaking changes
11 | @if(breakingDiffs.isEmpty) {
12 | None
13 | } else {
14 | @Html(breakingDiffs.map(_.description).mkString(""))
15 | }
16 |
17 | Other material changes
18 | @if(nonBreakingDiffs.filter(_.isMaterial).isEmpty) {
19 | None
20 | } else {
21 | @Html(nonBreakingDiffs.filter(_.isMaterial).map(_.description).mkString(""))
22 | }
23 |
24 | Other changes
25 | @if(nonBreakingDiffs.filterNot(_.isMaterial).isEmpty) {
26 | None
27 | } else {
28 | @Html(nonBreakingDiffs.filterNot(_.isMaterial).map(_.description).mkString(""))
29 | }
30 |
31 |
32 | View version
33 |
34 |
--------------------------------------------------------------------------------
/api/app/lib/DatabaseServiceFetcher.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import core.ServiceFetcher
4 | import db.{Authorization, InternalVersionsDao}
5 |
6 | import javax.inject.Inject
7 | import io.apibuilder.spec.v0.models.Service
8 | import models.VersionsModel
9 |
10 | /**
11 | * Implements service fetch by querying the DB
12 | */
13 | class DatabaseServiceFetcher @Inject() (
14 | versionsDao: InternalVersionsDao,
15 | versionsModel: VersionsModel,
16 | ) {
17 |
18 | def instance(authorization: Authorization): ServiceFetcher = {
19 | new ServiceFetcher {
20 | override def fetch(uri: String): Service = {
21 | val serviceUri = ServiceUri.parse(uri).getOrElse {
22 | sys.error(s"could not parse URI[$uri]")
23 | }
24 |
25 | versionsDao.findVersion(authorization, serviceUri.org, serviceUri.app, serviceUri.version)
26 | .flatMap(versionsModel.toModel)
27 | .map(_.service).getOrElse {
28 | sys.error(s"Error while fetching service for URI[$serviceUri] - could not find [${serviceUri.org}/${serviceUri.app}:${serviceUri.version}]")
29 | }
30 | }
31 | }
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/app/app/views/attributes/index.scala.html:
--------------------------------------------------------------------------------
1 | @(
2 | tpl: models.MainTemplate,
3 | attributes: lib.PaginatedCollection[io.apibuilder.api.v0.models.Attribute]
4 | )(implicit flash: Flash, messages: Messages)
5 |
6 | @main(tpl) {
7 |
8 | @tpl.user.map { user =>
9 |
12 | }
13 |
14 |
15 |
16 | @attributes.items.map { attribute =>
17 |
18 | @attribute.name
19 | @lib.Text.truncate(attribute.description.getOrElse(""))
20 |
21 | }
22 |
23 |
24 |
25 | @if(attributes.hasPrevious || attributes.hasNext) {
26 |
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/app/app/views/generators/service.scala.html:
--------------------------------------------------------------------------------
1 | @(
2 | tpl: models.MainTemplate,
3 | service: io.apibuilder.api.v0.models.GeneratorService,
4 | generatorWithServices: lib.PaginatedCollection[io.apibuilder.api.v0.models.GeneratorWithService]
5 | )(implicit flash: Flash, messages: Messages)
6 |
7 | @main(tpl) {
8 |
9 |
10 | URI: @service.uri
11 | @if(tpl.canDeleteGeneratorService(service)) {
12 | Delete
13 | }
14 |
15 |
16 | @generators.generators(generatorWithServices, displayService = false)
17 |
18 | @if(generatorWithServices.hasPrevious || generatorWithServices.hasNext) {
19 |
27 | }
28 |
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/api/app/processor/UserCreatedProcessor.scala:
--------------------------------------------------------------------------------
1 | package processor
2 |
3 | import cats.implicits._
4 | import cats.data.ValidatedNec
5 | import db._
6 | import io.apibuilder.common.v0.models.MembershipRole
7 | import io.apibuilder.task.v0.models.TaskType
8 |
9 | import java.util.UUID
10 | import javax.inject.Inject
11 |
12 |
13 | class UserCreatedProcessor @Inject()(
14 | args: TaskProcessorArgs,
15 | usersDao: InternalUsersDao,
16 | organizationsDao: InternalOrganizationsDao,
17 | membershipRequestsDao: InternalMembershipRequestsDao,
18 | emailVerificationsDao: InternalEmailVerificationsDao,
19 | ) extends TaskProcessorWithGuid(args, TaskType.UserCreated) {
20 |
21 | override def processRecord(userGuid: UUID): ValidatedNec[String, Unit] = {
22 | usersDao.findByGuid(userGuid).foreach { user =>
23 | organizationsDao.findAllByEmailDomain(user.email).foreach { org =>
24 | membershipRequestsDao.upsert(user, org, user, MembershipRole.Member)
25 | }
26 | emailVerificationsDao.upsert(user, user, user.email)
27 | }
28 | ().validNec
29 | }
30 |
31 | }
--------------------------------------------------------------------------------
/avro/src/main/scala/io/apibuilder/avro/SchemaType.scala:
--------------------------------------------------------------------------------
1 | package io.apibuilder.avro
2 |
3 | import org.apache.avro.Schema
4 |
5 | sealed trait SchemaType
6 |
7 | /**
8 | * Scala enumeration of the AVRO types
9 | */
10 | object SchemaType {
11 |
12 | case object Array extends SchemaType
13 | case object Boolean extends SchemaType
14 | case object Bytes extends SchemaType
15 | case object Double extends SchemaType
16 | case object Enum extends SchemaType
17 | case object Fixed extends SchemaType
18 | case object Float extends SchemaType
19 | case object Int extends SchemaType
20 | case object Long extends SchemaType
21 | case object Map extends SchemaType
22 | case object Null extends SchemaType
23 | case object Record extends SchemaType
24 | case object String extends SchemaType
25 | case object Union extends SchemaType
26 |
27 | val all = Seq(Array, Boolean, Bytes, Double, Enum, Fixed, Float, Int, Long, Map, Null, Record, String, Union)
28 |
29 | private
30 | val byName = all.map(x => x.toString.toLowerCase -> x).toMap
31 |
32 | def fromAvro(avroType: org.apache.avro.Schema.Type) = fromString(avroType.toString)
33 |
34 | def fromString(value: String): Option[SchemaType] = byName.get(value.toLowerCase)
35 |
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/api/app/util/BasicAuthorization.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import org.apache.commons.codec.binary.Base64
4 |
5 | object BasicAuthorization {
6 |
7 | sealed trait Authorization
8 | case class Session(id: String) extends Authorization
9 | case class Token(token: String) extends Authorization
10 | case class User(user: String, password: String) extends Authorization
11 |
12 | def get(value: Option[String]): Option[Authorization] = {
13 | value.flatMap(get)
14 | }
15 |
16 | /**
17 | * Parses the actual authorization header value
18 | */
19 | def get(value: String): Option[Authorization] = {
20 | val parts = value.split(" ").toSeq
21 | if (parts.length == 2 && parts.head == "Basic") {
22 | val userPassword = new String(Base64.decodeBase64(parts.last.getBytes)).split(":").toSeq
23 |
24 | if (userPassword.length == 1) {
25 | Some(Token(userPassword.head))
26 |
27 | } else if (userPassword.length == 2) {
28 | Some(User(userPassword.head, userPassword.last))
29 |
30 | } else {
31 | None
32 | }
33 |
34 | } else if (parts.length == 2 && parts.head.toLowerCase == "session") {
35 | Some(Session(parts.last))
36 |
37 | } else {
38 | None
39 | }
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/api/app/util/LockUtil.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import anorm._
4 | import io.flow.postgresql.Query
5 | import org.apache.commons.codec.digest.DigestUtils
6 | import play.api.db.Database
7 |
8 | import java.nio.ByteBuffer
9 | import java.sql.Connection
10 | import javax.inject.{Inject, Singleton}
11 |
12 | @Singleton
13 | class LockUtil @Inject() (
14 | db: Database
15 | ) {
16 |
17 | def lock[T](id: String)(f: Connection => T): Option[T] =
18 | db.withTransaction(lock(_)(id)(f))
19 |
20 | def lock[T](c: Connection)(id: String)(f: Connection => T): Option[T] = {
21 | require(!c.getAutoCommit, "Must be in a transaction")
22 | if (acquireLock(c)(id))
23 | Some(f(c))
24 | else
25 | None
26 | }
27 |
28 | private def acquireLock(c: Connection)(id: String): Boolean = {
29 | val (key1, key2) = toHashInts(id)
30 | Query("SELECT pg_try_advisory_xact_lock({key1}::int, {key2}::int)")
31 | .bind("key1", key1)
32 | .bind("key2", key2)
33 | .as(SqlParser.bool(1).single)(using c)
34 | }
35 |
36 | private def toHashInts(id: String): (Int, Int) = {
37 | val buffer = ByteBuffer.wrap(DigestUtils.md5(id))
38 | val key1 = buffer.getInt(0)
39 | val key2 = buffer.getInt(4)
40 | (key1, key2)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/app/views/doc/attributes.scala.html:
--------------------------------------------------------------------------------
1 | @(
2 | util: lib.Util,
3 | user: Option[io.apibuilder.api.v0.models.User]
4 | )
5 |
6 | @doc.main(routes.DocController.playRoutesFile.url, user, Some("Attributes")) {
7 |
8 | API Builder supports the notion of 'attributes' that can be used to
9 | enhance code generation:
10 |
11 |
12 |
13 | Attributes are globally unique and have URL friendly names.
14 |
15 | Attributes have an optional description.
16 |
17 | Attribute values can be set at the organization level,
18 | cascading to all of the applications that belong to that
19 | organization. Organization level values can be found by going to
20 | your organization, clicking '@lib.Labels.OrgDetailsText' in the
21 | nav bar, then '@lib.Labels.OrgAttributesText'.
22 |
23 | A code generator can return a list of the attribute names on
24 | which it depends. API Builder will then send any matching attribute
25 | values when invoking the code generator.
26 |
27 |
28 |
29 |
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/core/test/core/builder/api_json/InternalDatatypeSpec.scala:
--------------------------------------------------------------------------------
1 | package builder.api_json
2 |
3 | import org.scalatest.funspec.AnyFunSpec
4 | import org.scalatest.matchers.should.Matchers
5 |
6 | class InternalDatatypeSpec extends AnyFunSpec with Matchers {
7 |
8 | private val internalDatatypeBuilder = InternalDatatypeBuilder()
9 |
10 | it("label") {
11 | Seq("string", "uuid", "[string]", "[uuid]", "map[string]", "map[uuid]").foreach { name =>
12 | val dt = internalDatatypeBuilder.fromString(name).toOption.get
13 | dt.label should be(name)
14 | dt.required should be(true)
15 | }
16 | }
17 |
18 | it("map defaults to string type") {
19 | internalDatatypeBuilder.fromString("map").toOption.get.label should be("map[string]")
20 | }
21 |
22 | it("handles malformed input") {
23 | internalDatatypeBuilder.fromString("[").toOption.get.label should be("[")
24 |
25 | internalDatatypeBuilder.fromString("]").toOption.get.label should be("]")
26 |
27 | // Questionable how best to handle this. For now we allow empty
28 | // string - will get caught downstream when validating that the
29 | // name of the datatype is a valid name
30 | internalDatatypeBuilder.fromString("[]").toOption.get.label should be("[]")
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/api/app/controllers/PasswordResetRequests.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import db.{InternalUsersDao, InternalPasswordResetsDao}
4 | import io.apibuilder.api.v0.models.PasswordResetRequest
5 | import io.apibuilder.api.v0.models.json.*
6 | import lib.Validation
7 | import models.UsersModel
8 | import play.api.libs.json.*
9 | import play.api.mvc.*
10 |
11 | import javax.inject.{Inject, Singleton}
12 |
13 | @Singleton
14 | class PasswordResetRequests @Inject() (
15 | val apiBuilderControllerComponents: ApiBuilderControllerComponents,
16 | passwordResetRequestsDao: InternalPasswordResetsDao,
17 | usersDao: InternalUsersDao,
18 | ) extends ApiBuilderController {
19 |
20 | def post(): Action[JsValue] = Anonymous(parse.json) { request =>
21 | request.body.validate[PasswordResetRequest] match {
22 | case e: JsError => {
23 | Conflict(Json.toJson(Validation.invalidJson(e)))
24 | }
25 | case JsSuccess(data: PasswordResetRequest, _) => {
26 | usersDao.findByEmail(data.email).map { user =>
27 | passwordResetRequestsDao.create(request.user, user)
28 | }
29 | NoContent
30 | }
31 | }
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/api/test/util/GeneratorServiceUtilSpec.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import db.Authorization
4 | import db.generators.GeneratorHelpers
5 | import modules.clients.MockGeneratorsData
6 | import org.scalatestplus.play.PlaySpec
7 | import org.scalatestplus.play.guice.GuiceOneAppPerSuite
8 |
9 | import java.util.UUID
10 | import scala.concurrent.ExecutionContext.Implicits.global
11 |
12 | class GeneratorServiceUtilSpec extends PlaySpec with GuiceOneAppPerSuite with GeneratorHelpers {
13 |
14 | private def util: GeneratorServiceUtil = app.injector.instanceOf[GeneratorServiceUtil]
15 | private val data: MockGeneratorsData = app.injector.instanceOf[MockGeneratorsData]
16 |
17 | "syncAll" in {
18 | val s1 = createGeneratorService()
19 | val g1 = makeGenerator()
20 | val s2 = createGeneratorService()
21 | val g2 = makeGenerator()
22 | data.add(s1.uri, g1)
23 | data.add(s2.uri, g2)
24 |
25 | def find(serviceGuid: UUID) = {
26 | generatorsDao.findAll(
27 | serviceGuid = Some(serviceGuid),
28 | limit = Some(1)
29 | ).headOption
30 | }
31 |
32 | find(s1.guid) mustBe None
33 |
34 | util.syncAll(pageSize = 1)
35 | find(s1.guid).value.serviceGuid mustBe s1.guid
36 | find(s2.guid).value.serviceGuid mustBe s2.guid
37 | }
38 | }
--------------------------------------------------------------------------------
/swagger/src/test/scala/io/apibuilder/swagger/SwaggerDataSpec.scala:
--------------------------------------------------------------------------------
1 | package io.apibuilder.swagger
2 |
3 | import java.util
4 |
5 | import io.apibuilder.spec.v0.models.Attribute
6 | import io.swagger.models.SecurityRequirement
7 | import io.swagger.models.auth.SecuritySchemeDefinition
8 | import org.scalatest.funspec.AnyFunSpec
9 | import org.scalatest.matchers.should.Matchers
10 | import play.api.libs.json.{JsObject, JsString}
11 |
12 | class SwaggerDataSpec extends AnyFunSpec with Matchers {
13 |
14 | it("all data points empty/null") {
15 | SwaggerData().toAttribute should be (None)
16 | SwaggerData(
17 | serviceSecurity = new util.ArrayList[SecurityRequirement](),
18 | operationSecurity = new util.ArrayList[util.Map[String, util.List[String]]](),
19 | securityDefinitions = new util.HashMap[String, SecuritySchemeDefinition]()
20 | ).toAttribute should be (None)
21 | }
22 |
23 | it("one data point present") {
24 | SwaggerData(
25 | example = "Example object"
26 | ).toAttribute should be(
27 | Some( Attribute(
28 | name = SwaggerData.AttributeName,
29 | value = JsObject(Seq(
30 | ("example", JsString("Example object")))),
31 | description = Some(SwaggerData.AttributeDescription)
32 | )))
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/app/views/versions/parameters.scala.html:
--------------------------------------------------------------------------------
1 | @(org: io.apibuilder.api.v0.models.Organization,
2 | app: io.apibuilder.api.v0.models.Application,
3 | version: String,
4 | service: io.apibuilder.spec.v0.models.Service,
5 | operation: io.apibuilder.spec.v0.models.Operation
6 | )
7 |
8 |
9 |
10 |
11 | Name
12 | Type
13 | Location
14 | Required?
15 | Default
16 | Description
17 |
18 |
19 |
20 | @operation.parameters.map { param =>
21 |
22 | @param.name
23 | @datatype(org, app, version, service, param.`type`)
24 | @param.location
25 | @if(param.required) { Yes } else { No }
26 | @param.default.getOrElse("-")
27 | @Html(lib.Markdown(param.description))
28 |
29 | @param.minimum.map { v => Minimum: @v }
30 | @param.maximum.map { v => Maximum: @v }
31 |
32 | @param.example.map { example =>
33 | Example: @example
34 | }
35 |
36 | @param.deprecation.map(deprecation(_))
37 |
38 |
39 |
40 | }
41 |
42 |
43 |
--------------------------------------------------------------------------------
/core/test/core/ServiceHeadersSpec.scala:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import helpers.{ServiceHelpers, ValidatedTestHelpers}
4 | import org.scalatest.funspec.AnyFunSpec
5 | import org.scalatest.matchers.should.Matchers
6 |
7 | class ServiceHeadersSpec extends AnyFunSpec with Matchers
8 | with ServiceHelpers
9 | with ValidatedTestHelpers
10 | {
11 |
12 | it("headers can reference imported enums") {
13 | val enumsService = makeService(
14 | enums = Seq(
15 | makeEnum(
16 | name = "content_type",
17 | values = Seq(makeEnumValue("application/json"))
18 | )
19 | )
20 | )
21 |
22 | val service = s"""
23 | {
24 | "name": "API Builder",
25 | "apidoc": { "version": "0.9.6" },
26 | "imports": [
27 | { "uri": "${makeImportUri(enumsService)}" }
28 | ],
29 |
30 | "headers": [
31 | { "name": "Content-Type", "type": "${enumsService.namespace}.enums.content_type" }
32 | ]
33 | }
34 | """
35 |
36 | val fetcher = MockServiceFetcher()
37 | fetcher.add(makeImportUri(enumsService), enumsService)
38 |
39 | expectValid {
40 | TestHelper.serviceValidatorFromApiJson(service, fetcher = fetcher)
41 | }.headers.find(_.name == "Content-Type").get.`type` should be(s"${enumsService.namespace}.enums.content_type")
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/app/app/controllers/MembershipRequestReviews.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import play.api.mvc.{Action, AnyContent}
4 |
5 | import java.util.UUID
6 | import javax.inject.Inject
7 | import scala.concurrent.ExecutionContext
8 |
9 | class MembershipRequestReviews @Inject() (
10 | val apiBuilderControllerComponents: ApiBuilderControllerComponents
11 | ) extends ApiBuilderController {
12 |
13 | private implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global
14 |
15 | def accept(orgKey: String, membershipRequestGuid: UUID): Action[AnyContent] = IdentifiedOrg.async { implicit request =>
16 | request.withAdmin {
17 | for {
18 | _ <- request.api.MembershipRequests.postAcceptByGuid(membershipRequestGuid)
19 | } yield {
20 | Redirect(routes.Organizations.membershipRequests(orgKey)).flashing("success" -> "Request accepted")
21 | }
22 | }
23 | }
24 |
25 | def decline(orgKey: String, membershipRequestGuid: UUID): Action[AnyContent] = IdentifiedOrg.async { implicit request =>
26 | request.withAdmin {
27 | for {
28 | _ <- request.api.MembershipRequests.postDeclineByGuid(membershipRequestGuid)
29 | } yield {
30 | Redirect(routes.Organizations.membershipRequests(orgKey)).flashing("success" -> "Request declined")
31 | }
32 | }
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/examples/example-interface.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example-interface",
3 |
4 | "unions": {
5 | "person": {
6 | "discriminator": "discriminator",
7 | "interfaces": ["person"],
8 | "types": [
9 | { "type": "guest" },
10 | { "type": "user" }
11 | ]
12 | }
13 | },
14 |
15 | "interfaces": {
16 | "database_record": {
17 | "fields": [
18 | { "name": "id", "type": "string" }
19 | ]
20 | },
21 | "person": {
22 | "fields": [
23 | { "name": "email", "type": "string", "required": false },
24 | { "name": "name", "type": "string", "required": false }
25 | ]
26 | }
27 | },
28 |
29 | "models": {
30 | "user": {
31 | "interfaces": ["database_record"],
32 | "fields": [
33 | { "name": "id", "type": "string" },
34 | { "name": "email", "type": "string", "required": false },
35 | { "name": "name", "type": "string", "required": false },
36 | { "name": "registration_date", "type": "date-time-iso8601" }
37 | ]
38 | },
39 |
40 | "guest": {
41 | "fields": [
42 | { "name": "session_id", "type": "string" },
43 | { "name": "email", "type": "string", "required": false },
44 | { "name": "name", "type": "string", "required": false }
45 | ]
46 | }
47 | }
48 | }
49 |
50 |
51 |
--------------------------------------------------------------------------------
/.apibuilder/.tracked_files:
--------------------------------------------------------------------------------
1 | ---
2 | apicollective:
3 | apibuilder-api:
4 | play_2_9_scala_3_client:
5 | - generated/app/ApicollectiveApibuilderApiV0Client.scala
6 | play_2_x_routes:
7 | - api/conf/routes
8 | apibuilder-api-json:
9 | play_2_x_scala_3_json:
10 | - core/app/generated/ApicollectiveApibuilderApiJsonV0Models.scala
11 | apibuilder-common:
12 | play_2_8_mock_client:
13 | - generated/app/ApicollectiveApibuilderCommonV0MockClient.scala
14 | play_2_9_scala_3_client:
15 | - generated/app/ApicollectiveApibuilderCommonV0Client.scala
16 | apibuilder-generator:
17 | play_2_8_mock_client:
18 | - generated/app/ApicollectiveApibuilderGeneratorV0MockClient.scala
19 | play_2_9_scala_3_client:
20 | - generated/app/ApicollectiveApibuilderGeneratorV0Client.scala
21 | apibuilder-spec:
22 | play_2_8_mock_client:
23 | - generated/app/ApicollectiveApibuilderSpecV0MockClient.scala
24 | play_2_9_scala_3_client:
25 | - generated/app/ApicollectiveApibuilderSpecV0Client.scala
26 | play_2_x_standalone_json:
27 | - lib/src/main/scala/generated/ApicollectiveApibuilderSpecV0Models.scala
28 | apibuilder-task:
29 | play_2_8_mock_client:
30 | - generated/app/ApicollectiveApibuilderTaskV0MockClient.scala
31 | play_2_9_scala_3_client:
32 | - generated/app/ApicollectiveApibuilderTaskV0Client.scala
33 |
--------------------------------------------------------------------------------
/api/test/services/BatchDownloadApplicationsServiceSpec.scala:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import controllers.MockClient
4 | import db.Authorization
5 | import org.scalatestplus.play.PlaySpec
6 | import org.scalatestplus.play.guice.GuiceOneServerPerSuite
7 |
8 | class BatchDownloadApplicationsServiceSpec extends PlaySpec with MockClient with GuiceOneServerPerSuite
9 | with helpers.BatchDownloadApplicationHelpers
10 | with helpers.ValidatedTestHelpers
11 | {
12 | private def batchDownloadApplicationsService: BatchDownloadApplicationsService = app.injector.instanceOf[BatchDownloadApplicationsService]
13 |
14 | "process multiple applications" in {
15 | val org = createOrganization()
16 | val version1 = createVersion(createApplication(org))
17 | val version2 = createVersion(createApplication(org))
18 |
19 | expectValid {
20 | batchDownloadApplicationsService.process(
21 | Authorization.All,
22 | org.key,
23 | makeBatchDownloadApplicationsForm(
24 | applications = Seq(
25 | makeBatchDownloadApplicationForm(version1.application.key),
26 | makeBatchDownloadApplicationForm(version2.application.key),
27 | )
28 | )
29 | )
30 | }.applications.map(_.application.key) must equal(
31 | Seq(version1.application.key, version2.application.key)
32 | )
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/app/app/views/attributes/show.scala.html:
--------------------------------------------------------------------------------
1 | @(
2 | tpl: models.MainTemplate,
3 | attr: io.apibuilder.api.v0.models.Attribute,
4 | generatorWithServices: lib.PaginatedCollection[io.apibuilder.api.v0.models.GeneratorWithService]
5 | )(implicit flash: Flash, messages: Messages)
6 |
7 | @main(tpl) {
8 |
9 | @if(tpl.canEditAttribute(attr)) {
10 |
13 | }
14 |
15 | @Html(lib.Markdown.toHtml(attr.description.getOrElse("")))
16 |
17 | Code Generators Using this Attribute
18 | @if(generatorWithServices.isEmpty) {
19 | None
20 | } else {
21 | @generators.generators(generatorWithServices)
22 |
23 | @if(generatorWithServices.hasPrevious || generatorWithServices.hasNext) {
24 |
32 | }
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/app/app/views/members/add.scala.html:
--------------------------------------------------------------------------------
1 | @(tpl: models.MainTemplate,
2 | addForm: Form[controllers.Members.AddMemberData],
3 | error: Option[String] = None
4 | )(implicit flash: Flash, messages: Messages)
5 |
6 | @import helper._
7 |
8 |
9 | @main(tpl) {
10 |
11 |
12 | @helper.form(action = routes.Members.addPost(tpl.org.get.key)) {
13 |
14 | @error.map { msg =>
@msg }
15 |
16 |
17 |
18 | @helper.inputText(
19 | addForm("email"),
20 | Symbol("_label") -> "Email address",
21 | Symbol("_error") -> addForm.error("email")
22 | )
23 |
24 | @helper.inputText(
25 | addForm("nickname"),
26 | Symbol("_label") -> "Or nickname",
27 | Symbol("_error") -> addForm.error("nickname")
28 | )
29 |
30 | @helper.select(
31 | addForm("role"),
32 | options = io.apibuilder.common.v0.models.MembershipRole.all.map { r => (r.toString, lib.Text.initCap(r.toString)) },
33 | Symbol("_label") -> "Role",
34 | Symbol("_error") -> addForm.error("role")
35 | )
36 |
37 |
38 |
39 | Submit
40 |
41 | }
42 |
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/app/app/views/account/profile/edit.scala.html:
--------------------------------------------------------------------------------
1 | @(tpl: models.MainTemplate,
2 | user: io.apibuilder.api.v0.models.User,
3 | form: Form[controllers.AccountProfileController.ProfileData],
4 | errors: Seq[String] = Nil
5 | )(implicit flash: Flash, messages: Messages)
6 |
7 | @main(tpl.copy(title = Some("Edit Profile")), errorMessages = errors) {
8 |
9 | @helper.form(action = routes.AccountProfileController.postEdit()) {
10 |
11 | @form.globalErrors.map(_.message).map { msg =>
12 | @msg
13 | }
14 |
15 |
16 | @helper.inputText(
17 | form("email"),
18 | Symbol("_label") -> "Email address",
19 | Symbol("_error") -> form.error("email")
20 | )
21 |
22 | @helper.inputText(
23 | form("nickname"),
24 | Symbol("_label") -> "Nickname",
25 | Symbol("_error") -> form.error("nickname")
26 | )
27 |
28 | @helper.inputText(
29 | form("name"),
30 | Symbol("_label") -> "Name",
31 | Symbol("_error") -> form.error("name")
32 | )
33 |
34 |
35 |
36 | Save
37 | Cancel
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/app/app/controllers/SearchController.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import lib.{PaginatedCollection, Pagination, Util}
4 | import play.api.mvc.{Action, AnyContent}
5 |
6 | import javax.inject.Inject
7 | import scala.concurrent.ExecutionContext
8 |
9 | class SearchController @Inject() (
10 | val apiBuilderControllerComponents: ApiBuilderControllerComponents,
11 | util: Util
12 | ) extends ApiBuilderController {
13 |
14 | private implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global
15 |
16 | def index(q: Option[String], org: Option[String], page: Int = 0): Action[AnyContent] = Anonymous.async { implicit request =>
17 | val finalQuery = Seq(
18 | org.map { key => s"org:$key" },
19 | q
20 | ).filter(_.isDefined).flatten.mkString(" ")
21 |
22 | for {
23 | items <- request.api.items.get(
24 | q = Some(finalQuery),
25 | limit = Pagination.DefaultLimit+1,
26 | offset = page * Pagination.DefaultLimit
27 | )
28 | } yield {
29 | Ok(views.html.search.index(
30 | request.mainTemplate().copy(
31 | query = Some(finalQuery)
32 | ),
33 | util,
34 | q = q,
35 | org = org,
36 | items = PaginatedCollection(page, items)
37 | ))
38 | }
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/app/app/views/application_settings/form.scala.html:
--------------------------------------------------------------------------------
1 | @(tpl: models.MainTemplate,
2 | form: Form[controllers.ApplicationSettings.Settings],
3 | errors: Seq[String] = Seq.empty
4 | )(implicit flash: Flash, messages: Messages)
5 |
6 | @main(tpl) {
7 |
8 |
9 | @helper.form(action = routes.ApplicationSettings.postEdit(tpl.org.get.key, tpl.application.get.key, tpl.version.get)) {
10 |
11 |
12 | @if(!errors.isEmpty) {
13 |
14 | @errors.map { msg => @msg }
15 |
16 | }
17 |
18 | @helper.select(
19 | form("visibility"),
20 | Seq( ("" -> "-- select --") ) ++ io.apibuilder.api.v0.models.Visibility.all.map( v => (v.toString -> v.toString) ),
21 | Symbol("_label") -> "Visibility",
22 | Symbol("_error") -> form.error("visibility"),
23 | Symbol("_help") -> "Controls who is able to view this application."
24 | )
25 |
26 |
27 |
28 |
32 |
33 | }
34 |
35 |
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/core/app/builder/api_json/ServiceJsonServiceValidator.scala:
--------------------------------------------------------------------------------
1 | package builder.api_json
2 |
3 | import cats.data.ValidatedNec
4 | import cats.implicits._
5 | import com.fasterxml.jackson.core.{JsonParseException, JsonProcessingException}
6 | import cats.implicits._
7 | import cats.data.ValidatedNec
8 | import io.apibuilder.spec.v0.models.Service
9 | import io.apibuilder.spec.v0.models.json._
10 | import lib.ServiceValidator
11 | import play.api.libs.json.{JsError, JsSuccess, Json}
12 |
13 | import scala.util.{Failure, Success, Try}
14 |
15 | object ServiceJsonServiceValidator extends ServiceValidator[Service] {
16 |
17 | def validate(rawInput: String): ValidatedNec[String, Service] = {
18 | Try(Json.parse(rawInput)) match {
19 | case Success(js) => {
20 | js.validate[Service] match {
21 | case e: JsError => {
22 | ("Not a valid service.json document: " + e.toString).invalidNec
23 | }
24 | case s: JsSuccess[Service] => {
25 | s.get.validNec
26 | }
27 | }
28 | }
29 |
30 | case Failure(ex) => ex match {
31 | case e: JsonParseException => {
32 | ("Invalid JSON: " + e.getMessage).invalidNec
33 | }
34 | case e: JsonProcessingException => {
35 | ("Invalid JSON: " + e.getMessage).invalidNec
36 | }
37 | }
38 | }
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/api/app/processor/ScheduleSyncGeneratorServicesProcessor.scala:
--------------------------------------------------------------------------------
1 | package processor
2 |
3 | import cats.data.ValidatedNec
4 | import cats.implicits._
5 | import db.generators.InternalGeneratorServicesDao
6 | import db.{Authorization, InternalTasksDao}
7 | import io.apibuilder.task.v0.models.TaskType
8 | import lib.ValidatedHelpers
9 |
10 | import javax.inject.Inject
11 | import scala.annotation.tailrec
12 |
13 |
14 | class ScheduleSyncGeneratorServicesProcessor @Inject()(
15 | args: TaskProcessorArgs,
16 | servicesDao: InternalGeneratorServicesDao,
17 | internalTasksDao: InternalTasksDao
18 | ) extends TaskProcessor(args, TaskType.ScheduleSyncGeneratorServices) with ValidatedHelpers {
19 |
20 | override def processRecord(id: String): ValidatedNec[String, Unit] = {
21 | doSyncAll(pageSize = 200, offset = 0).validNec
22 | }
23 |
24 | @tailrec
25 | private def doSyncAll(pageSize: Long, offset: Long): Unit = {
26 | val all = servicesDao.findAll(
27 | limit = Some(pageSize),
28 | offset = offset
29 | ).map(_.guid.toString)
30 |
31 | if (all.nonEmpty) {
32 | internalTasksDao.queueBatch(TaskType.SyncGeneratorService, all)
33 | doSyncAll(pageSize, offset + all.length)
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/app/app/views/generators/generators.scala.html:
--------------------------------------------------------------------------------
1 | @(
2 | generatorWithServices: lib.PaginatedCollection[io.apibuilder.api.v0.models.GeneratorWithService],
3 | displayService: Boolean = true
4 | )
5 |
6 | @if(generatorWithServices.isEmpty) {
7 | No generators
8 |
9 | } else {
10 |
11 |
12 |
13 |
14 | @if(displayService) {
15 | Service
16 | }
17 | Key
18 | Name
19 | Language
20 | Attributes
21 | Description
22 |
23 |
24 | @generatorWithServices.items.map { gws =>
25 |
26 | @if(displayService) {
27 | @lib.Text.truncate(gws.service.uri)
28 | }
29 | @gws.generator.key
30 | @gws.generator.name
31 | @gws.generator.language.getOrElse("")
32 | @gws.generator.attributes.map { attr =>
33 | @attr
34 | }
35 | @Html(lib.Markdown(gws.generator.description.map(lib.Text.truncate(_))))
36 |
37 | }
38 |
39 |
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/api/test/db/InternalSubscriptionsDaoSpec.scala:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import io.apibuilder.api.v0.models.{Organization, Publication}
4 | import io.apibuilder.common.v0.models.MembershipRole
5 | import org.scalatestplus.play.PlaySpec
6 | import org.scalatestplus.play.guice.GuiceOneAppPerSuite
7 |
8 | class InternalSubscriptionsDaoSpec extends PlaySpec with GuiceOneAppPerSuite with db.Helpers {
9 |
10 | private lazy val org: InternalOrganization = createOrganization()
11 |
12 | "when a user loses admin role, we remove subscriptions that require admin" in {
13 | val user = createRandomUser()
14 | val membership = createMembership(org, user, MembershipRole.Admin)
15 |
16 | Publication.all.foreach { publication => createSubscription(org, user, publication) }
17 |
18 | membershipsDao.softDelete(testUser.reference, membership)
19 |
20 | val subscriptions = subscriptionsDao.findAll(
21 | Authorization.All,
22 | organizationGuid = Some(org.guid),
23 | userGuid = Some(user.guid),
24 | limit = None
25 | ).map(_.publication)
26 |
27 | Publication.all.foreach { publication =>
28 | if (InternalSubscriptionsDao.PublicationsRequiredAdmin.contains(publication)) {
29 | subscriptions.contains(publication) must be(false)
30 | } else {
31 | subscriptions.contains(publication) must be(true)
32 | }
33 | }
34 |
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/core/test/core/ServiceCommonReturnTypeSpec.scala:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import helpers.ApiJsonHelpers
4 | import org.scalatest.funspec.AnyFunSpec
5 | import org.scalatest.matchers.should.Matchers
6 |
7 | class ServiceCommonReturnTypeSpec extends AnyFunSpec with Matchers with ApiJsonHelpers {
8 |
9 | it("all 2xx return types must share a common types") {
10 | val json = """
11 | {
12 | "name": "API Builder",
13 | "apidoc": { "version": "0.9.6" },
14 | "models": {
15 | "user": {
16 | "fields": [
17 | { "name": "guid", "type": "uuid" }
18 | ]
19 | }
20 | },
21 | "resources": {
22 | "user": {
23 | "operations": [
24 | {
25 | "method": "GET",
26 | "responses": {
27 | "200": { "type": "[user]" }
28 | }
29 | },
30 | {
31 | "method": "POST",
32 | "responses": {
33 | "200": { "type": "user" },
34 | "201": { "type": "%s" }
35 | }
36 | }
37 | ]
38 | }
39 | }
40 | }
41 | """
42 | setupValidApiJson(json.format("user"))
43 | TestHelper.expectSingleError(json.format("[user]")) should be("Resource[user] cannot have varying response types for 2xx response codes: [user], user")
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/app/app/views/versions/unionTypes.scala.html:
--------------------------------------------------------------------------------
1 | @(org: io.apibuilder.api.v0.models.Organization,
2 | app: io.apibuilder.api.v0.models.Application,
3 | version: String,
4 | service: io.apibuilder.spec.v0.models.Service,
5 | union: io.apibuilder.spec.v0.models.Union)
6 |
7 |
8 |
9 |
10 | Type
11 | Discriminator Value
12 | Example Json
13 | Description
14 |
15 |
16 |
17 | @union.types.map { t =>
18 |
19 | @datatype(org, app, version, service, t.`type`)
20 | @t.discriminatorValue.getOrElse(t.`type`)
21 | @if(t.default.getOrElse(false)) {
22 | (Default)
23 | }
24 |
25 | Minimal |
26 | Full
27 |
28 | @Html(lib.Markdown(t.description))
29 |
30 | @t.deprecation.map(deprecation(_))
31 |
32 |
33 |
34 | }
35 |
36 |
37 |
--------------------------------------------------------------------------------
/api/app/util/Tables.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import db.generated.{ApplicationsTable, OrganizationsTable, UserPasswordsTable}
4 |
5 | sealed trait PrimaryKey {
6 | def name: String
7 | }
8 | object PrimaryKey {
9 | case object PkeyString extends PrimaryKey {
10 | override val name = "id"
11 | }
12 | case object PkeyLong extends PrimaryKey {
13 | override val name = "id"
14 | }
15 | case object PkeyUUID extends PrimaryKey {
16 | override val name = "guid"
17 | }
18 | }
19 |
20 | case class Table(schema: String, name: String, pkey: PrimaryKey) {
21 | def qualified: String = s"$schema.$name"
22 | }
23 | object Table {
24 | def public(name: String, pkey: PrimaryKey): Table = Table("public", name, pkey)
25 | def guid(schema: String, name: String): Table = Table(schema, name, PrimaryKey.PkeyUUID)
26 | def string(schema: String, name: String): Table = Table(schema, name, PrimaryKey.PkeyString)
27 | def long(schema: String, name: String): Table = Table(schema, name, PrimaryKey.PkeyLong)
28 | }
29 |
30 | object Tables {
31 | val organizations: Table = Table.public(OrganizationsTable.TableName, PrimaryKey.PkeyUUID)
32 | val applications: Table = Table.public(ApplicationsTable.TableName, PrimaryKey.PkeyUUID)
33 | val versions: Table = Table.public("versions", PrimaryKey.PkeyUUID)
34 | val userPasswords: Table = Table.public(UserPasswordsTable.TableName, PrimaryKey.PkeyUUID)
35 | }
36 |
--------------------------------------------------------------------------------
/core/test/core/ImportedResourcePathsSpec.scala:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import helpers.ValidatedTestHelpers
4 | import org.scalatest.funspec.AnyFunSpec
5 | import org.scalatest.matchers.should.Matchers
6 |
7 | class ImportedResourcePathsSpec extends AnyFunSpec with Matchers with ValidatedTestHelpers {
8 |
9 | it("generates appropriate path for resources from imported models") {
10 | val common = """
11 | {
12 | "name": "common",
13 | "namespace": "test.common.v0",
14 | "models": {
15 | "user": {
16 | "fields": [
17 | { "name": "id", "type": "string" }
18 | ]
19 | }
20 | }
21 | }
22 | """
23 |
24 | val uri = "http://localhost/test/common/0.0.1/service.json"
25 | val user = s"""
26 | {
27 | "name": "user",
28 | "imports": [ { "uri": "$uri" } ],
29 |
30 | "resources": {
31 | "test.common.v0.models.user": {
32 | "operations": [
33 | { "method": "DELETE" }
34 | ]
35 | }
36 | }
37 | }
38 | """
39 |
40 | val fetcher = MockServiceFetcher()
41 | fetcher.add(uri, expectValid {
42 | TestHelper.serviceValidatorFromApiJson(common)
43 | })
44 |
45 | expectValid {
46 | TestHelper.serviceValidatorFromApiJson(user, fetcher = fetcher)
47 | }.resources.head.operations.map(_.path) should be(Seq("/users"))
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/core/test/resources/avro/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "base_url": "http://localhost:9000",
3 | "name": "API Builder",
4 | "enums": {
5 | "gender": {
6 | "values": [
7 | { "name": "male" },
8 | { "name": "female" },
9 | { "name": "other" },
10 | { "name": "unknown" }
11 | ]
12 | }
13 | },
14 | "models": {
15 | "user": {
16 | "fields": [
17 | { "name": "uuid", "type": "uuid" },
18 | { "name": "first_name", "type": "string" },
19 | { "name": "age", "type": "integer", "required": false },
20 | { "name": "gender", "type": "gender" },
21 | { "name": "interests", "type": "[string]" },
22 | { "name": "has_purchased", "type": "boolean" },
23 | { "name": "num_visits", "type": "long" },
24 | { "name": "last_visit", "type": "date-time-iso8601"},
25 | { "name": "loyalty_score", "type": "double", "default": "0.0"},
26 | { "name": "account_balance", "type": "decimal", "default": "0.0"},
27 | { "name": "last_purchase", "type": "purchase", "required": false },
28 | { "name": "referring_user_uuid", "type": "uuid", "required": false },
29 | { "name": "adhoc", "type": "map"}
30 | ]
31 | },
32 | "purchase": {
33 | "fields": [
34 | { "name": "product", "type": "string" },
35 | { "name": "price", "type": "decimal" }
36 | ]
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/api/app/models/GeneratorWithServiceModel.scala:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import db.Authorization
4 | import db.generators.{InternalGenerator, InternalGeneratorServicesDao}
5 | import io.apibuilder.api.v0.models.GeneratorWithService
6 | import io.apibuilder.generator.v0.models.Generator
7 |
8 | import javax.inject.Inject
9 |
10 | class GeneratorWithServiceModel @Inject()(
11 | servicesDao: InternalGeneratorServicesDao,
12 | servicesModel: GeneratorServicesModel
13 | ) {
14 | def toModel(generator: InternalGenerator): Option[GeneratorWithService] = {
15 | toModels(Seq(generator)).headOption
16 | }
17 |
18 | def toModels(generators: Seq[InternalGenerator]): Seq[GeneratorWithService] = {
19 | val services = servicesDao.findAll(
20 | guids = Some(generators.map(_.serviceGuid).distinct),
21 | limit = None,
22 | ).map { s => s.guid -> servicesModel.toModel(s) }.toMap
23 |
24 | generators.flatMap { g =>
25 | services.get(g.serviceGuid).map { s =>
26 | GeneratorWithService(
27 | s,
28 | Generator(
29 | key = g.key,
30 | name = g.name,
31 | language = g.language,
32 | description = g.description,
33 | attributes = g.attributes
34 | )
35 | )
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/api/test/util/BasicAuthorizationSpec.scala:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import org.apache.commons.codec.binary.Base64
4 | import org.scalatestplus.play.PlaySpec
5 | import org.scalatestplus.play.guice.GuiceOneAppPerSuite
6 |
7 | class BasicAuthorizationSpec extends PlaySpec with GuiceOneAppPerSuite {
8 |
9 | def encode(v: String):String = {
10 | new String(Base64.encodeBase64(v.getBytes))
11 | }
12 |
13 | "parses a valid token" in {
14 | BasicAuthorization.get("Basic " + encode("abc")) must be(Some(BasicAuthorization.Token("abc")))
15 | }
16 |
17 | "parses a valid optional token" in {
18 | BasicAuthorization.get(Some("Basic " + encode("abc"))) must be(Some(BasicAuthorization.Token("abc")))
19 | }
20 |
21 | "parses a valid token with a colon" in {
22 | BasicAuthorization.get("Basic " + encode("abc:")) must be(Some(BasicAuthorization.Token("abc")))
23 | }
24 |
25 | "parses username and password" in {
26 | BasicAuthorization.get("Basic " + encode("user:pass")) must be(Some(BasicAuthorization.User("user", "pass")))
27 | }
28 |
29 | "Ignores non basic auth" in {
30 | BasicAuthorization.get("Other " + encode("user:pass")) must be(None)
31 | }
32 |
33 | "Ignores invalid auth" in {
34 | BasicAuthorization.get("Bøasic " + encode("user:pass:other")) must be(None)
35 | }
36 |
37 | "Ignores empty string" in {
38 | BasicAuthorization.get("Basic " + encode("")) must be(None)
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/core/test/core/ImportServiceApiJsonSpec.scala:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import helpers.ValidatedTestHelpers
4 | import org.scalatest.funspec.AnyFunSpec
5 | import org.scalatest.matchers.should.Matchers
6 |
7 | class ImportServiceApiJsonSpec extends AnyFunSpec with Matchers with helpers.ApiJsonHelpers with ValidatedTestHelpers {
8 |
9 | describe("validation") {
10 |
11 | it("import uri is present") {
12 | val json =
13 | """{
14 | "name": "Import Shared",
15 | "apidoc": { "version": "0.9.6" },
16 | "info": {},
17 | "imports": [ { "foo": "bar" } ]
18 | }"""
19 | expectInvalid {
20 | TestHelper.serviceValidatorFromApiJson(json)
21 | }.mkString(",") should be("Import Unrecognized element[foo],Import Missing uri")
22 | }
23 |
24 | it("import uri cannot be empty") {
25 | def testUri(uri: String) = {
26 | expectInvalid {
27 | TestHelper.serviceValidator(
28 | makeApiJson(
29 | imports = Seq(makeImport(uri = uri))
30 | )
31 | )
32 | }
33 | }
34 |
35 | testUri(" ") should be(Seq("Import uri must be a non empty string"))
36 | testUri("foobar") should be(Seq("URI[foobar] must start with http://, https://, or file://"))
37 | testUri("https://app.apibuilder.io/") should be(Seq("URI[https://app.apibuilder.io/] cannot end with a '/'"))
38 | }
39 |
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/app/lib/MemberDownload.scala:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import com.github.tototoshi.csv.{CSVFormat, CSVWriter}
4 | import com.github.tototoshi.csv.defaultCSVFormat
5 |
6 | import java.io.File
7 | import io.apibuilder.api.v0.models.Membership
8 |
9 | import scala.concurrent.{Await, ExecutionContext, Future}
10 | import scala.concurrent.duration.*
11 |
12 | case class MemberDownload(
13 | client: io.apibuilder.api.v0.Client,
14 | orgKey: String
15 | ) {
16 |
17 | def csv()(implicit ec: ExecutionContext): Future[File] = Future {
18 | val file = File.createTempFile(s"member-download-$orgKey", "csv")
19 |
20 | val writer = CSVWriter.open(file)(using defaultCSVFormat)
21 | writer.writeRow(Seq("guid", "role", "user_guid", "user_email", "user_nickname", "user_name"))
22 |
23 | Pager.eachPage[Membership] { offset =>
24 | Await.result(
25 | client.memberships.get(
26 | orgKey = Some(orgKey),
27 | limit = 250,
28 | offset = offset
29 | ),
30 | 5000.millis
31 | )
32 | } { membership =>
33 | writer.writeRow(
34 | Seq(
35 | membership.guid,
36 | membership.role,
37 | membership.user.guid,
38 | membership.user.email,
39 | membership.user.nickname,
40 | membership.user.name.getOrElse("")
41 | )
42 | )
43 | }
44 |
45 | writer.close()
46 |
47 | file
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/api/app/models/MembershipsModel.scala:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import cats.implicits.*
4 | import db.{Authorization, InternalMembership, InternalUsersDao}
5 | import io.apibuilder.api.v0.models.Membership
6 | import io.apibuilder.common.v0.models.{Audit, ReferenceGuid}
7 |
8 | import javax.inject.Inject
9 |
10 | class MembershipsModel @Inject()(
11 | usersModel: UsersModel,
12 | orgModel: OrganizationsModel
13 | ) {
14 |
15 | def toModel(mr: InternalMembership): Option[Membership] = {
16 | toModels(Seq(mr)).headOption
17 | }
18 |
19 | def toModels(memberships: Seq[InternalMembership]): Seq[Membership] = {
20 | val users = usersModel.toModelByGuids(memberships.map(_.userGuid)).map { u => u.guid -> u }.toMap
21 |
22 | val orgs = orgModel.toModelByGuids(Authorization.All, memberships.map(_.organizationGuid))
23 | .map { o => o.guid -> o }.toMap
24 |
25 | memberships.flatMap { m =>
26 | (users.get(m.userGuid), orgs.get(m.organizationGuid)).mapN { case (user, org) =>
27 | Membership(
28 | guid = m.guid,
29 | user = user,
30 | organization = org,
31 | role = m.role,
32 | audit = Audit(
33 | createdAt = m.db.createdAt,
34 | createdBy = ReferenceGuid(m.db.createdByGuid),
35 | updatedAt = m.db.createdAt,
36 | updatedBy = ReferenceGuid(m.db.createdByGuid),
37 | )
38 | )
39 | }
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/api/test/controllers/ApplicationMetadataSpec.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import org.scalatestplus.play.guice.GuiceOneServerPerSuite
4 | import org.scalatestplus.play.PlaySpec
5 | import play.api.libs.ws.WSClient
6 |
7 | class ApplicationMetadataSpec extends PlaySpec with MockClient with GuiceOneServerPerSuite {
8 |
9 | import scala.concurrent.ExecutionContext.Implicits.global
10 |
11 | private lazy val org = createOrganization()
12 | private lazy val application = {
13 | val a = createApplication(org)
14 | createVersion(a, version = "1.0.0")
15 | createVersion(a, version = "2.0.0")
16 | a
17 | }
18 |
19 | "GET /:orgKey/:applicationKey/metadata/versions" in {
20 | await(
21 | client.applications.getMetadataAndVersionsByApplicationKey(org.key, application.key)
22 | ).map(_.version) must equal(
23 | Seq("2.0.0", "1.0.0")
24 | )
25 | }
26 |
27 | "GET /:orgKey/:applicationKey/metadata/versions/latest.txt" in {
28 | val ws = app.injector.instanceOf[WSClient]
29 | val auth = sessionHelper.createAuthentication(testUser)
30 |
31 | val result = await(
32 | ws.url(
33 | s"http://localhost:$port/${org.key}/metadata/${application.key}/versions/latest.txt"
34 | ).addHttpHeaders(
35 | "Authorization" -> s"Session ${auth.session.id}"
36 | ).get()
37 | )
38 | result.status must equal(200)
39 | result.bodyAsBytes.utf8String must equal("2.0.0")
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/api/app/play/LoggingFilter.scala:
--------------------------------------------------------------------------------
1 | package io.apicollective.play
2 |
3 | import org.apache.pekko.stream.Materializer
4 | import play.api.{Logger, Logging}
5 | import play.api.mvc.*
6 |
7 | import scala.concurrent.{ExecutionContext, Future}
8 | import play.api.http.HttpFilters
9 |
10 | /**
11 | * Add this to your base.conf:
12 | * play.http.filters=io.apicollective.play.LoggingFilter
13 | **/
14 | class LoggingFilter @javax.inject.Inject() (loggingFilter: ApibuilderLoggingFilter) extends HttpFilters {
15 | def filters = Seq(loggingFilter)
16 | }
17 |
18 | class ApibuilderLoggingFilter @javax.inject.Inject() (
19 | implicit ec: ExecutionContext,
20 | m: Materializer
21 | ) extends Filter with Logging {
22 |
23 | def apply(f: RequestHeader => Future[Result])(requestHeader: RequestHeader): Future[Result] = {
24 | val startTime = System.currentTimeMillis
25 | f(requestHeader).map { result =>
26 | val endTime = System.currentTimeMillis
27 | val requestTime = endTime - startTime
28 | val headerMap = requestHeader.headers.toMap
29 | val line = Seq(
30 | requestHeader.method,
31 | s"${requestHeader.host}${requestHeader.uri}",
32 | result.header.status,
33 | s"${requestTime}ms",
34 | headerMap.getOrElse("User-Agent", Nil).mkString(",")
35 | ).mkString(" ")
36 |
37 | logger.info(line)
38 | result
39 | }
40 | }
41 |
42 | override implicit def mat: Materializer = m
43 | }
44 |
--------------------------------------------------------------------------------