├── .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 | @label 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 | [![Build Status](https://travis-ci.org/apicollective/apibuilder.svg?branch=main)](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 | 8 | 9 | } 10 | 11 |
    @imp.uri
    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 |
    8 | Edit 9 |
    10 | 11 | 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 | 18 | 19 |

    20 | The following invariants were all valid 21 |

    22 | 23 | 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 | 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 | 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 | 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 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
    TypeDescription
    @datatype(org, app, version, service, body.`type`)@Html(lib.Markdown.toHtml(body.description.getOrElse("N/A")))@body.deprecation.map(deprecation(_))
    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 | 21 | 22 | 23 | 24 | } 25 | 26 | 27 |
    @example.labelOnline docsapi.json
    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 | 10 | 11 | 12 | 13 | 14 | 15 | @e.deprecation.map { depr => 16 | 17 | 18 | 19 | } 20 | @e.values.map { value => 21 | 22 | 23 | 24 | 25 | 26 | } 27 | 28 |
    NameValueDescription
    @depr
    @value.name@value.value.getOrElse(value.name)@Html(lib.Markdown(value.description))
    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 | 11 | 12 | 13 | 14 | 18 | 19 | } 20 | 21 |
    @request.org.name@request.user.name.getOrElse("")@request.user.email@request.role 15 | Approve | 16 | Decline 17 |
    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 | 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 |
    10 |
    11 |
    12 | 17 |
    18 | 19 |
    20 | 25 |
    26 |
    27 |
    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 | 23 | 24 | 25 | } 26 | 27 |
    15 | @if(up.isSubscribed) { 16 | Subscribed 17 | } else { 18 | Not subscribed 19 | } 20 |
    21 | (Toggle) 22 |
    @up.label
    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 |
    14 | Add domain 15 |
    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 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | @headers.map { header => 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | } 28 | 29 |
    NameTypeRequired?DefaultDescription
    @header.name@header.deprecation.map(deprecation(_))@datatype(org, app, version, service, header.`type`)@if(header.required) { Yes } else { No }@header.default.getOrElse("-")@Html(lib.Markdown(header.description))
    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 |
    10 | Add generator 11 |
    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 |
    20 | 21 | Cancel 22 |
    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 |
    11 | Edit 12 |
    13 | } 14 | 15 | 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 | 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 | 16 | } 17 | 18 | @helper.inputText( 19 | form("name"), 20 | Symbol("_label") -> "Domain name", 21 | Symbol("_error") -> form.error("name") 22 | ) 23 | 24 |
    25 | 26 |
    27 | 28 | Cancel 29 |
    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 | 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 | 14 | 15 | 16 | 17 | } 18 | @otherAttributes.map { attr => 19 | 20 | 21 | 22 | 23 | 24 | } 25 | 26 |
    edit@value.attribute.name@value.value
    Edit@attr.name-
    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 | 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 | 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 |
    12 | 13 | Download (.zip) 14 | | Download (.tar.gz) 15 | 16 |
    17 | 18 | 19 | 20 | @files.map { file => 21 | 22 | 23 | 24 | } 25 | 26 |
    @file.name (@{lib.Bytes.label(file.contents.getBytes.length)})
    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 | 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 | 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 | 22 | 23 | } 24 | 25 |
    19 | @item.label 20 |
    @item.description 21 |
    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 |
    10 | Add attribute 11 |
    12 | } 13 | 14 | 15 | 16 | @attributes.items.map { attribute => 17 | 18 | 19 | 20 | 21 | } 22 | 23 |
    @attribute.name@lib.Text.truncate(attribute.description.getOrElse(""))
    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 | 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 |

    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 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | @operation.parameters.map { param => 21 | 22 | 23 | 24 | 25 | 26 | 27 | 39 | 40 | } 41 | 42 |
    NameTypeLocationRequired?DefaultDescription
    @param.name@datatype(org, app, version, service, param.`type`)@param.location@if(param.required) { Yes } else { No }@param.default.getOrElse("-")@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 |
    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 |
    11 | Delete 12 |
    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 | 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 | 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 | 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 |
    29 | 30 | Cancel 31 |
    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 | 16 | } 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | @generatorWithServices.items.map { gws => 25 | 26 | @if(displayService) { 27 | 28 | } 29 | 30 | 31 | 32 | 35 | 36 | 37 | } 38 | 39 |
    ServiceKeyNameLanguageAttributesDescription
    @lib.Text.truncate(gws.service.uri)@gws.generator.key@gws.generator.name@gws.generator.language.getOrElse("")@gws.generator.attributes.map { attr => 33 | @attr 34 | }@Html(lib.Markdown(gws.generator.description.map(lib.Text.truncate(_))))
    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 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | @union.types.map { t => 18 | 19 | 20 | 25 | 28 | 33 | 34 | } 35 | 36 |
    TypeDiscriminator ValueExample JsonDescription
    @datatype(org, app, version, service, t.`type`)@t.discriminatorValue.getOrElse(t.`type`) 21 | @if(t.default.getOrElse(false)) { 22 | (Default) 23 | } 24 | Minimal | 26 | Full 27 | @Html(lib.Markdown(t.description)) 29 |

    30 | @t.deprecation.map(deprecation(_)) 31 |

    32 |
    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 | --------------------------------------------------------------------------------