├── CODEOWNERS ├── backend ├── project │ ├── build.properties │ ├── ProjectKeys.scala │ ├── Versions.scala │ └── plugins.sbt ├── .sbtopts ├── mockingbird-native │ └── src │ │ ├── main │ │ └── resources │ │ │ └── META-INF │ │ │ └── native-image │ │ │ └── ru.tinkoff.tcb │ │ │ └── mockingbird │ │ │ ├── proxy-config.json │ │ │ ├── predefined-classes-config.json │ │ │ └── serialization-config.json │ │ └── universal │ │ ├── protoc-25.2-linux-x86_64.zip │ │ └── qa.conf ├── mockingbird │ └── src │ │ ├── main │ │ ├── scala │ │ │ ├── ru │ │ │ │ └── tinkoff │ │ │ │ │ └── tcb │ │ │ │ │ ├── validation │ │ │ │ │ └── package.scala │ │ │ │ │ ├── mockingbird │ │ │ │ │ ├── error │ │ │ │ │ │ ├── StateSearchError.scala │ │ │ │ │ │ ├── CompoundError.scala │ │ │ │ │ │ ├── SourceFault.scala │ │ │ │ │ │ ├── CallbackError.scala │ │ │ │ │ │ ├── StubSearchError.scala │ │ │ │ │ │ ├── ScenarioExecError.scala │ │ │ │ │ │ ├── ScenarioSearchError.scala │ │ │ │ │ │ ├── EventProcessingError.scala │ │ │ │ │ │ ├── ResourceManagementError.scala │ │ │ │ │ │ ├── DuplicationError.scala │ │ │ │ │ │ ├── EarlyReturn.scala │ │ │ │ │ │ ├── ValidationError.scala │ │ │ │ │ │ └── SpawnError.scala │ │ │ │ │ ├── stream │ │ │ │ │ │ └── package.scala │ │ │ │ │ ├── config │ │ │ │ │ │ └── PureconfigEnum.scala │ │ │ │ │ ├── api │ │ │ │ │ │ ├── request │ │ │ │ │ │ │ ├── XPathTestRequest.scala │ │ │ │ │ │ │ ├── ScenarioResolveRequest.scala │ │ │ │ │ │ │ ├── SearchRequest.scala │ │ │ │ │ │ │ ├── CreateServiceRequest.scala │ │ │ │ │ │ │ ├── UpdateDestinationConfigurationRequest.scala │ │ │ │ │ │ │ ├── CreateDestinationConfigurationRequest.scala │ │ │ │ │ │ │ ├── UpdateSourceConfigurationRequest.scala │ │ │ │ │ │ │ ├── CreateSourceConfigurationRequest.scala │ │ │ │ │ │ │ └── CreateGrpcMethodDescriptionRequest.scala │ │ │ │ │ │ ├── response │ │ │ │ │ │ │ ├── OperationResult.scala │ │ │ │ │ │ │ ├── SourceDTO.scala │ │ │ │ │ │ │ └── DestinationDTO.scala │ │ │ │ │ │ └── package.scala │ │ │ │ │ ├── model │ │ │ │ │ │ ├── Label.scala │ │ │ │ │ │ ├── Service.scala │ │ │ │ │ │ ├── CallbackResponseMode.scala │ │ │ │ │ │ ├── ResourceRequest.scala │ │ │ │ │ │ ├── HttpMethod.scala │ │ │ │ │ │ ├── GrpcConnectionType.scala │ │ │ │ │ │ ├── PersistentState.scala │ │ │ │ │ │ ├── EventDestinationRequest.scala │ │ │ │ │ │ ├── Scope.scala │ │ │ │ │ │ ├── EventSourceRequest.scala │ │ │ │ │ │ ├── DestinationConfiguration.scala │ │ │ │ │ │ ├── SourceConfiguration.scala │ │ │ │ │ │ ├── RequestBody.scala │ │ │ │ │ │ └── CallbackChecker.scala │ │ │ │ │ ├── grpc │ │ │ │ │ │ ├── UniversalHandlerRegistry.scala │ │ │ │ │ │ └── Method.scala │ │ │ │ │ ├── misc │ │ │ │ │ │ └── Substitute.scala │ │ │ │ │ └── dal │ │ │ │ │ │ ├── ServiceDAO.scala │ │ │ │ │ │ ├── GrpcStubDAO.scala │ │ │ │ │ │ ├── ScenarioDAO.scala │ │ │ │ │ │ ├── GrpcMethodDescriptionDAO.scala │ │ │ │ │ │ └── HttpStubDAO.scala │ │ │ │ │ ├── utils │ │ │ │ │ ├── controlflow │ │ │ │ │ │ └── package.scala │ │ │ │ │ ├── sandboxing │ │ │ │ │ │ └── CodeRunner.scala │ │ │ │ │ ├── regex │ │ │ │ │ │ ├── OneOrMore.scala │ │ │ │ │ │ └── package.scala │ │ │ │ │ ├── transformation │ │ │ │ │ │ ├── package.scala │ │ │ │ │ │ ├── xml │ │ │ │ │ │ │ └── XCData.scala │ │ │ │ │ │ └── string │ │ │ │ │ │ │ └── package.scala │ │ │ │ │ ├── time │ │ │ │ │ │ └── package.scala │ │ │ │ │ ├── xttp │ │ │ │ │ │ ├── xml │ │ │ │ │ │ │ └── package.scala │ │ │ │ │ │ └── package.scala │ │ │ │ │ ├── webform │ │ │ │ │ │ └── package.scala │ │ │ │ │ ├── refinedchimney │ │ │ │ │ │ └── package.scala │ │ │ │ │ ├── json │ │ │ │ │ │ └── package.scala │ │ │ │ │ ├── id │ │ │ │ │ │ ├── package.scala │ │ │ │ │ │ └── IDCompanion.scala │ │ │ │ │ └── xml │ │ │ │ │ │ └── package.scala │ │ │ │ │ ├── protocol │ │ │ │ │ ├── fields.scala │ │ │ │ │ ├── rof.scala │ │ │ │ │ ├── log.scala │ │ │ │ │ └── bson.scala │ │ │ │ │ ├── xpath │ │ │ │ │ └── package.scala │ │ │ │ │ ├── instances │ │ │ │ │ ├── jsonNumber.scala │ │ │ │ │ └── predicate │ │ │ │ │ │ └── and.scala │ │ │ │ │ ├── logging │ │ │ │ │ ├── LogContext.scala │ │ │ │ │ └── Mdc.scala │ │ │ │ │ └── predicatedsl │ │ │ │ │ └── PredicateConstructionError.scala │ │ │ └── tofu │ │ │ │ └── logging │ │ │ │ └── impl │ │ │ │ ├── ZUniversalLogging.scala │ │ │ │ └── ZUniversalContextLogging.scala │ │ └── resources │ │ │ ├── qa.conf │ │ │ ├── reference.conf │ │ │ └── application.conf │ │ └── test │ │ ├── resources │ │ ├── not_optional_proto3.proto │ │ ├── optional_proto3.proto │ │ ├── not_optional_proto2.proto │ │ ├── optional_proto2.proto │ │ ├── nested.proto │ │ └── requests.proto │ │ └── scala │ │ └── ru │ │ └── tinkoff │ │ └── tcb │ │ ├── xpath │ │ └── SXpathSpec.scala │ │ ├── utils │ │ └── transformation │ │ │ └── string │ │ │ └── StringTransformationsSpec.scala │ │ └── protobuf │ │ ├── ProtoToDescriptorSpec.scala │ │ └── Utils.scala ├── mockingbird-api │ └── src │ │ ├── universal │ │ └── protoc-25.2-linux-x86_64.zip │ │ └── main │ │ ├── resources │ │ └── logback.xml │ │ └── scala │ │ └── ru │ │ └── tinkoff │ │ └── tcb │ │ └── mockingbird │ │ ├── package.scala │ │ └── api │ │ ├── MetricsHttp.scala │ │ └── input │ │ └── package.scala ├── dataAccess │ └── src │ │ ├── main │ │ └── scala │ │ │ └── ru │ │ │ └── tinkoff │ │ │ └── tcb │ │ │ ├── bson │ │ │ ├── optics │ │ │ │ └── package.scala │ │ │ ├── enumeratum │ │ │ │ ├── BsonEnum.scala │ │ │ │ └── values │ │ │ │ │ └── EnumHandler.scala │ │ │ └── PatchGenerator.scala │ │ │ ├── generic │ │ │ └── Identifiable.scala │ │ │ └── dataaccess │ │ │ ├── UpdateResult.scala │ │ │ └── DAO.scala │ │ └── test │ │ └── scala │ │ └── ru │ │ └── tinkoff │ │ └── tcb │ │ ├── bson │ │ ├── enumeratum │ │ │ ├── Dummy.scala │ │ │ ├── values │ │ │ │ └── EnumBsonHandlerSpec.scala │ │ │ └── BsonEnumSpec.scala │ │ ├── RoundRobinSpec.scala │ │ └── PatchGeneratorSpec.scala │ │ └── generic │ │ ├── RootOptionFieldsSpec.scala │ │ └── FieldsSpec.scala ├── edsl │ └── src │ │ ├── main │ │ └── scala │ │ │ └── ru │ │ │ └── tinkoff │ │ │ └── tcb │ │ │ └── mockingbird │ │ │ └── edsl │ │ │ ├── model │ │ │ ├── package.scala │ │ │ ├── ExampleDescription.scala │ │ │ ├── Step.scala │ │ │ └── http.scala │ │ │ └── interpreter │ │ │ └── package.scala │ │ └── test │ │ └── scala │ │ └── ru │ │ └── tinkoff │ │ └── tcb │ │ └── mockingbird │ │ └── examples │ │ └── CatsFacts.scala ├── utils │ └── src │ │ ├── main │ │ └── scala │ │ │ └── ru │ │ │ └── tinkoff │ │ │ └── tcb │ │ │ └── utils │ │ │ ├── unpack │ │ │ └── package.scala │ │ │ ├── resource │ │ │ ├── package.scala │ │ │ └── Resource.scala │ │ │ ├── any │ │ │ └── package.scala │ │ │ ├── map │ │ │ └── package.scala │ │ │ ├── instances │ │ │ └── predicate │ │ │ │ ├── and.scala │ │ │ │ └── or.scala │ │ │ ├── lazy │ │ │ └── Lazy.scala │ │ │ ├── base64 │ │ │ └── package.scala │ │ │ └── string │ │ │ └── package.scala │ │ └── test │ │ └── scala │ │ └── ru │ │ └── tinkoff │ │ └── tcb │ │ └── utils │ │ ├── crypto │ │ └── SyncAESSpec.scala │ │ ├── lazy │ │ └── LazySpec.scala │ │ └── ExtStringOpsSpec.scala ├── examples │ └── src │ │ └── test │ │ ├── scala │ │ └── ru │ │ │ └── tinkoff │ │ │ └── tcb │ │ │ └── mockingbird │ │ │ └── examples │ │ │ ├── BasicHttpStubSuite.scala │ │ │ └── HttpStubWithStateSuite.scala │ │ └── resources │ │ └── logback.xml ├── secrets-for-test.conf ├── circe-utils │ └── src │ │ ├── main │ │ └── scala │ │ │ └── ru │ │ │ └── tinkoff │ │ │ └── tcb │ │ │ └── utils │ │ │ └── circe │ │ │ └── optics │ │ │ └── PathPart.scala │ │ └── test │ │ └── scala │ │ └── ru │ │ └── tinkoff │ │ └── tcb │ │ └── utils │ │ └── circe │ │ ├── JsonOpsTest.scala │ │ └── MergerSpec.scala ├── integrationTests │ └── k6 │ │ ├── howToTest.md │ │ ├── v4 │ │ ├── scope │ │ │ └── definitions │ │ │ │ └── test_service.proto │ │ ├── responseMode │ │ │ └── definitions │ │ │ │ └── test_service.proto │ │ └── contentType │ │ │ └── definitions │ │ │ └── test_service.proto │ │ └── v2 │ │ └── definitions │ │ └── test_service.proto ├── compose-test.yml.native.tmpl ├── compose-test.yml.jvm.tmpl ├── .scalafix.conf └── .scalafmt.conf ├── frontend ├── .eslintignore ├── .prettierrc.js ├── src │ ├── components │ │ ├── List │ │ │ ├── List.css │ │ │ ├── List.tsx │ │ │ ├── ListEmpty.tsx │ │ │ ├── ListLoading.tsx │ │ │ └── ListError.tsx │ │ ├── Copy │ │ │ └── Copy.css │ │ └── PageHeader │ │ │ └── PageHeader.tsx │ ├── mockingbird │ │ ├── modules │ │ │ ├── labels │ │ │ │ ├── index.ts │ │ │ │ ├── hooks │ │ │ │ │ └── index.ts │ │ │ │ ├── actions │ │ │ │ │ └── index.ts │ │ │ │ └── reducers │ │ │ │ │ └── index.ts │ │ │ ├── service │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ ├── hooks │ │ │ │ │ └── index.ts │ │ │ │ ├── actions │ │ │ │ │ └── fetchAction.ts │ │ │ │ └── reducers │ │ │ │ │ └── store.ts │ │ │ ├── services │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ ├── ui │ │ │ │ │ └── ServiceItem.css │ │ │ │ ├── actions │ │ │ │ │ ├── fetchAction.ts │ │ │ │ │ └── createAction.ts │ │ │ │ └── reducers │ │ │ │ │ ├── store.ts │ │ │ │ │ └── createStore.ts │ │ │ ├── sources │ │ │ │ ├── types.ts │ │ │ │ ├── index.ts │ │ │ │ ├── ui │ │ │ │ │ ├── Sources.tsx │ │ │ │ │ └── SourceItem.tsx │ │ │ │ ├── hooks │ │ │ │ │ └── index.ts │ │ │ │ ├── actions │ │ │ │ │ └── index.ts │ │ │ │ └── reducers │ │ │ │ │ └── index.ts │ │ │ ├── destinations │ │ │ │ ├── types.ts │ │ │ │ ├── index.ts │ │ │ │ ├── ui │ │ │ │ │ ├── Destinations.tsx │ │ │ │ │ └── DestinationItem.tsx │ │ │ │ ├── hooks │ │ │ │ │ └── index.ts │ │ │ │ ├── actions │ │ │ │ │ └── index.ts │ │ │ │ └── reducers │ │ │ │ │ └── index.ts │ │ │ ├── destination │ │ │ │ ├── refs.ts │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ ├── utils.ts │ │ │ │ └── ui │ │ │ │ │ └── PageDestinationNew.tsx │ │ │ └── source │ │ │ │ ├── index.ts │ │ │ │ ├── refs.ts │ │ │ │ ├── types.ts │ │ │ │ ├── utils.ts │ │ │ │ └── ui │ │ │ │ └── PageSourceNew.tsx │ │ ├── layers │ │ │ ├── layout │ │ │ │ ├── Layout.css │ │ │ │ ├── Shadow.css │ │ │ │ ├── Shadow.tsx │ │ │ │ ├── Layout.tsx │ │ │ │ └── Header.tsx │ │ │ └── pages │ │ │ │ ├── mock │ │ │ │ ├── Callbacks.css │ │ │ │ ├── JSONRequest.css │ │ │ │ ├── GrpcNew.tsx │ │ │ │ ├── HttpNew.tsx │ │ │ │ ├── types.ts │ │ │ │ └── ScenarioNew.tsx │ │ │ │ ├── common.css │ │ │ │ ├── mocks │ │ │ │ ├── Item.css │ │ │ │ ├── GRPCItem.tsx │ │ │ │ ├── HTTPMock.tsx │ │ │ │ └── ScenarioItem.tsx │ │ │ │ └── NotFound.tsx │ │ ├── components │ │ │ ├── Page │ │ │ │ ├── Page.css │ │ │ │ └── index.tsx │ │ │ ├── Language │ │ │ │ ├── index.ts │ │ │ │ └── LanguagePicker.css │ │ │ └── form │ │ │ │ ├── Select │ │ │ │ └── index.tsx │ │ │ │ ├── Input │ │ │ │ └── index.tsx │ │ │ │ ├── ToggleBlock │ │ │ │ └── index.tsx │ │ │ │ ├── InputCount │ │ │ │ └── index.tsx │ │ │ │ └── InputJson │ │ │ │ └── index.tsx │ │ ├── postcss.js │ │ ├── main.css │ │ ├── reset.css │ │ ├── settings.ts │ │ ├── infrastructure │ │ │ ├── helpers │ │ │ │ ├── state │ │ │ │ │ └── index.ts │ │ │ │ └── forms │ │ │ │ │ └── index.ts │ │ │ ├── utils │ │ │ │ └── forms │ │ │ │ │ └── index.ts │ │ │ ├── request │ │ │ │ └── index.ts │ │ │ └── pluralize │ │ │ │ └── index.ts │ │ ├── i18n.ts │ │ ├── models │ │ │ ├── mockCreate │ │ │ │ └── reducers │ │ │ │ │ └── store.ts │ │ │ └── mock │ │ │ │ └── types.ts │ │ └── bundles │ │ │ └── mainDefault.ts │ ├── infrastructure │ │ ├── notifications │ │ │ ├── index.ts │ │ │ ├── store │ │ │ │ ├── actions.ts │ │ │ │ └── store.ts │ │ │ └── utils.ts │ │ ├── helpers │ │ │ ├── scroll │ │ │ │ └── index.ts │ │ │ └── copy-to-clipboard │ │ │ │ └── index.ts │ │ ├── utils │ │ │ ├── hooks │ │ │ │ └── debouce.ts │ │ │ └── lruCache.ts │ │ └── request │ │ │ └── index.ts │ └── vendor.ts ├── typings.d.ts ├── .tramvai-migrate-applied.json ├── .eslintrc ├── stylelint.config.js ├── Dockerfile ├── renovate.json ├── env.development.js ├── tramvai.json └── tsconfig.json ├── img └── mascot.png ├── confluent.md ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore └── docker-compose.yml /CODEOWNERS: -------------------------------------------------------------------------------- 1 | @danslapman @Metastasis 2 | -------------------------------------------------------------------------------- /backend/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.10.11 -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .fleet 4 | -------------------------------------------------------------------------------- /backend/.sbtopts: -------------------------------------------------------------------------------- 1 | -Dsbt.io.implicit.relative.glob.conversion=allow 2 | -------------------------------------------------------------------------------- /frontend/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('prettier-config-tinkoff'); 2 | -------------------------------------------------------------------------------- /frontend/src/components/List/List.css: -------------------------------------------------------------------------------- 1 | .root > * + * { 2 | margin-top: 12px; 3 | } 4 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/labels/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './hooks'; 2 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/service/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './hooks'; 2 | -------------------------------------------------------------------------------- /img/mascot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leviysoft/mockingbird/HEAD/img/mascot.png -------------------------------------------------------------------------------- /frontend/src/mockingbird/layers/layout/Layout.css: -------------------------------------------------------------------------------- 1 | .content { 2 | padding-bottom: 24px; 3 | } 4 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/services/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ui/Services'; 2 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/services/types.ts: -------------------------------------------------------------------------------- 1 | export { Service } from '../service/types'; 2 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/components/Page/Page.css: -------------------------------------------------------------------------------- 1 | .root > * + * { 2 | margin-top: 16px; 3 | } 4 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/components/Language/index.ts: -------------------------------------------------------------------------------- 1 | export { LanguagePicker } from './LanguagePicker'; 2 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/postcss.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('postcss-nested')], 3 | }; 4 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/layers/layout/Shadow.css: -------------------------------------------------------------------------------- 1 | .root { 2 | box-shadow: 0 4px 56px rgb(0 0 0 / 12%); 3 | } 4 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/sources/types.ts: -------------------------------------------------------------------------------- 1 | export { Source } from 'src/mockingbird/modules/source/types'; 2 | -------------------------------------------------------------------------------- /backend/mockingbird-native/src/main/resources/META-INF/native-image/ru.tinkoff.tcb/mockingbird/proxy-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | ] 3 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/service/types.ts: -------------------------------------------------------------------------------- 1 | export type Service = { 2 | name: string; 3 | suffix: string; 4 | }; 5 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/main.css: -------------------------------------------------------------------------------- 1 | @import './reset.css'; 2 | 3 | html, 4 | body { 5 | height: 100vh; 6 | margin: 0; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/destinations/types.ts: -------------------------------------------------------------------------------- 1 | export { Destination } from 'src/mockingbird/modules/destination/types'; 2 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/reset.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | border: 0; 5 | outline-width: 0; 6 | vertical-align: baseline; 7 | } -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/sources/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ui/Sources'; 2 | export { default as PageSources } from './ui/PageSources'; 3 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/destination/refs.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_REQUEST = { 2 | url: 'https://tinkoff.ru', 3 | method: 'POST', 4 | headers: {}, 5 | }; 6 | -------------------------------------------------------------------------------- /backend/project/ProjectKeys.scala: -------------------------------------------------------------------------------- 1 | import sbt.settingKey 2 | 3 | object ProjectKeys { 4 | val dockerize = settingKey[Boolean]("Build native image inside of docker") 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/destinations/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ui/Destinations'; 2 | export { default as PageDestinations } from './ui/PageDestinations'; 3 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/source/index.ts: -------------------------------------------------------------------------------- 1 | export { default as PageSource } from './ui/PageSource'; 2 | export { default as PageSourceNew } from './ui/PageSourceNew'; 3 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/validation/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb 2 | 3 | package object validation { 4 | type Rule[-T] = T => Vector[String] 5 | } 6 | -------------------------------------------------------------------------------- /backend/mockingbird-api/src/universal/protoc-25.2-linux-x86_64.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leviysoft/mockingbird/HEAD/backend/mockingbird-api/src/universal/protoc-25.2-linux-x86_64.zip -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/source/refs.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_REQUEST = { 2 | url: 'https://tinkoff.ru', 3 | method: 'POST', 4 | jstringdecode: true, 5 | headers: {}, 6 | }; 7 | -------------------------------------------------------------------------------- /backend/dataAccess/src/main/scala/ru/tinkoff/tcb/bson/optics/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.bson 2 | 3 | package object optics { 4 | val BLens: BsonOptic = BsonOptic(Seq.empty) 5 | } 6 | -------------------------------------------------------------------------------- /backend/mockingbird-native/src/universal/protoc-25.2-linux-x86_64.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leviysoft/mockingbird/HEAD/backend/mockingbird-native/src/universal/protoc-25.2-linux-x86_64.zip -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/error/StateSearchError.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.error 2 | 3 | final case class StateSearchError() extends Exception 4 | -------------------------------------------------------------------------------- /frontend/src/infrastructure/notifications/index.ts: -------------------------------------------------------------------------------- 1 | export { default as NotificationStack } from './ui/Stack'; 2 | export { addToastAction } from './store/actions'; 3 | export * from './utils'; 4 | -------------------------------------------------------------------------------- /frontend/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' { 2 | interface IClassNames { 3 | [className: string]: string; 4 | } 5 | const classNames: IClassNames; 6 | export = classNames; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/destination/index.ts: -------------------------------------------------------------------------------- 1 | export { default as PageDestination } from './ui/PageDestination'; 2 | export { default as PageDestinationNew } from './ui/PageDestinationNew'; 3 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/error/CompoundError.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.error 2 | 3 | final case class CompoundError(excs: List[Throwable]) extends Exception 4 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/error/SourceFault.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.error 2 | 3 | final case class SourceFault(message: String) extends Exception(message) 4 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/error/CallbackError.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.error 2 | 3 | final case class CallbackError(message: String) extends Exception(message) 4 | -------------------------------------------------------------------------------- /backend/mockingbird-native/src/main/resources/META-INF/native-image/ru.tinkoff.tcb/mockingbird/predefined-classes-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type":"agent-extracted", 4 | "classes":[ 5 | ] 6 | } 7 | ] 8 | 9 | -------------------------------------------------------------------------------- /backend/mockingbird-native/src/main/resources/META-INF/native-image/ru.tinkoff.tcb/mockingbird/serialization-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "types":[ 3 | ], 4 | "lambdaCapturingTypes":[ 5 | ], 6 | "proxies":[ 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/error/StubSearchError.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.error 2 | 3 | final case class StubSearchError(message: String) extends Exception(message) 4 | -------------------------------------------------------------------------------- /frontend/src/vendor.ts: -------------------------------------------------------------------------------- 1 | // meaningless mistake from a useful rule https://github.com/yannickcr/eslint-plugin-react/issues/2857 2 | // eslint-disable-next-line react/no-typos 3 | import 'react'; 4 | import 'react-dom'; 5 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/error/ScenarioExecError.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.error 2 | 3 | final case class ScenarioExecError(message: String) extends Exception(message) 4 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/error/ScenarioSearchError.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.error 2 | 3 | final case class ScenarioSearchError(message: String) extends Exception(message) 4 | -------------------------------------------------------------------------------- /backend/edsl/src/main/scala/ru/tinkoff/tcb/mockingbird/edsl/model/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.edsl 2 | 3 | import cats.free.Free 4 | 5 | package object model { 6 | type Example[T] = Free[Step, T] 7 | } 8 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/error/EventProcessingError.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.error 2 | 3 | final case class EventProcessingError(message: String) extends Exception(message) 4 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/error/ResourceManagementError.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.error 2 | 3 | final case class ResourceManagementError(message: String) extends Exception(message) 4 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/error/DuplicationError.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.error 2 | 3 | final case class DuplicationError(message: String, ids: Vector[String]) extends Exception(message) 4 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/error/EarlyReturn.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.error 2 | 3 | //TODO: replace all occurrences with selective functors 4 | case object EarlyReturn extends Exception 5 | -------------------------------------------------------------------------------- /backend/mockingbird/src/test/resources/not_optional_proto3.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | enum Bar { 4 | BAR_ZERO = 0; 5 | BAR_ONE = 1; 6 | } 7 | 8 | message Foo { 9 | string field1 = 1; 10 | Bar field2 = 3; 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/components/Copy/Copy.css: -------------------------------------------------------------------------------- 1 | .copyButton { 2 | display: block; 3 | padding: 0; 4 | border: none; 5 | background: none; 6 | outline: none; 7 | cursor: pointer; 8 | width: 24px; 9 | height: 24px; 10 | } 11 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/stream/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird 2 | 3 | import cron4s.Cron 4 | 5 | package object stream { 6 | final val midnight = Cron.unsafeParse("0 0 0 ? * *") 7 | } 8 | -------------------------------------------------------------------------------- /backend/mockingbird/src/test/resources/optional_proto3.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | enum Bar { 4 | BAR_ZERO = 0; 5 | BAR_ONE = 1; 6 | } 7 | 8 | message Foo { 9 | string field1 = 1; 10 | optional Bar field2 = 3; 11 | } 12 | -------------------------------------------------------------------------------- /frontend/.tramvai-migrate-applied.json: -------------------------------------------------------------------------------- 1 | {"package":{"module-render":{"migrations":["2-async-universal-to-lazy.js"]},"module-router":{"migrations":["1-link-from-module.js"]},"cli":{"migrations":["d2023-02-01b-tramvai-config-refactoring.js"]}}} 2 | -------------------------------------------------------------------------------- /backend/mockingbird/src/test/resources/not_optional_proto2.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | enum Bar { 4 | BAR_ZERO = 0; 5 | BAR_ONE = 1; 6 | } 7 | 8 | message Foo { 9 | required string field1 = 1; 10 | required Bar field2 = 3; 11 | } 12 | -------------------------------------------------------------------------------- /backend/mockingbird/src/test/resources/optional_proto2.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | enum Bar { 4 | BAR_ZERO = 0; 5 | BAR_ONE = 1; 6 | } 7 | 8 | message Foo { 9 | required string field1 = 1; 10 | optional Bar field2 = 3; 11 | } 12 | -------------------------------------------------------------------------------- /backend/utils/src/main/scala/ru/tinkoff/tcb/utils/unpack/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils 2 | 3 | package object unpack { 4 | object <*> { 5 | def unapply[A, B](ab: (A, B)): Some[(A, B)] = 6 | Some((ab._1, ab._2)) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/error/ValidationError.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.error 2 | 3 | final case class ValidationError(fails: Vector[String]) extends Exception(s"Validation error: ${fails.mkString(",")}") 4 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/controlflow/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils 2 | 3 | package object controlflow { 4 | @inline def partial[T](f: => PartialFunction[T, T]): T => T = t => f.applyOrElse(t, identity[T]) 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/layers/pages/mock/Callbacks.css: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | align-items: center; 4 | } 5 | 6 | .header > * + * { 7 | margin-left: 16px; 8 | } 9 | 10 | .items, 11 | .items > * + * { 12 | margin-top: 8px; 13 | } 14 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/error/SpawnError.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.error 2 | 3 | import ru.tinkoff.tcb.utils.id.SID 4 | 5 | final case class SpawnError[E](source: SID[E], cause: Throwable) extends Exception(cause) 6 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/CodeRunner.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils.sandboxing 2 | 3 | import scala.util.Try 4 | 5 | import io.circe.Json 6 | 7 | trait CodeRunner { 8 | def eval(code: String): Try[Json] 9 | } 10 | -------------------------------------------------------------------------------- /backend/edsl/src/main/scala/ru/tinkoff/tcb/mockingbird/edsl/model/ExampleDescription.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.edsl.model 2 | 3 | import org.scalactic.source 4 | 5 | final case class ExampleDescription(name: String, steps: Example[Any], pos: source.Position) 6 | -------------------------------------------------------------------------------- /backend/examples/src/test/scala/ru/tinkoff/tcb/mockingbird/examples/BasicHttpStubSuite.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.examples 2 | 3 | class BasicHttpStubSuite extends BaseSuite { 4 | private val set = new BasicHttpStub[HttpResponseR] 5 | generateTests(set) 6 | } 7 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/regex/OneOrMore.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils.regex 2 | 3 | import scala.util.matching.Regex 4 | 5 | class OneOrMore(rx: Regex) { 6 | def unapply(str: String): Boolean = rx.findFirstMatchIn(str).isDefined 7 | } 8 | -------------------------------------------------------------------------------- /frontend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@tinkoff/eslint-config/app", 4 | "@tinkoff/eslint-config-react", 5 | "plugin:@tinkoff/tramvai/recommended" 6 | ], 7 | "rules": { 8 | "promise/catch-or-return": "off" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/protocol/fields.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.protocol 2 | 3 | import ru.tinkoff.tcb.generic.Fields 4 | import ru.tinkoff.tcb.utils.id.SID 5 | 6 | object fields { 7 | implicit def sidFields[T]: Fields[SID[T]] = Fields.mk(Nil) 8 | } 9 | -------------------------------------------------------------------------------- /backend/examples/src/test/scala/ru/tinkoff/tcb/mockingbird/examples/HttpStubWithStateSuite.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.examples 2 | 3 | class HttpStubWithStateSuite extends BaseSuite { 4 | private val set = new HttpStubWithState[HttpResponseR] 5 | generateTests(set) 6 | } 7 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils 2 | 3 | import scala.util.matching.Regex 4 | 5 | package object transformation { 6 | val SubstRx: Regex = """\$\{(.+?)\}""".r 7 | val CodeRx: Regex = """%\{(.+?)\}""".r 8 | } 9 | -------------------------------------------------------------------------------- /confluent.md: -------------------------------------------------------------------------------- 1 | Документация: 2 | https://docs.confluent.io/platform/current/kafka-rest/quickstart.html 3 | https://docs.confluent.io/platform/current/tutorials/examples/clients/docs/rest-proxy.html#basic-producer-and-consumer 4 | https://docs.confluent.io/platform/current/kafka-rest/api.html#consumers -------------------------------------------------------------------------------- /frontend/src/mockingbird/layers/pages/mock/JSONRequest.css: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | align-items: center; 4 | 5 | & > * + * { 6 | margin-left: 16px; 7 | } 8 | } 9 | 10 | .description { 11 | margin-top: 8px; 12 | } 13 | 14 | .json { 15 | margin-top: 12px; 16 | } 17 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/protocol/rof.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.protocol 2 | 3 | import io.circe.Json 4 | 5 | import ru.tinkoff.tcb.generic.RootOptionFields 6 | 7 | object rof { 8 | implicit val jsonRof: RootOptionFields[Json] = RootOptionFields.mk(Set.empty) 9 | } 10 | -------------------------------------------------------------------------------- /backend/utils/src/main/scala/ru/tinkoff/tcb/utils/resource/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils 2 | 3 | import scala.io.Source 4 | import scala.util.Using 5 | 6 | package object resource { 7 | def readStr(fileName: String): String = Using.resource(Source.fromResource(fileName))(_.mkString) 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/components/List/List.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './List.css'; 3 | 4 | interface Props { 5 | children: React.ReactNode; 6 | } 7 | 8 | export default function List({ children }: Props) { 9 | return
{children}
; 10 | } 11 | -------------------------------------------------------------------------------- /backend/utils/src/main/scala/ru/tinkoff/tcb/utils/any/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils 2 | 3 | package object any { 4 | implicit class AnyExtensionOps[T](private val t: T) extends AnyVal { 5 | @inline def applyIf(condition: T => Boolean)(fun: T => T): T = 6 | if (condition(t)) fun(t) else t 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/layers/layout/Shadow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './Shadow.css'; 3 | 4 | type Props = { children: React.ReactNode }; 5 | 6 | export function Shadow(props: Props) { 7 | const { children } = props; 8 | return
{children}
; 9 | } 10 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/xpath/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb 2 | 3 | import advxml.transform.XmlZoom 4 | import advxml.xpath.* 5 | 6 | package object xpath { 7 | object XZoom { 8 | def unapply(xpath: String): Option[XmlZoom] = 9 | XmlZoom.fromXPath(xpath).toOption 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/settings.ts: -------------------------------------------------------------------------------- 1 | type Settings = { 2 | relativePath: string; 3 | }; 4 | 5 | const settings: Settings = { 6 | relativePath: '', 7 | }; 8 | 9 | export function configureSettings(configuredSettings: Settings) { 10 | Object.assign(settings, configuredSettings); 11 | } 12 | 13 | export default settings; 14 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/instances/jsonNumber.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.instances 2 | 3 | import io.circe.JsonNumber 4 | 5 | object jsonNumber { 6 | implicit val jsonNumberOrdering: scala.math.Ordering[JsonNumber] = 7 | (lhs: JsonNumber, rhs: JsonNumber) => lhs.toBigDecimal.get.compare(rhs.toBigDecimal.get) 8 | } 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | This template isn't a strict requirement to open issues, but please try to provide as much information as possible. 2 | 3 | **Version**: (e.g. `x.y.z`) 4 | 5 | ### Expected behavior 6 | 7 | ### Actual behavior 8 | 9 | ### Steps to reproduce the behavior 10 | 11 | ### Workaround 12 | 13 | @mockingbird/maintainers 14 | -------------------------------------------------------------------------------- /backend/secrets-for-test.conf: -------------------------------------------------------------------------------- 1 | { 2 | "secrets": { 3 | "server": { 4 | "allowedOrigins": [ 5 | "*" 6 | ], 7 | "healthCheckRoute": "/ready", 8 | }, 9 | "security": { 10 | "secret": "secret" 11 | }, 12 | "mongodb": { 13 | "uri": "mongodb://mongo/mockingbird" 14 | } 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/components/Page/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { ReactNode } from 'react'; 3 | import styles from './Page.css'; 4 | 5 | type Props = { 6 | children: ReactNode; 7 | }; 8 | 9 | export default function Page({ children }: Props) { 10 | return
{children}
; 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/layers/pages/common.css: -------------------------------------------------------------------------------- 1 | .root > * + * { 2 | margin-top: 16px; 3 | } 4 | 5 | .row { 6 | display: flex; 7 | align-items: center; 8 | justify-content: space-between; 9 | } 10 | 11 | .row > * + * { 12 | margin-left: 16px; 13 | } 14 | 15 | .buttonMore { 16 | display: flex; 17 | justify-content: center; 18 | } 19 | -------------------------------------------------------------------------------- /backend/dataAccess/src/main/scala/ru/tinkoff/tcb/generic/Identifiable.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.generic 2 | 3 | import scala.annotation.implicitNotFound 4 | 5 | @implicitNotFound("Could not find an instance of Identifiable for ${T}") 6 | trait Identifiable[T] extends Serializable { 7 | def getId(t: T): String 8 | } 9 | 10 | object Identifiable 11 | -------------------------------------------------------------------------------- /frontend/src/infrastructure/helpers/scroll/index.ts: -------------------------------------------------------------------------------- 1 | export function scrollTop(behavior: 'smooth' | 'auto' = 'auto') { 2 | window.scrollTo({ top: 0, behavior }); 3 | } 4 | 5 | export function scrollToId(id: string, behavior: 'smooth' | 'auto' = 'smooth') { 6 | const el = window.document.getElementById(id); 7 | if (el) el.scrollIntoView({ behavior }); 8 | } 9 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/xml/XCData.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils.transformation.xml 2 | 3 | import scala.util.Try 4 | import scala.xml.Node 5 | 6 | import ru.tinkoff.tcb.utils.xml.SafeXML 7 | 8 | object XCData { 9 | def unapply(arg: String): Option[Node] = Try(SafeXML.loadString(arg)).toOption 10 | } 11 | -------------------------------------------------------------------------------- /frontend/stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@tinkoff/stylelint-config/less'], 3 | ignoreFiles: ['**/dist/**', '**/node_modules/**'], 4 | rules: { 5 | 'selector-class-pattern': null, 6 | 'property-no-unknown': [ 7 | true, 8 | { 9 | ignoreProperties: ['composes'], 10 | }, 11 | ], 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/time/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils 2 | 3 | import java.time.format.DateTimeFormatter 4 | import scala.util.Try 5 | 6 | package object time { 7 | object Formatter { 8 | def unapply(arg: String): Option[DateTimeFormatter] = 9 | Try(DateTimeFormatter.ofPattern(arg)).toOption 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/resources/qa.conf: -------------------------------------------------------------------------------- 1 | include classpath("application.conf") 2 | include file("/opt/mockingbird/conf/secrets.conf") 3 | 4 | ru.tinkoff.tcb { 5 | db.mongo = ${?secrets.mongodb} 6 | server = ${?secrets.server} 7 | security = ${secrets.security} 8 | proxy = ${?secrets.proxy} 9 | event = ${?secrets.event} 10 | tracing = ${?secrets.tracing} 11 | } -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/config/PureconfigEnum.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.config 2 | 3 | import enumeratum.* 4 | import pureconfig.ConfigReader 5 | 6 | trait PureconfigEnum[T <: EnumEntry] { self: Enum[T] => 7 | implicit val vreader: ConfigReader[T] = 8 | ConfigReader[String].map(v => self.withNameInsensitive(v)) 9 | } 10 | -------------------------------------------------------------------------------- /backend/utils/src/main/scala/ru/tinkoff/tcb/utils/map/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils 2 | 3 | package object map { 4 | implicit class MapOps[K, V](map: Map[K, V]) { 5 | 6 | /** 7 | * Adds a key is the value is Some(..) 8 | */ 9 | @inline def +?(kv: (K, Option[V])): Map[K, V] = 10 | kv._2.fold(map)(v => map + (kv._1 -> v)) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/infrastructure/helpers/state/index.ts: -------------------------------------------------------------------------------- 1 | export function selectorAsIs(state: any) { 2 | return state; 3 | } 4 | 5 | export function extractError(e: any) { 6 | if (!e || !e.body) return; 7 | if (e.body.error) { 8 | if (e.body.messages) return [e.body.error, ...e.body.messages].join('. '); 9 | return e.body.error; 10 | } 11 | return e.body; 12 | } 13 | -------------------------------------------------------------------------------- /backend/dataAccess/src/test/scala/ru/tinkoff/tcb/bson/enumeratum/Dummy.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.bson.enumeratum 2 | 3 | import enumeratum.* 4 | 5 | sealed trait Dummy extends EnumEntry 6 | object Dummy extends Enum[Dummy] with BsonEnum[Dummy] { 7 | case object A extends Dummy 8 | case object B extends Dummy 9 | case object C extends Dummy 10 | val values = findValues 11 | } 12 | -------------------------------------------------------------------------------- /backend/mockingbird-native/src/universal/qa.conf: -------------------------------------------------------------------------------- 1 | include classpath("application.conf") 2 | include file("/opt/mockingbird-native/conf/secrets.conf") 3 | 4 | ru.tinkoff.tcb { 5 | db.mongo = ${?secrets.mongodb} 6 | server = ${?secrets.server} 7 | security = ${secrets.security} 8 | proxy = ${?secrets.proxy} 9 | event = ${?secrets.event} 10 | tracing = ${?secrets.tracing} 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bsp 2 | .idea 3 | .idea_modules 4 | .ensime 5 | .ensime_cache/ 6 | *.sublime-project 7 | *.sublime-workspace 8 | target/ 9 | .DS_Store 10 | **/main/resources/local.conf 11 | keys/private 12 | .metals 13 | scalafix-commons.conf 14 | .vscode 15 | .bloop 16 | **/project/metals.sbt 17 | .sbt-cache 18 | node_modules 19 | dist 20 | .fleet 21 | secrets.conf 22 | /backend/compose-test.yml 23 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/xttp/xml/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils.xttp 2 | 3 | import scala.xml.Node 4 | 5 | import sttp.client4.ResponseAs 6 | import sttp.client4.asString 7 | 8 | import ru.tinkoff.tcb.utils.xml.SafeXML 9 | 10 | package object xml { 11 | def asXML: ResponseAs[Either[String, Node]] = asString.map(_.map(SafeXML.loadString)) 12 | } 13 | -------------------------------------------------------------------------------- /backend/project/Versions.scala: -------------------------------------------------------------------------------- 1 | object Versions { 2 | val cats = "2.10.0" 3 | val mongoScalaDriver = "4.10.2" 4 | val tapir = "1.9.2" 5 | val graalvm = "22.3.0" 6 | val micrometer = "1.8.5" 7 | val glass = "0.3.0" 8 | val sttp = "4.0.0-M13" 9 | val zio = "2.0.19" 10 | val circe = "0.14.10" 11 | } 12 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | 3 | WORKDIR /app 4 | 5 | ARG APP_ID 6 | ARG PUBLIC_FOLDER=/app/public/${APP_ID}/compiled 7 | 8 | COPY ./package.json /app/ 9 | COPY ./webpackAssets.json /app/ 10 | COPY ./dist/server /app 11 | COPY ./dist/client/stats.json /app 12 | COPY ./dist/client ${PUBLIC_FOLDER} 13 | 14 | ENV APP_ID ${APP_ID} 15 | 16 | ENTRYPOINT [ "node", "/app/server.js" ] 17 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/destination/types.ts: -------------------------------------------------------------------------------- 1 | export interface Destination { 2 | name: string; 3 | description: string; 4 | service: string; 5 | request: any; 6 | init?: any[]; 7 | shutdown?: any[]; 8 | } 9 | 10 | export interface DestinationFormData { 11 | name: string; 12 | description: string; 13 | request: string; 14 | init: string; 15 | shutdown: string; 16 | } 17 | -------------------------------------------------------------------------------- /frontend/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":preserveSemverRanges" 5 | ], 6 | "stabilityDays": 3, 7 | "packageRules": [ 8 | { 9 | "packagePatterns": ["^@tramvai/cli"], 10 | "groupName": "Tramvai cli packages" 11 | }, 12 | { 13 | "packagePatterns": ["^@tramvai"], 14 | "groupName": "tramvai packages" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/regex/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils 2 | 3 | import scala.util.matching.Regex 4 | 5 | package object regex { 6 | private val Group = "<(?[a-zA-Z0-9]+)>".r 7 | 8 | implicit class RegexExt(private val rx: Regex) { 9 | def groups: Seq[String] = Group.findAllMatchIn(rx.pattern.pattern()).map(_.group("name")).to(Seq) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/instances/predicate/and.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.instances.predicate 2 | 3 | object and { 4 | implicit def logicalAndMonoid[T]: Monoid[T => Boolean] = 5 | new Monoid[T => Boolean] { 6 | override def empty: T => Boolean = _ => true 7 | 8 | override def combine(x: T => Boolean, y: T => Boolean): T => Boolean = t => x(t) && y(t) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/components/Language/LanguagePicker.css: -------------------------------------------------------------------------------- 1 | .control { 2 | display: flex; 3 | width: 150px; 4 | justify-content: flex-end; 5 | padding: 8px 16px; 6 | border-radius: 16px; 7 | transition: background-color 150ms ease; 8 | } 9 | 10 | .itemContent { 11 | align-content: center; 12 | justify-content: space-between; 13 | } 14 | 15 | .icon { 16 | width: 1.2rem; 17 | height: 1.2rem; 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/components/List/ListEmpty.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { Text } from '@mantine/core'; 4 | 5 | interface Props { 6 | text?: string; 7 | } 8 | 9 | export default function ListEmpty(props: Props) { 10 | const { t } = useTranslation(); 11 | const { text = t('components.list.textDefault') } = props; 12 | return {text}; 13 | } 14 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/XPathTestRequest.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.api.request 2 | 3 | import io.circe.Decoder 4 | import io.circe.Encoder 5 | import sttp.tapir.Schema 6 | 7 | import ru.tinkoff.tcb.utils.xml.XMLString 8 | import ru.tinkoff.tcb.xpath.SXpath 9 | 10 | final case class XPathTestRequest(xml: XMLString.Type, path: SXpath) derives Decoder, Encoder, Schema 11 | -------------------------------------------------------------------------------- /backend/utils/src/main/scala/ru/tinkoff/tcb/utils/instances/predicate/and.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils.instances.predicate 2 | 3 | object and { 4 | implicit def logicalAndMonoid[T]: Monoid[T => Boolean] = 5 | new Monoid[T => Boolean] { 6 | override def empty: T => Boolean = _ => true 7 | 8 | override def combine(x: T => Boolean, y: T => Boolean): T => Boolean = 9 | t => x(t) && y(t) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/utils/src/main/scala/ru/tinkoff/tcb/utils/instances/predicate/or.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils.instances.predicate 2 | 3 | object or { 4 | implicit def logicalOrMonoid[T]: Monoid[T => Boolean] = 5 | new Monoid[T => Boolean] { 6 | override def empty: T => Boolean = _ => false 7 | 8 | override def combine(x: T => Boolean, y: T => Boolean): T => Boolean = 9 | t => x(t) || y(t) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/utils/src/main/scala/ru/tinkoff/tcb/utils/lazy/Lazy.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils.`lazy` 2 | 3 | final class Lazy[T](compute: () => T) { 4 | private var wasEvaluated: Boolean = false 5 | 6 | lazy val value: T = { 7 | wasEvaluated = true 8 | compute() 9 | } 10 | 11 | def isComputed: Boolean = wasEvaluated 12 | } 13 | 14 | object Lazy { 15 | def apply[T](t: => T): Lazy[T] = new Lazy[T](() => t) 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/source/types.ts: -------------------------------------------------------------------------------- 1 | export interface Source { 2 | name: string; 3 | description: string; 4 | service: string; 5 | request: any; 6 | init?: any[]; 7 | shutdown?: any[]; 8 | reInitTriggers?: any[]; 9 | } 10 | 11 | export interface SourceFormData { 12 | name: string; 13 | description: string; 14 | request: string; 15 | init: string; 16 | shutdown: string; 17 | reInitTriggers: string; 18 | } 19 | -------------------------------------------------------------------------------- /backend/mockingbird/src/test/scala/ru/tinkoff/tcb/xpath/SXpathSpec.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.xpath 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | class SXpathSpec extends AnyFunSuite with Matchers { 7 | 8 | test("SXpath equals") { 9 | val xp1 = SXpath.fromString("/user/id") 10 | val xp2 = SXpath.fromString("/user/id") 11 | 12 | xp1 shouldBe xp2 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /backend/dataAccess/src/main/scala/ru/tinkoff/tcb/bson/enumeratum/BsonEnum.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.bson.enumeratum 2 | 3 | import enumeratum.* 4 | import oolong.bson.BsonDecoder 5 | import oolong.bson.BsonEncoder 6 | 7 | trait BsonEnum[A <: EnumEntry] { self: Enum[A] => 8 | implicit val bsonEncoder: BsonEncoder[A] = 9 | EnumHandler.writer(this) 10 | 11 | implicit val bsonDecoder: BsonDecoder[A] = 12 | EnumHandler.reader(this) 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/components/List/ListLoading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { FlexProps } from '@mantine/core'; 3 | import { Flex, Loader } from '@mantine/core'; 4 | 5 | export function ListLoading(props: FlexProps) { 6 | return ( 7 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/response/OperationResult.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.api.response 2 | 3 | import io.circe.Decoder 4 | import io.circe.Encoder 5 | import sttp.tapir.Schema 6 | 7 | final case class OperationResult[T](status: String, id: Option[T] = None) derives Decoder, Encoder, Schema 8 | 9 | object OperationResult { 10 | def apply[T](status: String, id: T): OperationResult[T] = OperationResult(status, Some(id)) 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/infrastructure/utils/hooks/debouce.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | function useDebounce(value: T, delay = 500): T { 4 | const [debouncedValue, setDebouncedValue] = useState(value); 5 | useEffect(() => { 6 | const timer = setTimeout(() => setDebouncedValue(value), delay); 7 | return () => { 8 | clearTimeout(timer); 9 | }; 10 | }, [value, delay]); 11 | return debouncedValue; 12 | } 13 | 14 | export default useDebounce; 15 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/infrastructure/utils/forms/index.ts: -------------------------------------------------------------------------------- 1 | export function parseJSON(json: string, nullByEmpty?: boolean) { 2 | let result = null; 3 | if (!json) return result; 4 | try { 5 | result = JSON.parse(json); 6 | if (nullByEmpty && !Object.keys(result).length) return null; 7 | } catch (e) {} 8 | return result; 9 | } 10 | 11 | export function stringifyJSON(data: any, defaultValue = {}) { 12 | return JSON.stringify(data || defaultValue, undefined, 2); 13 | } 14 | -------------------------------------------------------------------------------- /frontend/env.development.js: -------------------------------------------------------------------------------- 1 | /* value should be only string */ 2 | module.exports = { 3 | PORT: '3000', 4 | APP_ID: 'mockingbird', 5 | NODE_ENV: 'development', 6 | MOCKINGBIRD_API: 'http://localhost:8228/api/internal/mockingbird', 7 | MOCKINGBIRD_EXEC_API: 'http://localhost:8228/api/mockingbird/exec', 8 | RELATIVE_PATH: '/mockingbird', 9 | ASSETS_PREFIX: 'static', 10 | // logs 11 | DEBUG_LEVEL: 'info', 12 | LOG_LEVEL: 'trace', 13 | LOG_ENABLE: '*', 14 | DEBUG_MODE: true, 15 | }; 16 | -------------------------------------------------------------------------------- /frontend/src/infrastructure/notifications/store/actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction as createActionCore } from '@tramvai/core'; 2 | import { addToast, removeToast } from './store'; 3 | 4 | export const addToastAction = createActionCore({ 5 | name: 'ADD_TOAST_ACTION', 6 | fn: ({ dispatch }, item) => dispatch(addToast(item)), 7 | }); 8 | 9 | export const removeToastAction = createActionCore({ 10 | name: 'REMOVE_TOAST_ACTION', 11 | fn: ({ dispatch }, item) => dispatch(removeToast(item)), 12 | }); 13 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/ScenarioResolveRequest.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.api.request 2 | 3 | import io.circe.Decoder 4 | import io.circe.Encoder 5 | import sttp.tapir.Schema 6 | 7 | import ru.tinkoff.tcb.mockingbird.model.SourceConfiguration 8 | import ru.tinkoff.tcb.utils.id.SID 9 | 10 | final case class ScenarioResolveRequest(source: SID[SourceConfiguration], message: String) 11 | derives Encoder, 12 | Decoder, 13 | Schema 14 | -------------------------------------------------------------------------------- /frontend/src/infrastructure/utils/lruCache.ts: -------------------------------------------------------------------------------- 1 | import LRU from 'lru-cache'; 2 | 3 | const allCaches = []; 4 | 5 | const getOpts = (opts) => { 6 | const defaults = { max: 1000 }; 7 | return { ...defaults, ...opts }; 8 | }; 9 | 10 | const lruCache = (opts) => { 11 | const cache = new LRU(getOpts(opts)); 12 | 13 | allCaches.push(cache); 14 | 15 | return cache; 16 | }; 17 | 18 | export const clearAllCaches = () => { 19 | allCaches.forEach((cache) => cache.reset()); 20 | }; 21 | 22 | export default lruCache; 23 | -------------------------------------------------------------------------------- /backend/utils/src/test/scala/ru/tinkoff/tcb/utils/crypto/SyncAESSpec.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils.crypto 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | class SyncAESSpec extends AnyFunSuite with Matchers { 7 | private val zaes = new SyncAES("TOP SECRET") 8 | 9 | test("Encode-Decode") { 10 | val (encoded, salt, iv) = zaes.encrypt("my data") 11 | val decoded = zaes.decrypt(encoded, salt, iv) 12 | 13 | decoded shouldBe "my data" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/sources/ui/Sources.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { Props as SelectProps } from 'src/mockingbird/components/form/Select'; 3 | import Select from 'src/mockingbird/components/form/Select'; 4 | import { useSources } from '../hooks'; 5 | 6 | type Props = Omit & { 7 | serviceId: string; 8 | }; 9 | 10 | export default function Sources({ serviceId, ...props }: Props) { 11 | const sources = useSources(serviceId); 12 | return ; 13 | } 14 | -------------------------------------------------------------------------------- /backend/utils/src/test/scala/ru/tinkoff/tcb/utils/lazy/LazySpec.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils.`lazy` 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | class LazySpec extends AnyFunSuite with Matchers { 7 | test("laziness test") { 8 | var wasTouched = false 9 | 10 | val sut = Lazy { wasTouched = true; 42 } 11 | 12 | wasTouched shouldBe false 13 | sut.isComputed shouldBe false 14 | 15 | sut.value shouldBe 42 16 | wasTouched shouldBe true 17 | sut.isComputed shouldBe true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/tramvai.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@tramvai/cli/schema.json", 3 | "projects": { 4 | "mockingbird": { 5 | "name": "mockingbird", 6 | "root": "src/mockingbird", 7 | "type": "application", 8 | "sourceMap": true, 9 | "postcss": { 10 | "config": "src/mockingbird/postcss", 11 | "cssLocalIdentName": {} 12 | }, 13 | "dedupe": { 14 | "ignore": [ 15 | "@platform-ui/" 16 | ] 17 | }, 18 | "hotRefresh": { 19 | "enabled": true 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/Service.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.model 2 | 3 | import io.circe.Decoder 4 | import io.circe.Encoder 5 | import oolong.bson.BsonDecoder 6 | import oolong.bson.BsonEncoder 7 | import oolong.bson.given 8 | import oolong.bson.meta.QueryMeta 9 | import oolong.bson.meta.queryMeta 10 | import sttp.tapir.Schema 11 | 12 | final case class Service(suffix: String, name: String) derives BsonEncoder, BsonDecoder, Decoder, Encoder, Schema 13 | 14 | object Service { 15 | inline given QueryMeta[Service] = queryMeta(_.suffix -> "_id") 16 | } 17 | -------------------------------------------------------------------------------- /backend/circe-utils/src/main/scala/ru/tinkoff/tcb/utils/circe/optics/PathPart.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils.circe.optics 2 | 3 | sealed private[optics] trait PathPart { 4 | def fold[T](onField: String => T, onIndex: Int => T, onTraverse: => T): T = 5 | this match { 6 | case Field(name) => onField(name) 7 | case Index(index) => onIndex(index) 8 | case Traverse => onTraverse 9 | } 10 | } 11 | final private[optics] case class Field(name: String) extends PathPart 12 | final private[optics] case class Index(index: Int) extends PathPart 13 | private[optics] case object Traverse extends PathPart 14 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/services/ui/ServiceItem.css: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | justify-content: space-between; 4 | padding: 16px; 5 | word-break: break-word; 6 | } 7 | 8 | .block { 9 | &:last-child { 10 | min-width: 100px; 11 | margin-left: 16px; 12 | text-align: right; 13 | } 14 | 15 | & > * + * { 16 | margin-top: 4px; 17 | } 18 | } 19 | 20 | .blockLinks { 21 | composes: block; 22 | } 23 | 24 | .blockLinks > * { 25 | display: block; 26 | margin-left: auto; 27 | margin-right: 0; 28 | line-height: 20px; 29 | 30 | &:first-child { 31 | line-height: 24px; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "esnext", 5 | "module": "esnext", 6 | "lib": ["es2015", "es2016", "es2017", "es2018", "dom"], 7 | "strict": true, 8 | "sourceMap": true, 9 | "allowJs": true, 10 | "allowSyntheticDefaultImports": true, 11 | "experimentalDecorators": true, 12 | "jsx": "react", 13 | "esModuleInterop": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": false, 16 | "noEmitOnError": true, 17 | "importHelpers": true, 18 | "resolveJsonModule": true, 19 | "baseUrl": "." 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes #issue_number 2 | 3 | ### Problem 4 | 5 | Explain here the context, and why you're making that change. 6 | What is the problem you're trying to solve? 7 | 8 | ### Solution 9 | 10 | Describe the modifications you've done. 11 | 12 | ### Notes 13 | 14 | Additional notes. 15 | 16 | ### Checklist 17 | 18 | - [ ] Unit test all changes 19 | - [ ] Update `README.md` if applicable 20 | - [ ] Add `[WIP]` to the pull request title if it's work in progress 21 | - [ ] [Squash commits](https://ariejan.net/2011/07/05/git-squash-your-latests-commits-into-one) that aren't meaningful changes 22 | 23 | @mockingbird/maintainers 24 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/layers/pages/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '@mantine/core'; 3 | import { useNavigate } from '@tramvai/module-router'; 4 | import PageHeader from 'src/components/PageHeader/PageHeader'; 5 | import { useTranslation } from 'react-i18next'; 6 | 7 | export default function NotFound() { 8 | const { t } = useTranslation(); 9 | const navigateTo = useNavigate('/'); 10 | return ( 11 |
12 | 13 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /backend/integrationTests/k6/howToTest.md: -------------------------------------------------------------------------------- 1 | # How to test with k6 2 | 3 | k6 is used for integration testing. 4 | At first start mockingbird app, then set up `httpHost` and `grpcHost` variables, after that run tests. 5 | 6 | ### Shell commands 7 | 8 | - To create new test use `k6 new fileName.js` 9 | 10 | - To run test script use `k6 run filename.js` 11 | 12 | - To group several tests in a single run use scenarios (e.g. see file `scenario.js`). 13 | To run scenario use `k6 run scenario.js` 14 | 15 | ### Links 16 | 17 | - [Running k6](https://grafana.com/docs/k6/latest/get-started/running-k6/) 18 | - [Javascript API](https://grafana.com/docs/k6/latest/javascript-api/) -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/service/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useActions, useStoreSelector } from '@tramvai/state'; 3 | import { selectorAsIs } from 'src/mockingbird/infrastructure/helpers/state'; 4 | import store from '../reducers/store'; 5 | import { fetchAction } from '../actions/fetchAction'; 6 | 7 | export default function useService(serviceId: string) { 8 | const { data: service } = useStoreSelector(store, selectorAsIs); 9 | const fetchService = useActions(fetchAction); 10 | useEffect(() => { 11 | if (!process.env.BROWSER) return; 12 | fetchService(serviceId); 13 | }, [serviceId, fetchService]); 14 | return service; 15 | } 16 | -------------------------------------------------------------------------------- /backend/integrationTests/k6/v4/scope/definitions/test_service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package market_data; 3 | 4 | enum InstrumentIDKind { 5 | ID_1 = 0; 6 | ID_2 = 1; 7 | } 8 | 9 | message PricesRequest { 10 | string instrument_id = 1; 11 | optional InstrumentIDKind instrument_id_kind = 2; 12 | } 13 | 14 | message PricesResponse { 15 | 16 | string instrument_id = 1; 17 | string tracking_id = 2; 18 | optional string error = 100; 19 | } 20 | 21 | service OTCMarketDataService { 22 | rpc Countdown (PricesRequest) returns (PricesResponse) {} 23 | rpc Ephemeral (PricesRequest) returns (PricesResponse) {} 24 | rpc Persistent (PricesRequest) returns (PricesResponse) {} 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/components/List/ListError.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { Text, Button } from '@mantine/core'; 4 | 5 | interface Props { 6 | text?: string; 7 | onRetry?: () => void; 8 | } 9 | 10 | export default function ListError(props: Props) { 11 | const { t } = useTranslation(); 12 | const { text = t('components.list.loadError'), onRetry } = props; 13 | return ( 14 | 15 | {text} 16 | {onRetry && ( 17 | 20 | )} 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/labels/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useActions, useStoreSelector } from '@tramvai/state'; 3 | import { selectorAsIs } from 'src/mockingbird/infrastructure/helpers/state'; 4 | import labelsStore from '../reducers'; 5 | import { fetchAction as fetchLabelsAction } from '../actions'; 6 | 7 | export default function useLabels(serviceId: string): string[] { 8 | const { labels } = useStoreSelector(labelsStore, selectorAsIs); 9 | const fetchLabels = useActions(fetchLabelsAction); 10 | useEffect(() => { 11 | if (!process.env.BROWSER) return; 12 | fetchLabels(serviceId); 13 | }, [serviceId, fetchLabels]); 14 | return labels; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/layers/layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { ComponentType } from 'react'; 3 | import { MantineProvider, Container, SimpleGrid } from '@mantine/core'; 4 | import { NotificationStack } from 'src/infrastructure/notifications'; 5 | 6 | interface Props { 7 | children: ComponentType[]; 8 | } 9 | 10 | export const Layout = (props: Props) => { 11 | const [header, page] = props.children; 12 | return ( 13 | 14 | {header} 15 | 16 | 17 | {page} 18 | 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/CallbackResponseMode.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.model 2 | 3 | import enumeratum.* 4 | import enumeratum.EnumEntry.Lowercase 5 | import sttp.tapir.codec.enumeratum.TapirCodecEnumeratum 6 | 7 | import ru.tinkoff.tcb.bson.enumeratum.BsonEnum 8 | 9 | sealed trait CallbackResponseMode extends EnumEntry with Lowercase 10 | object CallbackResponseMode 11 | extends Enum[CallbackResponseMode] 12 | with CirceEnum[CallbackResponseMode] 13 | with BsonEnum[CallbackResponseMode] 14 | with TapirCodecEnumeratum { 15 | case object Json extends CallbackResponseMode 16 | case object Xml extends CallbackResponseMode 17 | 18 | val values = findValues 19 | } 20 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/response/SourceDTO.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.api.response 2 | 3 | import io.circe.Decoder 4 | import io.circe.Encoder 5 | import oolong.bson.* 6 | import oolong.bson.given 7 | import oolong.bson.meta.QueryMeta 8 | import oolong.bson.meta.queryMeta 9 | import sttp.tapir.Schema 10 | 11 | import ru.tinkoff.tcb.mockingbird.model.SourceConfiguration 12 | import ru.tinkoff.tcb.utils.id.SID 13 | 14 | final case class SourceDTO(name: SID[SourceConfiguration], description: String) 15 | derives Encoder, 16 | Decoder, 17 | Schema, 18 | BsonDecoder 19 | 20 | object SourceDTO { 21 | inline given QueryMeta[SourceConfiguration] = queryMeta(_.name -> "_id") 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/infrastructure/request/index.ts: -------------------------------------------------------------------------------- 1 | import request from '@tinkoff/request-core'; 2 | import deduplicateCache from '@tinkoff/request-plugin-cache-deduplicate'; 3 | import memoryCache from '@tinkoff/request-plugin-cache-memory'; 4 | import http from '@tinkoff/request-plugin-protocol-http'; 5 | import memoryConstructor from '../utils/lruCache'; 6 | 7 | const makeRequest = request([ 8 | memoryCache({ 9 | memoryConstructor, 10 | lruOptions: { max: 1000, maxAge: 15 * 60 * 1000 }, 11 | }), 12 | deduplicateCache(), 13 | http(), 14 | ]); 15 | 16 | export function getJson(url: string, abortPromise?: Promise) { 17 | return makeRequest({ 18 | url, 19 | httpMethod: 'get', 20 | cache: true, 21 | abortPromise, 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /backend/mockingbird-api/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | UTF-8 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/infrastructure/request/index.ts: -------------------------------------------------------------------------------- 1 | import request from '@tinkoff/request-core'; 2 | import http from '@tinkoff/request-plugin-protocol-http'; 3 | 4 | const EMPTY_OBJECT = {}; 5 | 6 | const makeRequest = request([http()]); 7 | 8 | export function getJson(url: string, options: any = EMPTY_OBJECT) { 9 | const { 10 | body, 11 | query, 12 | httpMethod = 'get', 13 | type = 'application/json', 14 | } = options; 15 | return makeRequest({ 16 | ...options, 17 | url, 18 | payload: body ? JSON.stringify(body) : body, 19 | query, 20 | httpMethod, 21 | type, 22 | }); 23 | } 24 | 25 | export function patchJson(url: string, options: any = EMPTY_OBJECT) { 26 | return getJson(url, { ...options, httpMethod: 'patch' }); 27 | } 28 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/response/DestinationDTO.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.api.response 2 | 3 | import io.circe.Decoder 4 | import io.circe.Encoder 5 | import oolong.bson.* 6 | import oolong.bson.given 7 | import oolong.bson.meta.QueryMeta 8 | import oolong.bson.meta.queryMeta 9 | import sttp.tapir.Schema 10 | 11 | import ru.tinkoff.tcb.mockingbird.model.DestinationConfiguration 12 | import ru.tinkoff.tcb.utils.id.SID 13 | 14 | final case class DestinationDTO(name: SID[DestinationConfiguration], description: String) 15 | derives Encoder, 16 | Decoder, 17 | Schema, 18 | BsonDecoder 19 | 20 | object DestinationDTO { 21 | inline given QueryMeta[DestinationConfiguration] = queryMeta(_.name -> "_id") 22 | } 23 | -------------------------------------------------------------------------------- /backend/integrationTests/k6/v4/responseMode/definitions/test_service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package market_data; 3 | 4 | enum InstrumentIDKind { 5 | ID_1 = 0; 6 | ID_2 = 1; 7 | } 8 | 9 | message PricesRequest { 10 | string instrument_id = 1; 11 | optional InstrumentIDKind instrument_id_kind = 2; 12 | } 13 | 14 | message PricesResponse { 15 | 16 | string instrument_id = 1; 17 | string tracking_id = 2; 18 | optional string error = 100; 19 | } 20 | 21 | service OTCMarketDataService { 22 | rpc FillStream (PricesRequest) returns (stream PricesResponse) {} 23 | rpc Repeat (PricesRequest) returns (stream PricesResponse) {} 24 | rpc NoBody (PricesRequest) returns (stream PricesResponse) {} 25 | rpc Unary (PricesRequest) returns (PricesResponse) {} 26 | } 27 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird 2 | 3 | import io.circe.Json 4 | import io.circe.literal.* 5 | import io.circe.parser.parse 6 | 7 | package object api { 8 | /* 9 | "World" for this application 10 | */ 11 | type WLD = Tracing 12 | 13 | def mkErrorResponse(message: String): String = 14 | json"""{"error": $message}""".noSpaces 15 | 16 | def queryParamsToJsonObject(query: Seq[(String, Seq[String])]): Json = 17 | Json.fromFields( 18 | query.map { case (k, vs) => 19 | val js = vs.map(s => parse(s).getOrElse(Json.fromString(s))) match { 20 | case Seq(x) => x 21 | case xs => Json.arr(xs*) 22 | } 23 | k -> js 24 | } 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /backend/dataAccess/src/test/scala/ru/tinkoff/tcb/generic/FieldsSpec.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.generic 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | class FieldsSpec extends AnyFunSuite with Matchers { 7 | test("Fields of empty case class") { 8 | Fields[Evidence].fields shouldBe Nil 9 | } 10 | 11 | case class Projection(ev: Option[Evidence], label: String) 12 | 13 | test("Fields of Projection") { 14 | Fields[Projection].fields shouldBe List("ev", "label") 15 | } 16 | 17 | test("Fields of sealed trait") { 18 | Fields[ST].fields shouldBe Nil 19 | } 20 | } 21 | 22 | final case class Evidence() 23 | 24 | sealed trait ST 25 | final case class A(a: Int) extends ST 26 | final case class B(b: Int) extends ST 27 | -------------------------------------------------------------------------------- /backend/mockingbird-api/src/main/scala/ru/tinkoff/tcb/mockingbird/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb 2 | 3 | import ru.tinkoff.tcb.mockingbird.api.Tracing 4 | import ru.tinkoff.tcb.mockingbird.api.WLD 5 | import ru.tinkoff.tcb.mockingbird.config.MockingbirdConfiguration 6 | import ru.tinkoff.tcb.mockingbird.config.SecurityConfig 7 | import ru.tinkoff.tcb.utils.crypto.AES 8 | import ru.tinkoff.tcb.utils.crypto.SyncAES 9 | 10 | package object mockingbird { 11 | val wldRuntime: Runtime[WLD] = 12 | Unsafe.unsafe { implicit uns => 13 | Runtime.unsafe.fromLayer( 14 | MockingbirdConfiguration.tracing >>> 15 | Tracing.live 16 | ) 17 | } 18 | 19 | val aesEncoder: URLayer[SecurityConfig, AES] = 20 | ZLayer.fromFunction((sc: SecurityConfig) => new SyncAES(sc.secret)) 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/layers/pages/mocks/GRPCItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate } from '@tramvai/module-router'; 3 | import type { TGRPCMock } from 'src/mockingbird/models/mock/types'; 4 | import { getPathMock } from 'src/mockingbird/paths'; 5 | import Item from './Item'; 6 | 7 | type Props = { 8 | serviceId: string; 9 | item: TGRPCMock; 10 | }; 11 | 12 | export default function GRPCItem({ serviceId, item }: Props) { 13 | const { id, name, methodName, scope, times, labels } = item; 14 | const navigate = useNavigate(getPathMock(serviceId, id, 'grpc')); 15 | return ( 16 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/service/actions/fetchAction.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@tramvai/core'; 2 | import { getJson } from 'src/mockingbird/infrastructure/request'; 3 | import { setLoading, fetchSuccess, fetchFail } from '../reducers/store'; 4 | 5 | export const fetchAction = createAction({ 6 | name: 'FETCH_SERVICE_ACTION', 7 | fn: ({ dispatch, getState }, id) => { 8 | dispatch(setLoading()); 9 | const { 10 | environment: { MOCKINGBIRD_API }, 11 | } = getState(); 12 | return getJson(`${MOCKINGBIRD_API}/v2/service/${id}`) 13 | .then((response) => { 14 | if (!response || !response.name) { 15 | throw new Error(); 16 | } 17 | return dispatch(fetchSuccess(response)); 18 | }) 19 | .catch(() => dispatch(fetchFail())); 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/services/actions/fetchAction.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@tramvai/core'; 2 | import { getJson } from 'src/mockingbird/infrastructure/request'; 3 | import { setLoading, fetchSuccess, fetchFail } from '../reducers/store'; 4 | 5 | export const fetchAction = createAction({ 6 | name: 'FETCH_SERVICES_ACTION', 7 | fn: ({ dispatch, getState }) => { 8 | dispatch(setLoading()); 9 | const { 10 | environment: { MOCKINGBIRD_API }, 11 | } = getState(); 12 | return getJson(`${MOCKINGBIRD_API}/v2/service`) 13 | .then((response) => { 14 | if (!response || !Array.isArray(response)) { 15 | throw new Error(); 16 | } 17 | return dispatch(fetchSuccess(response)); 18 | }) 19 | .catch(() => dispatch(fetchFail())); 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/webform/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils 2 | 3 | import java.net.URLDecoder 4 | 5 | import io.circe.Json 6 | 7 | package object webform { 8 | def decode(s: String): Map[String, List[String]] = 9 | s.split("&") 10 | .toList 11 | .flatMap(kv => 12 | kv.split("=", 2) match { 13 | case Array(k, v) => 14 | Some((URLDecoder.decode(k, "UTF-8"), URLDecoder.decode(v, "UTF-8"))) 15 | case _ => None 16 | } 17 | ) 18 | .groupMap(_._1)(_._2) 19 | 20 | def toJson(form: Map[String, List[String]]): Json = 21 | Json.fromFields(form.view.mapValues { 22 | case List(single) => Json.fromString(single) 23 | case list => Json.fromValues(list.map(Json.fromString)) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/labels/actions/index.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@tramvai/core'; 2 | import { getJson } from 'src/mockingbird/infrastructure/request'; 3 | import { setLoading, fetchSuccess, fetchFail } from '../reducers'; 4 | 5 | export const fetchAction = createAction({ 6 | name: 'FETCH_LABELS_ACTION', 7 | fn: ({ dispatch, getState }, service) => { 8 | dispatch(setLoading()); 9 | const { 10 | environment: { MOCKINGBIRD_API }, 11 | } = getState(); 12 | return getJson(`${MOCKINGBIRD_API}/v2/label`, { query: { service } }) 13 | .then((response) => { 14 | if (!response || !Array.isArray(response)) { 15 | throw new Error(); 16 | } 17 | return dispatch(fetchSuccess(response)); 18 | }) 19 | .catch(() => dispatch(fetchFail())); 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/infrastructure/pluralize/index.ts: -------------------------------------------------------------------------------- 1 | /** Получить склонение слова */ 2 | export function plural( 3 | count: number, 4 | singular: string, 5 | plural1: string, 6 | plural2: string = plural1 7 | ) { 8 | const hasFloatingPoint = count % 1 !== 0; 9 | if (hasFloatingPoint) return plural2; 10 | const c1 = Math.abs(count % 100); 11 | if (c1 >= 5 && c1 <= 20) return plural2; 12 | const c2 = Math.abs(c1 % 10); 13 | if (c2 === 1) return singular; 14 | if (c2 >= 2 && c2 <= 4) return plural1; 15 | return plural2; 16 | } 17 | 18 | /** Получить отформатированное число со склоняемым словом */ 19 | export default function pluralize( 20 | count: number, 21 | singular: string, 22 | plural1: string, 23 | plural2: string = plural1 24 | ) { 25 | return `${count} ${plural(count, singular, plural1, plural2)}`; 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/layers/pages/mocks/HTTPMock.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate } from '@tramvai/module-router'; 3 | import type { THTTPMock } from 'src/mockingbird/models/mock/types'; 4 | import { getPathMock } from 'src/mockingbird/paths'; 5 | import Item from './Item'; 6 | 7 | type Props = { 8 | serviceId: string; 9 | item: THTTPMock; 10 | }; 11 | 12 | export default function HTTPMock({ serviceId, item }: Props) { 13 | const { id, name, method, path, pathPattern, scope, times, labels } = item; 14 | const navigate = useNavigate(getPathMock(serviceId, id, 'http')); 15 | return ( 16 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /backend/dataAccess/src/main/scala/ru/tinkoff/tcb/dataaccess/DAO.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.dataaccess 2 | 3 | import ru.tinkoff.tcb.generic.RootOptionFields 4 | 5 | trait DAO[F[_], T] { 6 | type Query 7 | type Patch 8 | type Sort 9 | 10 | def findOne(query: Query): F[Option[T]] 11 | 12 | def findChunk(query: Query, offset: Int, size: Int, sort: Sort*): F[Vector[T]] 13 | 14 | def insert(t: T): F[Int] 15 | 16 | def insertMany(ts: Seq[T]): F[Int] 17 | 18 | def update(query: Query, patches: Patch*): F[UpdateResult] 19 | 20 | def update(query: Query, patches: Iterable[Patch]): F[UpdateResult] 21 | 22 | def update(entity: T)(implicit rof: RootOptionFields[T]): F[UpdateResult] 23 | 24 | def updateIf(query: Query, entity: T)(implicit rof: RootOptionFields[T]): F[UpdateResult] 25 | 26 | def delete(query: Query): F[Long] 27 | } 28 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/refinedchimney/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils 2 | 3 | import eu.timepit.refined.api.Refined 4 | import eu.timepit.refined.api.Validate 5 | import eu.timepit.refined.refineV 6 | import io.scalaland.chimney.PartialTransformer 7 | import io.scalaland.chimney.Transformer 8 | import io.scalaland.chimney.partial 9 | 10 | package object refinedchimney { 11 | implicit def extractRefined[Type, Refinement]: Transformer[Type Refined Refinement, Type] = 12 | _.value 13 | 14 | implicit def validateRefined[Type, Refinement](implicit 15 | validate: Validate.Plain[Type, Refinement] 16 | ): PartialTransformer[Type, Type Refined Refinement] = 17 | PartialTransformer[Type, Type Refined Refinement] { value => 18 | partial.Result.fromEitherString(refineV[Refinement](value)) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/sources/ui/SourceItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate } from '@tramvai/module-router'; 3 | import { Paper, Text, Box } from '@mantine/core'; 4 | import { getPathSource } from 'src/mockingbird/paths'; 5 | import type { Source } from '../types'; 6 | 7 | type Props = { 8 | item: Source; 9 | serviceId: string; 10 | }; 11 | 12 | export default function SourceItem({ item, serviceId }: Props) { 13 | const { name, description } = item; 14 | const onNavigate = useNavigate(getPathSource(serviceId, name)); 15 | return ( 16 | 17 | 18 | {name} 19 | 20 | 21 | 22 | {description} 23 | 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /backend/dataAccess/src/test/scala/ru/tinkoff/tcb/bson/RoundRobinSpec.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.bson 2 | 3 | import scala.util.matching.Regex 4 | 5 | import oolong.bson.* 6 | import oolong.bson.given 7 | import org.scalactic.Equality 8 | import org.scalatest.TryValues 9 | import org.scalatest.funsuite.AnyFunSuite 10 | import org.scalatest.matchers.should.Matchers 11 | 12 | class RoundRobinSpec extends AnyFunSuite with Matchers with TryValues { 13 | implicit private val regexEquality: Equality[Regex] = 14 | (a: Regex, b: Any) => 15 | b match { 16 | case rb: Regex => a.regex == rb.regex 17 | case _ => false 18 | } 19 | 20 | test("Regex serialization") { 21 | val group = "<(?[a-zA-Z0-9]+)>".r 22 | 23 | val sut = BsonDecoder[Regex].fromBson(group.bson) 24 | 25 | sut.success.value shouldEqual group 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/examples/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} %msg%n 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/layers/pages/mocks/ScenarioItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate } from '@tramvai/module-router'; 3 | import type { TScenarioMock } from 'src/mockingbird/models/mock/types'; 4 | import { getPathMock } from 'src/mockingbird/paths'; 5 | import Item from './Item'; 6 | 7 | type Props = { 8 | serviceId: string; 9 | item: TScenarioMock; 10 | }; 11 | 12 | export default function ScenarioItem({ serviceId, item }: Props) { 13 | const { id, name, source, destination = '', scope, times, labels } = item; 14 | const navigate = useNavigate(getPathMock(serviceId, id, 'scenario')); 15 | return ( 16 | ${destination}` : ''}`} 19 | scope={scope} 20 | times={times} 21 | labels={labels} 22 | onClick={navigate} 23 | /> 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/ResourceRequest.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.model 2 | 3 | import io.circe.Decoder 4 | import io.circe.Encoder 5 | import oolong.bson.* 6 | import oolong.bson.given 7 | import sttp.tapir.Schema 8 | 9 | import ru.tinkoff.tcb.utils.crypto.AES 10 | 11 | final case class ResourceRequest( 12 | url: SecureString.Type, 13 | method: HttpMethod, 14 | headers: Map[String, SecureString.Type] = Map(), 15 | body: Option[SecureString.Type] = None, 16 | ) derives Decoder, 17 | Encoder, 18 | Schema 19 | 20 | object ResourceRequest { 21 | implicit def resourceRequestBsonEncoder(implicit aes: AES): BsonEncoder[ResourceRequest] = 22 | BsonEncoder.derived 23 | 24 | implicit def resourceRequestBsonDecoder(implicit aes: AES): BsonDecoder[ResourceRequest] = 25 | BsonDecoder.derived 26 | } 27 | -------------------------------------------------------------------------------- /backend/integrationTests/k6/v2/definitions/test_service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package market_data; 3 | 4 | enum InstrumentIDKind { 5 | ID_1 = 0; 6 | ID_2 = 1; 7 | } 8 | 9 | message PricesRequest { 10 | string instrument_id = 1; 11 | optional InstrumentIDKind instrument_id_kind = 3; 12 | } 13 | 14 | message PricesResponse { 15 | enum Code { 16 | OK = 0; 17 | ERROR = 1; 18 | } 19 | 20 | string instrument_id = 1; 21 | string tracking_id = 3; 22 | Code code = 4; 23 | optional string error = 100; 24 | } 25 | 26 | service OTCMarketDataService { 27 | rpc PricesUnary (PricesRequest) returns (PricesResponse) {} 28 | rpc PricesClient (stream PricesRequest) returns (PricesResponse) {} 29 | rpc PricesServer (PricesRequest) returns (stream PricesResponse) {} 30 | rpc PricesBidi (stream PricesRequest) returns (stream PricesResponse) {} 31 | } 32 | -------------------------------------------------------------------------------- /backend/integrationTests/k6/v4/contentType/definitions/test_service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package market_data; 3 | 4 | enum InstrumentIDKind { 5 | ID_1 = 0; 6 | ID_2 = 1; 7 | } 8 | 9 | message PricesRequest { 10 | string instrument_id = 1; 11 | optional InstrumentIDKind instrument_id_kind = 3; 12 | } 13 | 14 | message PricesResponse { 15 | enum Code { 16 | OK = 0; 17 | ERROR = 1; 18 | } 19 | 20 | string instrument_id = 1; 21 | string tracking_id = 3; 22 | Code code = 4; 23 | optional string error = 100; 24 | } 25 | 26 | service OTCMarketDataService { 27 | rpc PricesUnary (PricesRequest) returns (PricesResponse) {} 28 | rpc PricesClient (stream PricesRequest) returns (PricesResponse) {} 29 | rpc PricesServer (PricesRequest) returns (stream PricesResponse) {} 30 | rpc PricesBidi (stream PricesRequest) returns (stream PricesResponse) {} 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/sources/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useActions, useStoreSelector } from '@tramvai/state'; 3 | import { selectorAsIs } from 'src/mockingbird/infrastructure/helpers/state'; 4 | import store from '../reducers'; 5 | import { fetchAction } from '../actions'; 6 | import type { Source } from '../types'; 7 | 8 | export function useSources(serviceId: string) { 9 | const { status, sources } = useStoreSelector(store, selectorAsIs); 10 | const fetchSources = useActions(fetchAction); 11 | useEffect(() => { 12 | if (status === 'none') { 13 | fetchSources(serviceId); 14 | } 15 | }, [fetchSources, serviceId, status]); 16 | return sources.map(mapSelectItem); 17 | } 18 | 19 | export function mapSelectItem({ name: value, description }: Source) { 20 | return { 21 | label: value, 22 | description, 23 | value, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | ru.tinkoff.tcb { 2 | 3 | server { 4 | interface = 0.0.0.0 5 | port = 8228 6 | allowedOrigins = [ 7 | "http://localhost", 8 | "http://localhost:3000", 9 | "http://localhost:8228" 10 | ] 11 | vertx { 12 | maxFormAttributeSize = 262144 13 | compressionSupported = true 14 | } 15 | } 16 | 17 | proxy { 18 | excludedRequestHeaders = [] 19 | excludedResponseHeaders = [] 20 | insecureHosts = [] 21 | logOutgoingRequests = false 22 | disableAutoDecompressForRaw = true 23 | httpVersion = HTTP_1_1 24 | } 25 | 26 | event { 27 | fetchInterval = "5 s" 28 | reloadInterval = "1 m" 29 | } 30 | 31 | tracing { 32 | required = ["correlationId"] 33 | incomingHeaders = {} 34 | outcomingHeaders = { 35 | "correlationId" = "X-Correlation-ID" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/destinations/ui/DestinationItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate } from '@tramvai/module-router'; 3 | import { Box, Paper, Text } from '@mantine/core'; 4 | import { getPathDestination } from 'src/mockingbird/paths'; 5 | import type { Destination } from '../types'; 6 | 7 | type Props = { 8 | item: Destination; 9 | serviceId: string; 10 | }; 11 | 12 | export default function DestinationItem({ item, serviceId }: Props) { 13 | const { name, description } = item; 14 | const onNavigate = useNavigate(getPathDestination(serviceId, name)); 15 | return ( 16 | 17 | 18 | {name} 19 | 20 | 21 | 22 | {description} 23 | 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /backend/compose-test.yml.native.tmpl: -------------------------------------------------------------------------------- 1 | services: 2 | mongo: 3 | image: mongo 4 | environment: 5 | - MONGO_INITDB_DATABASE=mockingbird 6 | healthcheck: 7 | test: echo 'db.runCommand("ping").ok' | mongosh mongo:27017/test --quiet 8 | interval: 10s 9 | timeout: 10s 10 | retries: 5 11 | start_period: 10s 12 | networks: 13 | - app-tier 14 | 15 | mockingbird: 16 | image: "${MOCKINGBIRD_IMAGE}" 17 | depends_on: 18 | mongo: 19 | condition: service_healthy 20 | volumes: 21 | # Read the docs about secrets 22 | - ./secrets-for-test.conf:/opt/mockingbird-native/conf/secrets.conf 23 | command: -server -Xms256m -Xmx256m -XX:MaxDirectMemorySize=128m -Dconfig.file=/opt/mockingbird-native/qa.conf -Dlog.level=DEBUG -Dlog4j.formatMsgNoLookups=true 24 | networks: 25 | - app-tier 26 | networks: 27 | app-tier: 28 | driver: bridge 29 | -------------------------------------------------------------------------------- /backend/compose-test.yml.jvm.tmpl: -------------------------------------------------------------------------------- 1 | services: 2 | mongo: 3 | image: mongo 4 | environment: 5 | - MONGO_INITDB_DATABASE=mockingbird 6 | healthcheck: 7 | test: echo 'db.runCommand("ping").ok' | mongosh mongo:27017/test --quiet 8 | interval: 10s 9 | timeout: 10s 10 | retries: 5 11 | start_period: 10s 12 | networks: 13 | - app-tier 14 | 15 | mockingbird: 16 | image: "${MOCKINGBIRD_IMAGE}" 17 | depends_on: 18 | mongo: 19 | condition: service_healthy 20 | volumes: 21 | # Read the docs about secrets 22 | - ./secrets-for-test.conf:/opt/mockingbird/conf/secrets.conf 23 | environment: 24 | - JAVA_OPTS=-server -Xms256m -Xmx256m -XX:MaxDirectMemorySize=128m -Dconfig.resource=qa.conf -Dlog.level=DEBUG -Dlog4j.formatMsgNoLookups=true 25 | networks: 26 | - app-tier 27 | 28 | networks: 29 | app-tier: 30 | driver: bridge 31 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/labels/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { createReducer, createEvent } from '@tramvai/state'; 2 | 3 | type LabelsState = { 4 | status: 'none' | 'loading' | 'complete' | 'error'; 5 | labels: string[]; 6 | }; 7 | 8 | const storeName = 'labelsState'; 9 | const initialState: LabelsState = { 10 | status: 'none', 11 | labels: [], 12 | }; 13 | 14 | export const fetchSuccess = createEvent('FETCH_LABELS_SUCCESS'); 15 | export const fetchFail = createEvent('FETCH_LABELS_FAIL'); 16 | export const setLoading = createEvent('SET_LOADING_LABELS'); 17 | 18 | export default createReducer(storeName, initialState) 19 | .on(fetchSuccess, (state, labels) => ({ 20 | labels, 21 | status: 'complete', 22 | })) 23 | .on(fetchFail, (state) => ({ 24 | ...state, 25 | status: 'error', 26 | })) 27 | .on(setLoading, (state) => ({ 28 | ...state, 29 | status: 'loading', 30 | })); 31 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/json/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils 2 | 3 | import io.circe.Json 4 | import io.circe.JsonNumber 5 | import io.circe.JsonObject 6 | 7 | package object json { 8 | val json2StringFolder: Json.Folder[String] = 9 | new Json.Folder[String] { 10 | override val onNull: String = "" 11 | 12 | override def onBoolean(value: Boolean): String = value.toString 13 | 14 | override def onNumber(value: JsonNumber): String = value.toString 15 | 16 | override def onString(value: String): String = value 17 | 18 | override def onArray(value: Vector[Json]): String = value.map(_.foldWith(this)).mkString(",") 19 | 20 | override def onObject(value: JsonObject): String = Json.fromJsonObject(value).noSpaces 21 | } 22 | 23 | object JObject { 24 | def unapplySeq(jo: JsonObject): Some[Seq[(String, Json)]] = Some(jo.toList) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/CreateServiceRequest.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.api.request 2 | 3 | import eu.timepit.refined.api.Refined 4 | import eu.timepit.refined.boolean.* 5 | import eu.timepit.refined.collection.* 6 | import eu.timepit.refined.string.* 7 | import eu.timepit.refined.types.string.NonEmptyString 8 | import io.circe.Decoder 9 | import io.circe.Encoder 10 | import io.circe.refined.* 11 | import sttp.tapir.Schema 12 | import sttp.tapir.codec.refined.* 13 | 14 | import ru.tinkoff.tcb.generic.PropSubset 15 | import ru.tinkoff.tcb.mockingbird.model.Service 16 | 17 | final case class CreateServiceRequest( 18 | suffix: String Refined And[NonEmpty, MatchesRegex["[\\w-]+"]], 19 | name: NonEmptyString 20 | ) derives Decoder, 21 | Encoder, 22 | Schema 23 | 24 | object CreateServiceRequest { 25 | implicitly[PropSubset[CreateServiceRequest, Service]] 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/destinations/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useActions, useStoreSelector } from '@tramvai/state'; 3 | import { selectorAsIs } from 'src/mockingbird/infrastructure/helpers/state'; 4 | import store from '../reducers'; 5 | import { fetchAction } from '../actions'; 6 | import type { Destination } from '../types'; 7 | 8 | export function useDestinations(serviceId: string) { 9 | const { status, destinations } = useStoreSelector(store, selectorAsIs); 10 | const fetchDestinations = useActions(fetchAction); 11 | useEffect(() => { 12 | if (status === 'none') { 13 | fetchDestinations(serviceId); 14 | } 15 | }, [fetchDestinations, serviceId, status]); 16 | return destinations.map(mapSelectItem); 17 | } 18 | 19 | export function mapSelectItem({ name: value, description }: Destination) { 20 | return { 21 | label: value, 22 | description, 23 | value, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import LanguageDetector from 'i18next-browser-languagedetector'; 3 | import { initReactI18next } from 'react-i18next'; 4 | import ru from './translations/ru.json'; 5 | import en from './translations/en.json'; 6 | 7 | i18n 8 | // detect user language 9 | // learn more: https://github.com/i18next/i18next-browser-languageDetector 10 | .use(LanguageDetector) 11 | // pass the i18n instance to react-i18next. 12 | .use(initReactI18next) 13 | // init i18next 14 | // for all options read: https://www.i18next.com/overview/configuration-options 15 | .init({ 16 | resources: { 17 | ru: { 18 | translation: ru, 19 | }, 20 | en: { 21 | translation: en, 22 | }, 23 | }, 24 | fallbackLng: 'en', 25 | interpolation: { 26 | escapeValue: false, // not needed for react! 27 | }, 28 | debug: false, 29 | }); 30 | 31 | export default i18n; 32 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/models/mockCreate/reducers/store.ts: -------------------------------------------------------------------------------- 1 | import { createReducer, createEvent } from '@tramvai/state'; 2 | 3 | export type CreateMockState = { 4 | status: 'none' | 'loading' | 'complete' | 'error'; 5 | }; 6 | 7 | const storeName = 'createMockState'; 8 | const initialState: CreateMockState = { 9 | status: 'none', 10 | }; 11 | 12 | export const createSuccess = createEvent('CREATE_MOCK_SUCCESS'); 13 | export const createFail = createEvent('CREATE_MOCK_FAIL'); 14 | export const setLoading = createEvent('SET_LOADING_MOCK'); 15 | export const reset = createEvent('RESET_CREATE_MOCK'); 16 | 17 | const reducer = createReducer(storeName, initialState) 18 | .on(createSuccess, () => ({ 19 | status: 'complete', 20 | })) 21 | .on(createFail, () => ({ 22 | status: 'error', 23 | })) 24 | .on(setLoading, () => ({ 25 | status: 'loading', 26 | })); 27 | 28 | reducer.on(reset, () => initialState); 29 | 30 | export default reducer; 31 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/service/reducers/store.ts: -------------------------------------------------------------------------------- 1 | import { createReducer, createEvent } from '@tramvai/state'; 2 | import type { Service } from '../types'; 3 | 4 | export type ServiceState = { 5 | status: 'none' | 'loading' | 'complete' | 'error'; 6 | data?: Service; 7 | }; 8 | 9 | const storeName = 'serviceState'; 10 | const initialState: ServiceState = { 11 | status: 'none', 12 | }; 13 | 14 | export const fetchSuccess = createEvent('FETCH_SERVICE_SUCCESS'); 15 | export const fetchFail = createEvent('FETCH_SERVICE_FAIL'); 16 | export const setLoading = createEvent('SET_LOADING_SERVICE'); 17 | 18 | export default createReducer(storeName, initialState) 19 | .on(fetchSuccess, (state, data) => ({ 20 | data, 21 | status: 'complete', 22 | })) 23 | .on(fetchFail, (state) => ({ 24 | ...state, 25 | status: 'error', 26 | })) 27 | .on(setLoading, (state) => ({ 28 | ...state, 29 | status: 'loading', 30 | })); 31 | -------------------------------------------------------------------------------- /backend/mockingbird-api/src/main/scala/ru/tinkoff/tcb/mockingbird/api/MetricsHttp.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.api 2 | 3 | import io.micrometer.prometheus.PrometheusMeterRegistry 4 | import io.vertx.ext.web.Route 5 | import io.vertx.ext.web.Router 6 | import sttp.tapir.PublicEndpoint 7 | import sttp.tapir.server.vertx.zio.VertxZioServerInterpreter 8 | import sttp.tapir.ztapir.* 9 | 10 | import ru.tinkoff.tcb.mockingbird.wldRuntime 11 | 12 | class MetricsHttp(registry: PrometheusMeterRegistry) { 13 | private val metricsEndpoint: PublicEndpoint[Unit, Unit, String, Any] = 14 | endpoint.get.in("metrics").out(stringBody) 15 | 16 | val http: Router => Route = 17 | VertxZioServerInterpreter() 18 | .route(metricsEndpoint.zServerLogic[WLD](_ => ZIO.succeed(registry.scrape())))(using wldRuntime) 19 | } 20 | 21 | object MetricsHttp { 22 | def live: RLayer[PrometheusMeterRegistry, MetricsHttp] = ZLayer.fromFunction(new MetricsHttp(_)) 23 | } 24 | -------------------------------------------------------------------------------- /backend/dataAccess/src/test/scala/ru/tinkoff/tcb/bson/enumeratum/BsonEnumSpec.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.bson.enumeratum 2 | 3 | import oolong.bson.* 4 | import org.mongodb.scala.bson.* 5 | import org.scalatest.funspec.AnyFunSpec 6 | import org.scalatest.matchers.should.Matchers 7 | 8 | class BsonEnumSpec extends AnyFunSpec with Matchers { 9 | describe("BSON serdes") { 10 | 11 | describe("deserialisation") { 12 | 13 | it("should work with valid values") { 14 | val bsonValue: BsonValue = BsonString("A") 15 | BsonDecoder[Dummy].fromBson(bsonValue).get shouldBe Dummy.A 16 | } 17 | 18 | it("should fail with invalid values") { 19 | val strBsonValue: BsonValue = BsonString("D") 20 | val intBsonValue: BsonValue = BsonInt32(2) 21 | 22 | BsonDecoder[Dummy].fromBson(strBsonValue).toOption shouldBe None 23 | BsonDecoder[Dummy].fromBson(intBsonValue).toOption shouldBe None 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/HttpMethod.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.model 2 | 3 | import enumeratum.* 4 | import enumeratum.EnumEntry.Uppercase 5 | import sttp.tapir.codec.enumeratum.TapirCodecEnumeratum 6 | 7 | import ru.tinkoff.tcb.bson.enumeratum.BsonEnum 8 | import ru.tinkoff.tcb.mockingbird.config.PureconfigEnum 9 | 10 | sealed trait HttpMethod extends EnumEntry with Uppercase 11 | object HttpMethod 12 | extends Enum[HttpMethod] 13 | with BsonEnum[HttpMethod] 14 | with TapirCodecEnumeratum 15 | with CirceEnum[HttpMethod] 16 | with PureconfigEnum[HttpMethod] { 17 | case object Get extends HttpMethod 18 | case object Post extends HttpMethod 19 | case object Head extends HttpMethod 20 | case object Options extends HttpMethod 21 | case object Patch extends HttpMethod 22 | case object Put extends HttpMethod 23 | case object Delete extends HttpMethod 24 | 25 | val values = findValues 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/components/PageHeader/PageHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate } from '@tramvai/module-router'; 3 | import { Box, Flex, Title, Button, Space } from '@mantine/core'; 4 | 5 | interface Props { 6 | title: string; 7 | backText?: string; 8 | backPath?: string; 9 | right?: React.ReactNode; 10 | } 11 | 12 | export default function PageHeader(props: Props) { 13 | const { title, backText, backPath = '', right } = props; 14 | const navigate = useNavigate(backPath); 15 | const backButton = backText && backPath && ( 16 | 19 | ); 20 | return ( 21 | 22 | {backButton || } 23 | 24 | {title} 25 | {right} 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/sources/actions/index.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@tramvai/core'; 2 | import { getJson } from 'src/mockingbird/infrastructure/request'; 3 | import { setLoading, fetchSuccess, fetchFail, reset } from '../reducers'; 4 | 5 | export const fetchAction = createAction({ 6 | name: 'FETCH_SOURCES_ACTION', 7 | fn: ({ dispatch, getState }, service) => { 8 | dispatch(setLoading()); 9 | const { 10 | environment: { MOCKINGBIRD_API }, 11 | } = getState(); 12 | return getJson(`${MOCKINGBIRD_API}/v3/source`, { query: { service } }) 13 | .then((response) => { 14 | if (!response || !Array.isArray(response)) { 15 | throw new Error(); 16 | } 17 | dispatch(fetchSuccess(response)); 18 | }) 19 | .catch(() => dispatch(fetchFail())); 20 | }, 21 | }); 22 | 23 | export const resetAction = createAction({ 24 | name: 'RESET_SOURCES_ACTION', 25 | fn: ({ dispatch }) => dispatch(reset()), 26 | }); 27 | -------------------------------------------------------------------------------- /backend/dataAccess/src/main/scala/ru/tinkoff/tcb/bson/PatchGenerator.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.bson 2 | 3 | import scala.jdk.CollectionConverters.* 4 | 5 | import oolong.bson.* 6 | import org.mongodb.scala.bson.* 7 | 8 | import ru.tinkoff.tcb.generic.RootOptionFields 9 | 10 | object PatchGenerator { 11 | def mkPatch[T: BsonEncoder]( 12 | entity: T 13 | )(implicit rof: RootOptionFields[T]): (Option[BsonValue], BsonDocument) = { 14 | val bsonEntity = entity.bson.asDocument() 15 | 16 | val fieldToUnset = rof.fields -- bsonEntity.keySet().asScala 17 | val unsetElements = fieldToUnset.toVector.map(_ -> BsonString("")) 18 | val unset = 19 | if (unsetElements.nonEmpty) Seq("$unset" -> BsonDocument(unsetElements)) else Seq.empty 20 | 21 | bsonEntity.getFieldOpt("_id") -> BsonDocument("$set" -> bsonEntity.tap(_.remove("_id"))).tap { doc => 22 | unset.foreach { case (key, value) => 23 | doc.append(key, value) 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/edsl/src/main/scala/ru/tinkoff/tcb/mockingbird/edsl/model/http.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.edsl.model 2 | 3 | import enumeratum.* 4 | 5 | import ru.tinkoff.tcb.mockingbird.edsl.model.Check.* 6 | 7 | sealed trait HttpMethod extends EnumEntry 8 | object HttpMethod extends Enum[HttpMethod] { 9 | val values = findValues 10 | case object Get extends HttpMethod 11 | case object Post extends HttpMethod 12 | case object Delete extends HttpMethod 13 | } 14 | 15 | final case class HttpResponse(code: Int, body: Option[String], headers: Seq[(String, String)]) 16 | 17 | final case class HttpRequest( 18 | method: HttpMethod, 19 | path: String, 20 | body: Option[String] = None, 21 | headers: Seq[(String, String)] = Seq.empty, 22 | query: Seq[(String, String)] = Seq.empty, 23 | ) 24 | 25 | final case class HttpResponseExpected( 26 | code: Option[CheckInteger] = None, 27 | body: Option[Check] = None, 28 | headers: Seq[(String, CheckString)] = Seq.empty, 29 | ) 30 | -------------------------------------------------------------------------------- /backend/utils/src/main/scala/ru/tinkoff/tcb/utils/base64/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils 2 | 3 | import java.nio.charset.Charset 4 | import java.util.Base64 5 | 6 | package object base64 { 7 | private val Utf8 = Charset.forName("UTF-8") 8 | 9 | implicit class StringB64Ops(private val text: String) extends AnyVal { 10 | def toBase64Bytes: Array[Byte] = Base64.getEncoder.encode(text.getBytes(Utf8)) 11 | 12 | def toBase64String: String = new String(toBase64Bytes, Utf8) 13 | 14 | def bytesFromBase64String: Array[Byte] = text.getBytes(Utf8).fromBase64Bytes 15 | 16 | def fromBase64String: String = new String(bytesFromBase64String, Utf8) 17 | } 18 | 19 | implicit class ByteArrB64ops(private val bytes: Array[Byte]) extends AnyVal { 20 | def fromBase64Bytes: Array[Byte] = Base64.getDecoder.decode(bytes) 21 | 22 | def toBase64Bytes: Array[Byte] = Base64.getEncoder.encode(bytes) 23 | 24 | def toBase64String: String = new String(toBase64Bytes, Utf8) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/destinations/actions/index.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from '@tramvai/core'; 2 | import { getJson } from 'src/mockingbird/infrastructure/request'; 3 | import { setLoading, fetchSuccess, fetchFail, reset } from '../reducers'; 4 | 5 | export const fetchAction = createAction({ 6 | name: 'FETCH_DESTINATIONS_ACTION', 7 | fn: ({ dispatch, getState }, service) => { 8 | dispatch(setLoading()); 9 | const { 10 | environment: { MOCKINGBIRD_API }, 11 | } = getState(); 12 | return getJson(`${MOCKINGBIRD_API}/v3/destination`, { query: { service } }) 13 | .then((response) => { 14 | if (!response || !Array.isArray(response)) { 15 | throw new Error(); 16 | } 17 | return dispatch(fetchSuccess(response)); 18 | }) 19 | .catch(() => dispatch(fetchFail())); 20 | }, 21 | }); 22 | 23 | export const resetAction = createAction({ 24 | name: 'RESET_DESTINATIONS_ACTION', 25 | fn: ({ dispatch }) => dispatch(reset()), 26 | }); 27 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/services/reducers/store.ts: -------------------------------------------------------------------------------- 1 | import { createReducer, createEvent } from '@tramvai/state'; 2 | import type { Service } from '../../service/types'; 3 | 4 | export type ServicesState = { 5 | status: 'none' | 'loading' | 'complete' | 'error'; 6 | services: Service[]; 7 | }; 8 | 9 | const storeName = 'servicesState'; 10 | const initialState: ServicesState = { 11 | status: 'none', 12 | services: [], 13 | }; 14 | 15 | export const fetchSuccess = createEvent('FETCH_SERVICES_SUCCESS'); 16 | export const fetchFail = createEvent('FETCH_SERVICES_FAIL'); 17 | export const setLoading = createEvent('SET_LOADING_SERVICES'); 18 | 19 | export default createReducer(storeName, initialState) 20 | .on(fetchSuccess, (state, services) => ({ 21 | services, 22 | status: 'complete', 23 | })) 24 | .on(fetchFail, (state) => ({ 25 | ...state, 26 | status: 'error', 27 | })) 28 | .on(setLoading, (state) => ({ 29 | ...state, 30 | status: 'loading', 31 | })); 32 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/grpc/UniversalHandlerRegistry.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.grpc 2 | 3 | import io.grpc.HandlerRegistry 4 | import io.grpc.ServerCallHandler 5 | import io.grpc.ServerMethodDefinition 6 | 7 | final case class UniversalHandlerRegistry0(method: ServerMethodDefinition[?, ?]) extends HandlerRegistry { 8 | 9 | override def lookupMethod(methodName: String, authority: String): ServerMethodDefinition[?, ?] = 10 | method 11 | } 12 | 13 | final case class UniversalHandlerRegistry(handler: ServerCallHandler[Array[Byte], Array[Byte]]) 14 | extends HandlerRegistry { 15 | 16 | override def lookupMethod(methodName: String, authority: String): ServerMethodDefinition[Array[Byte], Array[Byte]] = { 17 | val methodNameArray = methodName.split("/") 18 | val serviceName = methodNameArray(0) 19 | val method = methodNameArray(1) 20 | ServerMethodDefinition.create(Method.byteMethod(serviceName, method), handler) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addDependencyTreePlugin 2 | addSbtPlugin("com.github.sbt" % "sbt-git" % "2.0.1") 3 | addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") 4 | addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.16") 5 | addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.3") 6 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") 7 | addSbtPlugin("ch.epfl.scala" % "sbt-missinglink" % "0.3.6") 8 | addSbtPlugin("com.thesamet" % "sbt-protoc" % "1.0.7") 9 | addSbtPlugin("org.scalameta" % "sbt-native-image" % "0.3.4") 10 | addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.0.1") 11 | addSbtPlugin("org.wartremover" % "sbt-wartremover" % "3.4.1") 12 | addSbtPlugin("org.wartremover" % "sbt-wartremover-contrib" % "2.3.3", "1.0", "2.12") 13 | 14 | libraryDependencies += 15 | "com.thesamet.scalapb.zio-grpc" %% "zio-grpc-codegen" % "0.6.1" 16 | 17 | libraryDependencies += 18 | "com.spotify" % "missinglink-core" % "0.2.11" -------------------------------------------------------------------------------- /backend/circe-utils/src/test/scala/ru/tinkoff/tcb/utils/circe/JsonOpsTest.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils.circe 2 | 3 | import io.circe.literal.* 4 | import org.scalatest.Assertion 5 | import org.scalatest.matchers.should.Matchers 6 | import org.scalatest.refspec.RefSpec 7 | 8 | class JsonOpsTest extends RefSpec with Matchers { 9 | object `A Json instance` { 10 | object `called with camelizeKeys` { 11 | def `should just work`: Assertion = { 12 | val doc = json""" 13 | { 14 | "first_name" : "foo", 15 | "last_name" : "bar", 16 | "parent" : { 17 | "first_name" : "baz", 18 | "last_name" : "bazz" 19 | } 20 | } 21 | """ 22 | 23 | val res = doc.camelizeKeys 24 | 25 | res shouldBe json"""{ 26 | "firstName" : "foo", 27 | "lastName" : "bar", 28 | "parent" : { 29 | "firstName" : "baz", 30 | "lastName" : "bazz" 31 | } 32 | }""" 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/GrpcConnectionType.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.model 2 | 3 | import enumeratum.* 4 | import enumeratum.EnumEntry.UpperSnakecase 5 | import sttp.tapir.codec.enumeratum.TapirCodecEnumeratum 6 | 7 | import ru.tinkoff.tcb.bson.enumeratum.BsonEnum 8 | 9 | sealed trait GrpcConnectionType extends EnumEntry with UpperSnakecase { 10 | val haveUnaryOutput: Boolean = true 11 | } 12 | 13 | object GrpcConnectionType 14 | extends Enum[GrpcConnectionType] 15 | with CirceEnum[GrpcConnectionType] 16 | with BsonEnum[GrpcConnectionType] 17 | with TapirCodecEnumeratum { 18 | 19 | case object Unary extends GrpcConnectionType 20 | case object ClientStreaming extends GrpcConnectionType 21 | case object ServerStreaming extends GrpcConnectionType { 22 | override val haveUnaryOutput = false 23 | } 24 | case object BidiStreaming extends GrpcConnectionType { 25 | override val haveUnaryOutput = false 26 | } 27 | 28 | val values = findValues 29 | } 30 | -------------------------------------------------------------------------------- /backend/mockingbird/src/test/resources/nested.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package utp.stock_service.v1; 4 | 5 | message GetStocksRequest { 6 | repeated string offer_codes = 1; 7 | int32 offset = 2; 8 | int32 limit = 3 ; 9 | } 10 | 11 | message GetStocksResponse { 12 | enum StockKinds { 13 | SK_UNKNOWN = 0; 14 | SK_GROWTH = 1; 15 | SK_INCOME = 2; 16 | SK_VALUE = 3; 17 | SK_BLUE_CHIP = 4; 18 | } 19 | message Stock { 20 | int64 quantity = 1; 21 | string name = 2; 22 | } 23 | message Stocks { 24 | repeated Stock stocks = 1; 25 | } 26 | message Event { 27 | enum Code { 28 | C_OK = 0; 29 | C_ERROR = 1; 30 | } 31 | message Data { 32 | string value = 1; 33 | } 34 | message Error { 35 | string info = 1; 36 | } 37 | Code code = 1; 38 | oneof payload { 39 | Data data = 4; 40 | Error error = 100; 41 | } 42 | } 43 | } 44 | 45 | service StockService { 46 | rpc GetStocks(GetStocksRequest) returns (GetStocksResponse); 47 | } 48 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/PersistentState.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.model 2 | 3 | import java.time.Instant 4 | import java.util.UUID 5 | 6 | import io.circe.Decoder 7 | import io.circe.Encoder 8 | import io.circe.Json 9 | import oolong.bson.* 10 | import oolong.bson.given 11 | import oolong.bson.meta.QueryMeta 12 | import oolong.bson.meta.queryMeta 13 | import sttp.tapir.Schema 14 | 15 | import ru.tinkoff.tcb.circe.bson.* 16 | import ru.tinkoff.tcb.protocol.schema.* 17 | import ru.tinkoff.tcb.utils.id.SID 18 | 19 | final case class PersistentState( 20 | id: SID[PersistentState], 21 | data: Json, 22 | created: Instant 23 | ) derives BsonDecoder, 24 | BsonEncoder, 25 | Decoder, 26 | Encoder, 27 | Schema 28 | 29 | object PersistentState { 30 | inline given QueryMeta[PersistentState] = queryMeta(_.id -> "_id") 31 | 32 | def fresh: Task[PersistentState] = 33 | ZIO.clockWith(_.instant).map(PersistentState(SID(UUID.randomUUID().toString), Json.obj(), _)) 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/sources/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { createReducer, createEvent } from '@tramvai/state'; 2 | import type { Source } from '../types'; 3 | 4 | type State = { 5 | status: 'none' | 'loading' | 'complete' | 'error'; 6 | sources: Source[]; 7 | }; 8 | 9 | const storeName = 'sourcesState'; 10 | const initialState: State = { 11 | status: 'none', 12 | sources: [], 13 | }; 14 | 15 | export const fetchSuccess = createEvent('FETCH_SOURCES_SUCCESS'); 16 | export const fetchFail = createEvent('FETCH_SOURCES_FAIL'); 17 | export const reset = createEvent('RESET_SOURCES'); 18 | export const setLoading = createEvent('SET_LOADING_SOURCES'); 19 | 20 | export default createReducer(storeName, initialState) 21 | .on(fetchSuccess, (state, sources) => ({ 22 | sources, 23 | status: 'complete', 24 | })) 25 | .on(fetchFail, (state) => ({ 26 | ...state, 27 | status: 'error', 28 | })) 29 | .on(reset, () => initialState) 30 | .on(setLoading, (state) => ({ 31 | ...state, 32 | status: 'loading', 33 | })); 34 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/misc/Substitute.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.misc 2 | 3 | import scala.xml.Node 4 | 5 | import io.circe.Json 6 | 7 | import ru.tinkoff.tcb.utils.sandboxing.GraalJsSandbox 8 | import ru.tinkoff.tcb.utils.transformation.json.* 9 | import ru.tinkoff.tcb.utils.transformation.xml.* 10 | 11 | /** 12 | * "Proof that B can be substituted into A 13 | */ 14 | trait Substitute[A, B] { 15 | def substitute(a: A, b: B): A 16 | } 17 | 18 | object Substitute { 19 | def apply[A, B](implicit subst: Substitute[A, B]): Substitute[A, B] = subst 20 | 21 | implicit def jsonSJson(implicit sandbox: GraalJsSandbox): Substitute[Json, Json] = (a: Json, b: Json) => 22 | a.substitute(b).useAsIs 23 | implicit val jsonSNode: Substitute[Json, Node] = (a: Json, b: Node) => a.substitute(b) 24 | implicit def nodeSJson(implicit sandbox: GraalJsSandbox): Substitute[Node, Json] = (a: Node, b: Json) => 25 | a.substitute(b).useAsIs 26 | implicit val nodeSNode: Substitute[Node, Node] = (a: Node, b: Node) => a.substitute(b) 27 | } 28 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/EventDestinationRequest.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.model 2 | 3 | import io.circe.Decoder 4 | import io.circe.Encoder 5 | import io.circe.Json 6 | import oolong.bson.* 7 | import oolong.bson.given 8 | import sttp.tapir.Schema 9 | 10 | import ru.tinkoff.tcb.circe.bson.given 11 | import ru.tinkoff.tcb.protocol.schema.* 12 | import ru.tinkoff.tcb.utils.crypto.AES 13 | 14 | final case class EventDestinationRequest( 15 | url: SecureString.Type, 16 | method: HttpMethod, 17 | headers: Map[String, SecureString.Type], 18 | body: Option[Json], 19 | stringifybody: Option[Boolean], 20 | encodeBase64: Option[Boolean] 21 | ) derives Decoder, 22 | Encoder, 23 | Schema 24 | 25 | object EventDestinationRequest { 26 | implicit def eventDestinationRequestBsonEncoder(implicit aes: AES): BsonEncoder[EventDestinationRequest] = 27 | BsonEncoder.derived 28 | 29 | implicit def eventDestinationRequestBsonDecoder(implicit aes: AES): BsonDecoder[EventDestinationRequest] = 30 | BsonDecoder.derived 31 | } 32 | -------------------------------------------------------------------------------- /backend/edsl/src/main/scala/ru/tinkoff/tcb/mockingbird/edsl/interpreter/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.edsl 2 | 3 | import sttp.client4.* 4 | import sttp.model.Uri 5 | import sttp.model.Uri.QuerySegment 6 | 7 | import ru.tinkoff.tcb.mockingbird.edsl.model.HttpMethod.* 8 | import ru.tinkoff.tcb.mockingbird.edsl.model.HttpRequest 9 | 10 | package object interpreter { 11 | def makeUri(host: Uri, req: HttpRequest): Uri = 12 | host 13 | .addPath(req.path.split("/").filter(_.nonEmpty)) 14 | .addQuerySegments(req.query.map { case (k, v) => QuerySegment.KeyValue(k, v) }) 15 | 16 | def buildRequest(host: Uri, m: HttpRequest): Request[String] = { 17 | val initialRequest = emptyRequest.response(asStringAlways) 18 | var req = m.body.fold(initialRequest)(initialRequest.body) 19 | req = m.headers.foldLeft(req) { case (r, (k, v)) => r.header(k, v, DuplicateHeaderBehavior.Replace) } 20 | val url = makeUri(host, m) 21 | m.method match { 22 | case Delete => req.delete(url) 23 | case Get => req.get(url) 24 | case Post => req.post(url) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/string/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils.transformation 2 | 3 | import scala.xml.Node 4 | 5 | import io.circe.Json 6 | 7 | import ru.tinkoff.tcb.utils.sandboxing.GraalJsSandbox 8 | import ru.tinkoff.tcb.utils.transformation.json.* 9 | 10 | package object string { 11 | implicit final class StringTransformations(private val s: String) extends AnyVal { 12 | def isTemplate: Boolean = 13 | CodeRx.findFirstIn(s).isDefined || SubstRx.findFirstIn(s).isDefined 14 | 15 | def substitute(jvalues: Json, xvalues: Node)(implicit sandbox: GraalJsSandbox): String = 16 | if (SubstRx.findFirstIn(s).isDefined || CodeRx.findFirstIn(s).isDefined) 17 | Json.fromString(s).substitute(jvalues).map(_.substitute(xvalues)).useAsIs.asString.getOrElse(s) 18 | else s 19 | 20 | def substitute(values: Json)(implicit sandbox: GraalJsSandbox): String = 21 | if (SubstRx.findFirstIn(s).isDefined || CodeRx.findFirstIn(s).isDefined) 22 | Json.fromString(s).substitute(values).useAsIs.asString.getOrElse(s) 23 | else s 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/utils/src/test/scala/ru/tinkoff/tcb/utils/ExtStringOpsSpec.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | import ru.tinkoff.tcb.utils.string.* 7 | 8 | class ExtStringOpsSpec extends AnyFunSuite with Matchers { 9 | test("camel2Underscore") { 10 | "peka".camel2Underscore shouldBe "peka" 11 | "Peka".camel2Underscore shouldBe "peka" 12 | "pekaNamePshhh".camel2Underscore shouldBe "peka_name_pshhh" 13 | "PekaNamePshhh".camel2Underscore shouldBe "peka_name_pshhh" 14 | } 15 | 16 | test("underscore2Camel") { 17 | "peka".underscore2Camel shouldBe "peka" 18 | "Peka".underscore2Camel shouldBe "peka" 19 | "peka_name_pshhh".underscore2Camel shouldBe "pekaNamePshhh" 20 | "PEKA".underscore2Camel shouldBe "peka" 21 | } 22 | 23 | test("underscore2UpperCamel") { 24 | "peka".underscore2UpperCamel shouldBe "Peka" 25 | "Peka".underscore2UpperCamel shouldBe "Peka" 26 | "peka_name_pshhh".underscore2UpperCamel shouldBe "PekaNamePshhh" 27 | "PEKA".underscore2UpperCamel shouldBe "Peka" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/components/form/Select/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { Control } from 'react-hook-form'; 3 | import { useController } from 'react-hook-form'; 4 | import { Select as SelectMantine } from '@mantine/core'; 5 | import type { SelectProps } from '@mantine/core'; 6 | import { extractError } from 'src/mockingbird/infrastructure/helpers/forms'; 7 | 8 | export type Props = Omit & { 9 | options: SelectProps['data']; 10 | name: string; 11 | control: Control; 12 | }; 13 | 14 | export default function Select(props: Props) { 15 | const { name, control, required = false, options, defaultValue = '' } = props; 16 | const { field, formState, fieldState } = useController({ 17 | name, 18 | control, 19 | rules: { 20 | required, 21 | }, 22 | defaultValue, 23 | }); 24 | const uiError = 25 | extractError(name, formState.errors) || fieldState.error?.message; 26 | return ( 27 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/destinations/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { createReducer, createEvent } from '@tramvai/state'; 2 | import type { Destination } from '../types'; 3 | 4 | export type State = { 5 | status: 'none' | 'loading' | 'complete' | 'error'; 6 | destinations: Destination[]; 7 | }; 8 | 9 | const storeName = 'destinationsState'; 10 | const initialState: State = { 11 | status: 'none', 12 | destinations: [], 13 | }; 14 | 15 | export const fetchSuccess = createEvent( 16 | 'FETCH_DESTINATIONS_SUCCESS' 17 | ); 18 | export const fetchFail = createEvent('FETCH_DESTINATIONS_FAIL'); 19 | export const reset = createEvent('RESET_DESTINATIONS'); 20 | export const setLoading = createEvent('SET_LOADING_DESTINATIONS'); 21 | 22 | export default createReducer(storeName, initialState) 23 | .on(fetchSuccess, (state, destinations) => ({ 24 | destinations, 25 | status: 'complete', 26 | })) 27 | .on(fetchFail, (state) => ({ 28 | ...state, 29 | status: 'error', 30 | })) 31 | .on(reset, () => initialState) 32 | .on(setLoading, (state) => ({ 33 | ...state, 34 | status: 'loading', 35 | })); 36 | -------------------------------------------------------------------------------- /backend/mockingbird-api/src/main/scala/ru/tinkoff/tcb/mockingbird/api/input/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.api 2 | 3 | import sttp.model.Header 4 | import sttp.model.Method 5 | import sttp.model.QueryParams 6 | import sttp.tapir.* 7 | 8 | import ru.tinkoff.tcb.mockingbird.model.HttpMethod 9 | import ru.tinkoff.tcb.mockingbird.model.RequestBody 10 | 11 | package object input { 12 | private[api] type ExecInput = (HttpMethod, String, Map[String, String], Seq[(String, Seq[String])]) 13 | private[api] type ExecInputB = (HttpMethod, String, Map[String, String], Seq[(String, Seq[String])], RequestBody) 14 | 15 | private[api] val execInput: EndpointInput[ExecInput] = 16 | extractFromRequest(_.method) 17 | .map(m => HttpMethod.withNameInsensitive(m.method))(m => Method.unsafeApply(m.entryName)) 18 | .and(paths.map(_.mkString("/", "/", ""))(_.split("/").to(List))) 19 | .and( 20 | extractFromRequest(_.headers) 21 | .map(_.map(h => h.name -> h.value).to(Map))(_.map { case (name, value) => Header(name, value) }.to(Seq)) 22 | ) 23 | .and(queryParams.map(_.ps)(QueryParams(_))) 24 | } 25 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/protocol/log.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.protocol 2 | 3 | import eu.timepit.refined.api.RefType 4 | import sttp.model.Header 5 | import sttp.model.Part 6 | import tofu.logging.LogRenderer 7 | import tofu.logging.Loggable 8 | import tofu.logging.derivation.loggable 9 | import tofu.syntax.logRenderer.* 10 | 11 | object log { 12 | implicit val headerLoggable: Loggable[Header] = new Loggable[Header] { 13 | override def fields[I, V, R, S](a: Header, i: I)(implicit r: LogRenderer[I, V, R, S]): R = 14 | i.sub("name")(Loggable.stringValue.putValue(a.name, _: V)) |+| 15 | r.sub("value", i)(Loggable.stringValue.putValue(a.value, _: V)) 16 | 17 | override def putValue[I, V, R, S](a: Header, v: V)(implicit r: LogRenderer[I, V, R, S]): S = v.dict(fields(a, _: I)) 18 | 19 | override def logShow(a: Header): String = a.toString() 20 | } 21 | 22 | implicit def partLoggable[T: Loggable]: Loggable[Part[T]] = loggable.instance[Part[T]] 23 | 24 | implicit def refinedLoggable[T: Loggable, P, F[_, _]: RefType]: Loggable[F[T, P]] = 25 | Loggable[T].contramap(RefType[F].unwrap) 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/components/form/Input/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { Control } from 'react-hook-form'; 3 | import { useController } from 'react-hook-form'; 4 | import { TextInput } from '@mantine/core'; 5 | import type { TextInputProps } from '@mantine/core'; 6 | import { extractError } from 'src/mockingbird/infrastructure/helpers/forms'; 7 | 8 | type Props = TextInputProps & { 9 | name: string; 10 | control: Control; 11 | }; 12 | 13 | export function Input(props: Props) { 14 | const { 15 | name, 16 | label, 17 | control, 18 | required = false, 19 | disabled = false, 20 | ...restProps 21 | } = props; 22 | const ctrl = useController({ 23 | name, 24 | control, 25 | rules: { 26 | required, 27 | }, 28 | defaultValue: '', 29 | }); 30 | const uiError = 31 | extractError(name, ctrl.formState.errors) || ctrl.fieldState.error?.message; 32 | return ( 33 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /backend/circe-utils/src/test/scala/ru/tinkoff/tcb/utils/circe/MergerSpec.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils.circe 2 | 3 | import io.circe.literal.* 4 | import org.scalatest.Assertion 5 | import org.scalatest.matchers.should.Matchers 6 | import org.scalatest.refspec.RefSpec 7 | 8 | class MergerSpec extends RefSpec with Matchers { 9 | object `smart merge` { 10 | object `of arrays` { 11 | def `should shrink long arrays`: Assertion = { 12 | val j1 = json"""{"a": [0, 1, 2]}""" 13 | val j2 = json"""{"a": [0, 3]}""" 14 | 15 | j1 +: j2 shouldBe j2 16 | } 17 | 18 | def `should update and expand`: Assertion = { 19 | val j1 = json"""{"a": [0, 1, 2]}""" 20 | val j2 = json"""{"a": [0, 1, 4, 7]}""" 21 | 22 | j1 +: j2 shouldBe j2 23 | } 24 | } 25 | 26 | object `of objects` { 27 | def `should replace sub-object fields`: Assertion = { 28 | val j1 = json"""{"a": {"c": 1, "d": 2, "e": 3}, "b": 1}""" 29 | val j2 = json"""{"a": {"c": 11, "d": 22}}""" 30 | 31 | j1 +: j2 shouldBe json"""{"a": {"c": 11, "d": 22, "e": 3}, "b": 1}""" 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/dataAccess/src/main/scala/ru/tinkoff/tcb/bson/enumeratum/values/EnumHandler.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.bson.enumeratum.values 2 | 3 | import enumeratum.values.* 4 | import oolong.bson.BsonDecoder 5 | import oolong.bson.BsonEncoder 6 | import org.mongodb.scala.bson.* 7 | 8 | object EnumHandler { 9 | 10 | /** 11 | * Returns a BSONReader for the provided ValueEnum based on the given base BSONReader for the Enum's value type 12 | */ 13 | def reader[ValueType, EntryType <: ValueEnumEntry[ValueType]]( 14 | `enum`: ValueEnum[ValueType, EntryType] 15 | )(implicit 16 | baseBsonReader: BsonDecoder[ValueType] 17 | ): BsonDecoder[EntryType] = 18 | (value: BsonValue) => baseBsonReader.fromBson(value).map(`enum`.withValue) 19 | 20 | /** 21 | * Returns a BSONWriter for the provided ValueEnum based on the given base BSONWriter for the Enum's value type 22 | */ 23 | def writer[ValueType, EntryType <: ValueEnumEntry[ValueType]]( 24 | `enum`: ValueEnum[ValueType, EntryType] 25 | )(implicit 26 | baseBsonWriter: BsonEncoder[ValueType] 27 | ): BsonEncoder[EntryType] = 28 | (value: EntryType) => value.value.bson 29 | } 30 | -------------------------------------------------------------------------------- /backend/.scalafix.conf: -------------------------------------------------------------------------------- 1 | rules = [ 2 | LeakingImplicitClassVal, 3 | NoValInForComprehension, 4 | ProcedureSyntax 5 | DisableSyntax, 6 | OrganizeImports 7 | ] 8 | 9 | DisableSyntax.regex = [ 10 | { 11 | id = "mapUnit" 12 | pattern = "\\.map\\(_\\s*=>\\s*\\(\\)\\)" 13 | message = "Use .void" 14 | }, { 15 | id = mouseAny 16 | pattern = "import mouse\\.any\\._" 17 | message = "Use scala.util.chaining" 18 | }, { 19 | id = utilsResourceManagement 20 | pattern = "import ru\\.tinkoff\\.tcb\\.utils\\.rm\\._" 21 | message = "Use scala.util.Using" 22 | },{ 23 | id = mapAs 24 | pattern = "\\.map\\(_\\s*=>\\s*[\\w\\d\\.\"\\(\\)]+\\)" 25 | message = "Use .as" 26 | }, { 27 | id = catsImplicits 28 | pattern = "import cats\\.implicits" 29 | message = "Use granular imports" 30 | }, { 31 | id = zioClock 32 | pattern = "Instant.now" 33 | message = "Use ZIO.clockWith(_.instant)" 34 | } 35 | ] 36 | 37 | OrganizeImports { 38 | groups = [ 39 | "re:(javax?|scala)\\.", 40 | "*", 41 | "ru.tinkoff." 42 | ] 43 | expandRelative = true 44 | importsOrder = SymbolsFirst 45 | targetDialect = Auto 46 | } -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/Scope.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.model 2 | 3 | import enumeratum.* 4 | import enumeratum.EnumEntry.Lowercase 5 | import mouse.option.* 6 | import oolong.bson.* 7 | import oolong.bson.given 8 | import sttp.tapir.codec.enumeratum.TapirCodecEnumeratum 9 | import tofu.logging.Loggable 10 | 11 | import ru.tinkoff.tcb.bson.enumeratum.BsonEnum 12 | 13 | sealed abstract class Scope(val priority: Int) extends EnumEntry with Lowercase 14 | 15 | object Scope extends Enum[Scope] with BsonEnum[Scope] with TapirCodecEnumeratum with CirceEnum[Scope] { 16 | case object Persistent extends Scope(0) 17 | case object Ephemeral extends Scope(1) 18 | case object Countdown extends Scope(2) 19 | 20 | val values = findValues 21 | 22 | implicit val scopeLoggable: Loggable[Scope] = Loggable.stringValue.contramap(_.toString) 23 | 24 | implicit val scopeBsonEncoder: BsonEncoder[Scope] = BsonEncoder[Int].beforeWrite(_.priority) 25 | 26 | implicit val scopeBsonDecoder: BsonDecoder[Scope] = 27 | BsonDecoder[Int].afterReadTry(p => values.find(_.priority == p).toTry(new Exception(s"No Scope with priority $p"))) 28 | } 29 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | mongo_express: 4 | image: "mongo-express:1.0.0-alpha.4" 5 | restart: always 6 | ports: 7 | - "3001:8081" 8 | networks: 9 | - app-tier 10 | mongo: 11 | image: mongo 12 | restart: always 13 | environment: 14 | - MONGO_INITDB_DATABASE=mockingbird 15 | ports: 16 | - "27017:27017" 17 | networks: 18 | - app-tier 19 | migration: 20 | image: mongo 21 | networks: 22 | - app-tier 23 | volumes: 24 | - ./backend/migrations/grpc-migration.js:/migrations/grpc-migration.js 25 | command: mongosh mongo/mockingbird /migrations/grpc-migration.js 26 | 27 | mock: 28 | image: "ghcr.io/tinkoff/mockingbird:3.10.0-native" 29 | ports: 30 | - "8228:8228" 31 | - "9000:9000" 32 | volumes: 33 | # Read the docs about secrets 34 | - ./secrets.conf:/opt/mockingbird-native/conf/secrets.conf 35 | networks: 36 | - app-tier 37 | command: -server -Xms256m -Xmx256m -XX:MaxDirectMemorySize=128m -Dconfig.file=/opt/mockingbird-native/qa.conf -Dlog.level=DEBUG -Dlog4j.formatMsgNoLookups=true 38 | networks: 39 | app-tier: 40 | driver: bridge 41 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/services/reducers/createStore.ts: -------------------------------------------------------------------------------- 1 | import { createReducer, createEvent } from '@tramvai/state'; 2 | import i18n from 'src/mockingbird/i18n'; 3 | 4 | export type ServiceState = { 5 | status: 'none' | 'loading' | 'complete' | 'error'; 6 | id?: string; 7 | errorMessage?: string; 8 | }; 9 | 10 | const storeName = 'createServiceState'; 11 | const initialState: ServiceState = { 12 | status: 'none', 13 | }; 14 | 15 | export const createSuccess = createEvent('CREATE_SERVICE_SUCCESS'); 16 | export const createFail = createEvent('CREATE_SERVICE_FAIL'); 17 | export const setLoading = createEvent('SET_LOADING_CREATE_SERVICE'); 18 | export const reset = createEvent('RESET_CREATE_SERVICE'); 19 | 20 | const reducer = createReducer(storeName, initialState) 21 | .on(createSuccess, (state, id) => ({ 22 | id, 23 | status: 'complete', 24 | })) 25 | .on(createFail, (state, e) => ({ 26 | status: 'error', 27 | errorMessage: (e && e.body && e.body.error) || i18n.t('services.tryAgain'), 28 | })) 29 | .on(setLoading, () => ({ 30 | status: 'loading', 31 | })); 32 | 33 | reducer.on(reset, () => initialState); 34 | 35 | export default reducer; 36 | -------------------------------------------------------------------------------- /backend/dataAccess/src/test/scala/ru/tinkoff/tcb/bson/PatchGeneratorSpec.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.bson 2 | 3 | import oolong.bson.* 4 | import oolong.bson.given 5 | import org.mongodb.scala.bson.BsonDocument 6 | import org.scalatest.funsuite.AnyFunSuite 7 | import org.scalatest.matchers.should.Matchers 8 | 9 | final case class TestEntity(_id: String, name: String, externalKey: Option[Int]) derives BsonDecoder, BsonEncoder 10 | 11 | class PatchGeneratorSpec extends AnyFunSuite with Matchers { 12 | test("Generate update with Some") { 13 | val entity = TestEntity("42", "name", Some(442)) 14 | 15 | val (_, patch) = PatchGenerator.mkPatch(entity) 16 | 17 | patch shouldBe BsonDocument( 18 | "$set" -> BsonDocument( 19 | "name" -> "name", 20 | "externalKey" -> 442 21 | ) 22 | ) 23 | } 24 | 25 | test("Generate update with None") { 26 | val entity = TestEntity("42", "name", None) 27 | 28 | val (_, patch) = PatchGenerator.mkPatch(entity) 29 | 30 | patch shouldBe BsonDocument( 31 | "$set" -> BsonDocument( 32 | "name" -> "name" 33 | ), 34 | "$unset" -> BsonDocument( 35 | "externalKey" -> "" 36 | ) 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/EventSourceRequest.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.model 2 | 3 | import io.circe.Decoder 4 | import io.circe.Encoder 5 | import oolong.bson.* 6 | import oolong.bson.given 7 | import sttp.tapir.Schema 8 | 9 | import ru.tinkoff.tcb.protocol.bson.* 10 | import ru.tinkoff.tcb.protocol.json.* 11 | import ru.tinkoff.tcb.protocol.schema.* 12 | import ru.tinkoff.tcb.utils.circe.optics.JsonOptic 13 | import ru.tinkoff.tcb.utils.crypto.AES 14 | 15 | final case class EventSourceRequest( 16 | url: SecureString.Type, 17 | method: HttpMethod, 18 | headers: Map[String, SecureString.Type], 19 | body: Option[SecureString.Type], 20 | jenumerate: Option[JsonOptic], 21 | jextract: Option[JsonOptic], 22 | bypassCodes: Option[Set[Int]], 23 | jstringdecode: Boolean = false 24 | ) derives Decoder, 25 | Encoder, 26 | Schema 27 | 28 | object EventSourceRequest { 29 | implicit def eventSourceRequestBsonEncoder(implicit aes: AES): BsonEncoder[EventSourceRequest] = 30 | BsonEncoder.derived 31 | 32 | implicit def eventSourceRequestBsonDecoder(implicit aes: AES): BsonDecoder[EventSourceRequest] = 33 | BsonDecoder.derived 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/infrastructure/notifications/utils.ts: -------------------------------------------------------------------------------- 1 | import { extractError } from 'src/mockingbird/infrastructure/helpers/state'; 2 | import i18n from 'src/mockingbird/i18n'; 3 | import { addToast } from './store/store'; 4 | 5 | export function getSuccessToast(title: string) { 6 | return addToast( 7 | successToast({ 8 | title, 9 | }) 10 | ); 11 | } 12 | 13 | export function getCreateErrorToast(e: any) { 14 | return getErrorToast(i18n.t('notifications.createError'), e); 15 | } 16 | 17 | export function getUpdateErrorToast(e: any) { 18 | return getErrorToast(i18n.t('notifications.updateError'), e); 19 | } 20 | 21 | export function getRemoveErrorToast(e: any) { 22 | return getErrorToast(i18n.t('notifications.removeError'), e); 23 | } 24 | 25 | function getErrorToast(title: string, e: any) { 26 | return addToast( 27 | errorToast({ 28 | title, 29 | description: e ? extractError(e) : undefined, 30 | }) 31 | ); 32 | } 33 | 34 | function successToast(item: any) { 35 | return { 36 | type: 'success', 37 | timer: 3000, 38 | ...item, 39 | }; 40 | } 41 | 42 | function errorToast(item: any) { 43 | return { 44 | type: 'error', 45 | timer: 5000, 46 | ...item, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/id/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils 2 | 3 | import java.util.UUID 4 | import scala.util.Success 5 | 6 | import io.circe.KeyDecoder 7 | import io.circe.KeyEncoder 8 | import oolong.bson.BsonKeyDecoder 9 | import oolong.bson.BsonKeyEncoder 10 | import sttp.tapir.Codec 11 | import sttp.tapir.CodecFormat 12 | 13 | package object id { 14 | type SID[T] = SID.Aux[T] 15 | 16 | object SID extends IDCompanion[String] { 17 | def random[T]: SID[T] = SID(UUID.randomUUID().toString) 18 | 19 | implicit def keyEncoderForSID[T]: KeyEncoder[SID[T]] = identity[SID[T]](_) 20 | implicit def keyDecoderForSID[T]: KeyDecoder[SID[T]] = (key: String) => Some(SID(key)) 21 | 22 | implicit def bsonKeyEncoderForSID[T]: BsonKeyEncoder[SID[T]] = (t: SID[T]) => t 23 | implicit def bsonKeyDecoderForSID[T]: BsonKeyDecoder[SID[T]] = (value: String) => Success(apply(value)) 24 | 25 | implicit def codecForSID[T]: Codec[String, SID[T], CodecFormat.TextPlain] = 26 | Codec.string.map(SID[T])(identity) 27 | } 28 | 29 | type ID[T] = ID.Aux[T] 30 | 31 | object ID extends IDCompanion[Int] 32 | 33 | type LID[T] = LID.Aux[T] 34 | 35 | object LID extends IDCompanion[Long] 36 | } 37 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/tofu/logging/impl/ZUniversalLogging.scala: -------------------------------------------------------------------------------- 1 | package tofu.logging.impl 2 | 3 | import org.slf4j.LoggerFactory 4 | import org.slf4j.Marker 5 | import tofu.logging.LoggedValue 6 | import tofu.logging.Logging 7 | 8 | class ZUniversalLogging(name: String) extends Logging[UIO] { 9 | override def write(level: Logging.Level, message: String, values: LoggedValue*): UIO[Unit] = 10 | ZIO.succeed { 11 | val logger = LoggerFactory.getLogger(name) 12 | if (UniversalLogging.enabled(level, logger)) 13 | UniversalLogging.write(level, logger, message, values) 14 | } 15 | 16 | override def writeMarker(level: Logging.Level, message: String, marker: Marker, values: LoggedValue*): UIO[Unit] = 17 | ZIO.succeed { 18 | val logger = LoggerFactory.getLogger(name) 19 | if (UniversalLogging.enabled(level, logger)) 20 | UniversalLogging.writeMarker(level, logger, marker, message, values) 21 | } 22 | 23 | override def writeCause(level: Logging.Level, message: String, cause: Throwable, values: LoggedValue*): UIO[Unit] = 24 | ZIO.succeed { 25 | val logger = LoggerFactory.getLogger(name) 26 | if (UniversalLogging.enabled(level, logger)) 27 | UniversalLogging.writeCause(level, logger, cause, message, values) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/layers/layout/Header.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { connect } from '@tramvai/state'; 3 | import { Container, Title, Flex } from '@mantine/core'; 4 | import { getJson } from 'src/infrastructure/request'; 5 | import { LanguagePicker } from 'src/mockingbird/components/Language'; 6 | import { Shadow } from './Shadow'; 7 | 8 | type Props = { 9 | assetsPrefix: string; 10 | }; 11 | 12 | function Header({ assetsPrefix }: Props) { 13 | const [version, setVersion] = useState(null); 14 | useEffect(() => { 15 | getJson(`${assetsPrefix}version.json`) 16 | .then((res) => { 17 | if (res && res.version) setVersion(res.version); 18 | }) 19 | .catch(() => null); 20 | }, [assetsPrefix]); 21 | const title = version ? `Mockingbird v${version}` : 'Mockingbird'; 22 | return ( 23 | 24 | 25 | 26 | 27 | {title} 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | const mapProps = ({ environment: { ASSETS_PREFIX: assetsPrefix } }) => ({ 37 | assetsPrefix, 38 | }); 39 | 40 | export default connect([], mapProps)(Header); 41 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/UpdateDestinationConfigurationRequest.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.api.request 2 | 3 | import cats.data.NonEmptyVector 4 | import io.circe.Decoder 5 | import io.circe.Encoder 6 | import sttp.tapir.Schema 7 | import sttp.tapir.Schema.annotations.description 8 | 9 | import ru.tinkoff.tcb.generic.PropSubset 10 | import ru.tinkoff.tcb.mockingbird.model.DestinationConfiguration 11 | import ru.tinkoff.tcb.mockingbird.model.EventDestinationRequest 12 | import ru.tinkoff.tcb.mockingbird.model.ResourceRequest 13 | import ru.tinkoff.tcb.protocol.schema.* 14 | 15 | final case class UpdateDestinationConfigurationRequest( 16 | @description("Configuration description") 17 | description: String, 18 | service: String, 19 | @description("Request specification") 20 | request: EventDestinationRequest, 21 | @description("Initializer specification") 22 | init: Option[NonEmptyVector[ResourceRequest]], 23 | @description("Finalizer specification") 24 | shutdown: Option[NonEmptyVector[ResourceRequest]], 25 | ) derives Decoder, 26 | Encoder, 27 | Schema 28 | 29 | object UpdateDestinationConfigurationRequest { 30 | implicitly[PropSubset[UpdateDestinationConfigurationRequest, DestinationConfiguration]] 31 | } 32 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/dal/ServiceDAO.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.dal 2 | 3 | import scala.annotation.implicitNotFound 4 | import scala.util.matching.Regex 5 | 6 | import oolong.bson.given 7 | import org.mongodb.scala.MongoCollection 8 | import org.mongodb.scala.bson.BsonDocument 9 | 10 | import ru.tinkoff.tcb.mockingbird.model.Service 11 | import ru.tinkoff.tcb.mongo.DAOBase 12 | import ru.tinkoff.tcb.mongo.MongoDAO 13 | 14 | @implicitNotFound("Could not find an instance of ServiceDAO for ${F}") 15 | trait ServiceDAO[F[_]] extends MongoDAO[F, Service] { 16 | def getServiceFor(path: String): F[Option[Service]] 17 | def getServiceFor(pattern: Regex): F[Option[Service]] 18 | } 19 | 20 | object ServiceDAO 21 | 22 | class ServiceDAOImpl(collection: MongoCollection[BsonDocument]) 23 | extends DAOBase[Service](collection) 24 | with ServiceDAO[Task] { 25 | override def getServiceFor(path: String): Task[Option[Service]] = 26 | findById(path.split('/').filter(_.nonEmpty).head) 27 | 28 | override def getServiceFor(pattern: Regex): Task[Option[Service]] = 29 | findById(pattern.regex.split('/').filter(_.nonEmpty).head) 30 | } 31 | 32 | object ServiceDAOImpl { 33 | val live: URLayer[MongoCollection[BsonDocument], ServiceDAO[Task]] = ZLayer.fromFunction(new ServiceDAOImpl(_)) 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/components/form/ToggleBlock/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import type { Control } from 'react-hook-form'; 3 | import { useController } from 'react-hook-form'; 4 | import type { SwitchProps } from '@mantine/core'; 5 | import { Switch } from '@mantine/core'; 6 | 7 | type Props = Omit & { 8 | name: string; 9 | control: Control; 10 | }; 11 | 12 | export default function ToggleBlock(props: Props) { 13 | const { 14 | name, 15 | label, 16 | control, 17 | labelPosition, 18 | required = false, 19 | defaultValue = false, 20 | ...restProps 21 | } = props; 22 | const { field } = useController({ 23 | name, 24 | control, 25 | rules: { 26 | required, 27 | }, 28 | defaultValue, 29 | }); 30 | const { onChange, value } = field; 31 | const handleChange = useCallback( 32 | (event) => { 33 | onChange(event.currentTarget.checked); 34 | }, 35 | [onChange] 36 | ); 37 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 38 | const { ref, ...fieldProps } = field; 39 | return ( 40 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/destination/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | parseJSON, 3 | stringifyJSON, 4 | } from 'src/mockingbird/infrastructure/utils/forms'; 5 | import i18n from 'src/mockingbird/i18n'; 6 | import { DEFAULT_REQUEST } from './refs'; 7 | import type { Destination, DestinationFormData } from './types'; 8 | 9 | export function mapDestinationToFormData( 10 | data?: Destination 11 | ): DestinationFormData { 12 | if (!data) 13 | return { 14 | name: 'test_out', 15 | description: i18n.t('destination.inputDescription'), 16 | request: stringifyJSON(DEFAULT_REQUEST), 17 | init: stringifyJSON([]), 18 | shutdown: stringifyJSON([]), 19 | }; 20 | return { 21 | name: data.name, 22 | description: data.description, 23 | request: stringifyJSON(data.request), 24 | init: stringifyJSON(data.init, []), 25 | shutdown: stringifyJSON(data.shutdown, []), 26 | }; 27 | } 28 | 29 | export function mapFormDataToDestination( 30 | data: DestinationFormData, 31 | serviceId: string 32 | ): Destination { 33 | const { name, description } = data; 34 | return { 35 | name: name.trim(), 36 | description: description.trim(), 37 | service: serviceId, 38 | request: parseJSON(data.request), 39 | init: parseJSON(data.init, true), 40 | shutdown: parseJSON(data.shutdown, true), 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/dal/GrpcStubDAO.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.dal 2 | 3 | import scala.annotation.implicitNotFound 4 | 5 | import com.github.dwickern.macros.NameOf.* 6 | import org.mongodb.scala.MongoCollection 7 | import org.mongodb.scala.bson.BsonDocument 8 | import org.mongodb.scala.model.Indexes.* 9 | 10 | import ru.tinkoff.tcb.mockingbird.model.GrpcStub 11 | import ru.tinkoff.tcb.mongo.DAOBase 12 | import ru.tinkoff.tcb.mongo.MongoDAO 13 | 14 | @implicitNotFound("Could not find an instance of GrpcStubDAO for ${F}") 15 | trait GrpcStubDAO[F[_]] extends MongoDAO[F, GrpcStub] 16 | 17 | object GrpcStubDAO 18 | 19 | class GrpcStubDAOImpl(collection: MongoCollection[BsonDocument]) 20 | extends DAOBase[GrpcStub](collection) 21 | with GrpcStubDAO[Task] { 22 | def createIndexes: Task[Unit] = createIndex( 23 | ascending(nameOf[GrpcStub](_.methodDescriptionId), nameOf[GrpcStub](_.scope)) 24 | ) *> createIndex( 25 | descending(nameOf[GrpcStub](_.created)) 26 | ) *> createIndex( 27 | ascending(nameOf[GrpcStub](_.labels)) 28 | ) 29 | } 30 | 31 | object GrpcStubDAOImpl { 32 | val live = ZLayer { 33 | for { 34 | mc <- ZIO.service[MongoCollection[BsonDocument]] 35 | sd = new GrpcStubDAOImpl(mc) 36 | _ <- sd.createIndexes 37 | } yield sd.asInstanceOf[GrpcStubDAO[Task]] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/string/StringTransformationsSpec.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils.transformation.string 2 | 3 | import io.circe.Json 4 | import io.circe.syntax.* 5 | import org.scalatest.TryValues 6 | import org.scalatest.funsuite.AnyFunSuite 7 | import org.scalatest.matchers.should.Matchers 8 | 9 | import ru.tinkoff.tcb.mockingbird.config.JsSandboxConfig 10 | import ru.tinkoff.tcb.utils.sandboxing.GraalJsSandbox 11 | 12 | class StringTransformationsSpec extends AnyFunSuite with Matchers with TryValues { 13 | test("Substitute JSON") { 14 | implicit val sandbox: GraalJsSandbox = new GraalJsSandbox(JsSandboxConfig()) 15 | 16 | "${a}" `substitute` (Json.obj("a" := "test")) shouldBe "test" 17 | } 18 | 19 | test("Substitute XML") { 20 | implicit val sandbox: GraalJsSandbox = new GraalJsSandbox(JsSandboxConfig()) 21 | 22 | "${/a}".substitute(Json.Null, test) shouldBe "test" 23 | } 24 | 25 | test("isTemplate test") { 26 | "".isTemplate shouldBe false 27 | "{}".isTemplate shouldBe false 28 | "${}".isTemplate shouldBe false 29 | "${a}".isTemplate shouldBe true 30 | "${a.b}".isTemplate shouldBe true 31 | "${a.[0]}".isTemplate shouldBe true 32 | "${a.[0].b}".isTemplate shouldBe true 33 | 34 | "%{}".isTemplate shouldBe false 35 | "%{var a = 1}".isTemplate shouldBe true 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/.scalafmt.conf: -------------------------------------------------------------------------------- 1 | align = most 2 | align.openParenCallSite = false 3 | align.openParenDefnSite = false 4 | align.tokens = [ 5 | { code = "extends", owner = "Defn.(Class|Trait|Object)" } 6 | { code = "//", owner = ".*" } 7 | { code = "{", owner = "Template" } 8 | { code = "}", owner = "Template" } 9 | { code = "%", owner = "Term.ApplyInfix" } 10 | { code = "=>", owner = "Case" } 11 | { code = "%%",owner = "Term.ApplyInfix" } 12 | { code = "%%%",owner = "Term.ApplyInfix" } 13 | { code = "<-", owner = "Enumerator.Generator" } 14 | { code = "->", owner = "Term.ApplyInfix" } 15 | { code = "=", owner = "(Enumerator.Val|Defn.(Va(l|r)|Def|Type))" } 16 | ] 17 | continuationIndent.defnSite = 4 18 | docstrings.style = Asterisk 19 | encoding = UTF-8 20 | importSelectors = singleLine 21 | maxColumn = 120 22 | newlines.beforeTypeBounds = unfold 23 | newlines.avoidForSimpleOverflow = [tooLong, punct, slc] 24 | optIn.configStyleArguments = true 25 | project.git = true 26 | rewrite.rules = [ 27 | PreferCurlyFors 28 | RedundantBraces 29 | RedundantParens 30 | SortModifiers 31 | ] 32 | rewrite.sortModifiers.order = [ 33 | implicit 34 | final 35 | sealed 36 | abstract 37 | override 38 | private 39 | protected 40 | lazy 41 | open 42 | transparent 43 | inline 44 | infix 45 | opaque 46 | ] 47 | runner.dialect = "scala3" 48 | style = IntelliJ 49 | trailingCommas = preserve 50 | version=3.4.0 51 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/DestinationConfiguration.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.model 2 | 3 | import java.time.Instant 4 | 5 | import cats.data.NonEmptyVector 6 | import io.circe.Decoder 7 | import io.circe.Encoder 8 | import oolong.bson.* 9 | import oolong.bson.given 10 | import oolong.bson.meta.QueryMeta 11 | import oolong.bson.meta.queryMeta 12 | import sttp.tapir.Schema 13 | 14 | import ru.tinkoff.tcb.protocol.bson.* 15 | import ru.tinkoff.tcb.protocol.schema.* 16 | import ru.tinkoff.tcb.utils.crypto.AES 17 | import ru.tinkoff.tcb.utils.id.SID 18 | 19 | final case class DestinationConfiguration( 20 | name: SID[DestinationConfiguration], 21 | created: Instant, 22 | description: String, 23 | service: String, 24 | request: EventDestinationRequest, 25 | init: Option[NonEmptyVector[ResourceRequest]], 26 | shutdown: Option[NonEmptyVector[ResourceRequest]], 27 | ) derives Decoder, 28 | Encoder, 29 | Schema 30 | 31 | object DestinationConfiguration { 32 | inline given QueryMeta[DestinationConfiguration] = queryMeta(_.name -> "_id") 33 | 34 | implicit def destinationConfigurationBsonEncoder(implicit aes: AES): BsonEncoder[DestinationConfiguration] = 35 | BsonEncoder.derived 36 | 37 | implicit def destinationConfigurationBsonDecoder(implicit aes: AES): BsonDecoder[DestinationConfiguration] = 38 | BsonDecoder.derived 39 | } 40 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/SourceConfiguration.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.model 2 | 3 | import java.time.Instant 4 | 5 | import cats.data.NonEmptyVector 6 | import io.circe.Decoder 7 | import io.circe.Encoder 8 | import oolong.bson.* 9 | import oolong.bson.given 10 | import oolong.bson.meta.QueryMeta 11 | import oolong.bson.meta.queryMeta 12 | import sttp.tapir.Schema 13 | 14 | import ru.tinkoff.tcb.protocol.bson.* 15 | import ru.tinkoff.tcb.protocol.schema.* 16 | import ru.tinkoff.tcb.utils.crypto.AES 17 | import ru.tinkoff.tcb.utils.id.SID 18 | 19 | final case class SourceConfiguration( 20 | name: SID[SourceConfiguration], 21 | created: Instant, 22 | description: String, 23 | service: String, 24 | request: EventSourceRequest, 25 | init: Option[NonEmptyVector[ResourceRequest]], 26 | shutdown: Option[NonEmptyVector[ResourceRequest]], 27 | reInitTriggers: Option[NonEmptyVector[ResponseSpec]] 28 | ) derives Decoder, 29 | Encoder, 30 | Schema 31 | 32 | object SourceConfiguration { 33 | inline given QueryMeta[SourceConfiguration] = queryMeta(_.name -> "_id") 34 | 35 | implicit def sourceConfigurationBsonEncoder(implicit aes: AES): BsonEncoder[SourceConfiguration] = 36 | BsonEncoder.derived 37 | 38 | implicit def sourceConfigurationBsonDecoder(implicit aes: AES): BsonDecoder[SourceConfiguration] = 39 | BsonDecoder.derived 40 | } 41 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/components/form/InputCount/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import type { Control } from 'react-hook-form'; 3 | import { useController } from 'react-hook-form'; 4 | import type { NumberInputProps } from '@mantine/core'; 5 | import { NumberInput } from '@mantine/core'; 6 | import { extractError } from 'src/mockingbird/infrastructure/helpers/forms'; 7 | 8 | type Props = Omit & { 9 | name: string; 10 | control: Control; 11 | }; 12 | 13 | export default function InputCount(props: Props) { 14 | const { 15 | name, 16 | label, 17 | control, 18 | required = false, 19 | disabled = false, 20 | min, 21 | max, 22 | ...restProps 23 | } = props; 24 | const { field, formState, fieldState } = useController({ 25 | name, 26 | control, 27 | rules: { 28 | required, 29 | }, 30 | defaultValue: min, 31 | }); 32 | const { onChange } = field; 33 | const handleChange = useCallback((value) => onChange(value), [onChange]); 34 | const uiError = 35 | extractError(name, formState.errors) || fieldState.error?.message; 36 | return ( 37 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/CreateDestinationConfigurationRequest.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.api.request 2 | 3 | import cats.data.NonEmptyVector 4 | import io.circe.Decoder 5 | import io.circe.Encoder 6 | import sttp.tapir.Schema 7 | import sttp.tapir.Schema.annotations.description 8 | 9 | import ru.tinkoff.tcb.generic.PropSubset 10 | import ru.tinkoff.tcb.mockingbird.model.DestinationConfiguration 11 | import ru.tinkoff.tcb.mockingbird.model.EventDestinationRequest 12 | import ru.tinkoff.tcb.mockingbird.model.ResourceRequest 13 | import ru.tinkoff.tcb.protocol.schema.* 14 | import ru.tinkoff.tcb.utils.id.SID 15 | 16 | final case class CreateDestinationConfigurationRequest( 17 | @description("Unique configuration name") 18 | name: SID[DestinationConfiguration], 19 | @description("Configuration description") 20 | description: String, 21 | service: String, 22 | @description("Request specification") 23 | request: EventDestinationRequest, 24 | @description("Initializer specification") 25 | init: Option[NonEmptyVector[ResourceRequest]], 26 | @description("Finalizer specification") 27 | shutdown: Option[NonEmptyVector[ResourceRequest]] 28 | ) derives Decoder, 29 | Encoder, 30 | Schema 31 | 32 | object CreateDestinationConfigurationRequest { 33 | implicitly[PropSubset[CreateDestinationConfigurationRequest, DestinationConfiguration]] 34 | } 35 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/RequestBody.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.model 2 | 3 | import glass.Subset 4 | import glass.macros.GenSubset 5 | import sttp.model.Part 6 | import tofu.logging.Loggable 7 | import tofu.logging.derivation.derived 8 | import tofu.logging.derivation.loggable 9 | 10 | import ru.tinkoff.tcb.protocol.log.* 11 | 12 | sealed trait RequestBody derives Loggable 13 | object RequestBody 14 | 15 | case object AbsentRequestBody extends RequestBody { 16 | final val subset: Subset[RequestBody, AbsentRequestBody.type] = GenSubset[RequestBody, AbsentRequestBody.type] 17 | 18 | implicit val absentRequestBodyLoggable: Loggable[AbsentRequestBody.type] = Loggable.empty[AbsentRequestBody.type] 19 | } 20 | 21 | final case class SimpleRequestBody(binary: Array[Byte]) extends RequestBody { 22 | val value: String = new String(binary) 23 | } 24 | object SimpleRequestBody { 25 | final val subset: Subset[RequestBody, SimpleRequestBody] = GenSubset[RequestBody, SimpleRequestBody] 26 | 27 | implicit val simpleRequestBodyLoggable: Loggable[SimpleRequestBody] = 28 | Loggable.stringValue.contramap[SimpleRequestBody](_.value) 29 | } 30 | 31 | final case class MultipartRequestBody(value: Seq[Part[String]]) extends RequestBody derives Loggable 32 | object MultipartRequestBody { 33 | final val subset: Subset[RequestBody, MultipartRequestBody] = GenSubset[RequestBody, MultipartRequestBody] 34 | } 35 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/UpdateSourceConfigurationRequest.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.api.request 2 | 3 | import cats.data.NonEmptyVector 4 | import io.circe.Decoder 5 | import io.circe.Encoder 6 | import sttp.tapir.Schema 7 | import sttp.tapir.Schema.annotations.description 8 | 9 | import ru.tinkoff.tcb.generic.PropSubset 10 | import ru.tinkoff.tcb.mockingbird.model.EventSourceRequest 11 | import ru.tinkoff.tcb.mockingbird.model.ResourceRequest 12 | import ru.tinkoff.tcb.mockingbird.model.ResponseSpec 13 | import ru.tinkoff.tcb.mockingbird.model.SourceConfiguration 14 | import ru.tinkoff.tcb.protocol.schema.* 15 | 16 | final case class UpdateSourceConfigurationRequest( 17 | @description("Configuration description") 18 | description: String, 19 | service: String, 20 | @description("Request specification") 21 | request: EventSourceRequest, 22 | @description("Initializer specification") 23 | init: Option[NonEmptyVector[ResourceRequest]], 24 | @description("Finalizer specification") 25 | shutdown: Option[NonEmptyVector[ResourceRequest]], 26 | @description("Reinitialization triggers specification") 27 | reInitTriggers: Option[NonEmptyVector[ResponseSpec]] 28 | ) derives Decoder, 29 | Encoder, 30 | Schema 31 | 32 | object UpdateSourceConfigurationRequest { 33 | implicitly[PropSubset[UpdateSourceConfigurationRequest, SourceConfiguration]] 34 | } 35 | -------------------------------------------------------------------------------- /backend/edsl/src/test/scala/ru/tinkoff/tcb/mockingbird/examples/CatsFacts.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.examples 2 | 3 | import ru.tinkoff.tcb.mockingbird.edsl.ExampleSet 4 | import ru.tinkoff.tcb.mockingbird.edsl.model.* 5 | import ru.tinkoff.tcb.mockingbird.edsl.model.Check.* 6 | import ru.tinkoff.tcb.mockingbird.edsl.model.HttpMethod.* 7 | import ru.tinkoff.tcb.mockingbird.edsl.model.ValueMatcher.syntax.* 8 | 9 | class CatsFacts[HttpResponseR] extends ExampleSet[HttpResponseR] { 10 | 11 | override val name = "Examples of using ExampleSet" 12 | 13 | example("Getting a random fact about kittens")( 14 | for { 15 | _ <- describe("Send a GET request") 16 | resp <- sendHttp( 17 | method = Get, 18 | path = "/fact", 19 | headers = Seq("X-CSRF-TOKEN" -> "unEENxJqSLS02rji2GjcKzNLc0C0ySlWih9hSxwn") 20 | ) 21 | _ <- describe("The response contains a random fact obtained from the server") 22 | _ <- checkHttp( 23 | resp, 24 | HttpResponseExpected( 25 | code = Some(CheckInteger(200)), 26 | body = Some( 27 | CheckJsonObject( 28 | "fact" -> CheckJsonString("There are approximately 100 breeds of cat.".sample), 29 | "length" -> CheckJsonNumber(42.sample) 30 | ) 31 | ), 32 | headers = Seq("Content-Type" -> CheckString("application/json")) 33 | ) 34 | ) 35 | } yield () 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/source/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | parseJSON, 3 | stringifyJSON, 4 | } from 'src/mockingbird/infrastructure/utils/forms'; 5 | import i18n from 'src/mockingbird/i18n'; 6 | import { DEFAULT_REQUEST } from './refs'; 7 | import type { Source, SourceFormData } from './types'; 8 | 9 | export function mapSourceToFormData(data?: Source): SourceFormData { 10 | if (!data) 11 | return { 12 | name: 'test_in', 13 | description: i18n.t('source.inputDescription'), 14 | request: stringifyJSON(DEFAULT_REQUEST), 15 | init: stringifyJSON([]), 16 | shutdown: stringifyJSON([]), 17 | reInitTriggers: stringifyJSON([]), 18 | }; 19 | return { 20 | name: data.name, 21 | description: data.description, 22 | request: stringifyJSON(data.request), 23 | init: stringifyJSON(data.init, []), 24 | shutdown: stringifyJSON(data.shutdown, []), 25 | reInitTriggers: stringifyJSON(data.reInitTriggers, []), 26 | }; 27 | } 28 | 29 | export function mapFormDataToSource( 30 | data: SourceFormData, 31 | serviceId: string 32 | ): Source { 33 | const { name, description } = data; 34 | return { 35 | name: name.trim(), 36 | description: description.trim(), 37 | service: serviceId, 38 | request: parseJSON(data.request), 39 | init: parseJSON(data.init, true), 40 | shutdown: parseJSON(data.shutdown, true), 41 | reInitTriggers: parseJSON(data.reInitTriggers, true), 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/dal/ScenarioDAO.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.dal 2 | 3 | import scala.annotation.implicitNotFound 4 | 5 | import com.github.dwickern.macros.NameOf.* 6 | import org.mongodb.scala.MongoCollection 7 | import org.mongodb.scala.bson.BsonDocument 8 | import org.mongodb.scala.model.Indexes.* 9 | 10 | import ru.tinkoff.tcb.mockingbird.model.Scenario 11 | import ru.tinkoff.tcb.mongo.DAOBase 12 | import ru.tinkoff.tcb.mongo.MongoDAO 13 | 14 | @implicitNotFound("Could not find an instance of ScenarioDAO for ${F}") 15 | trait ScenarioDAO[F[_]] extends MongoDAO[F, Scenario] 16 | 17 | object ScenarioDAO 18 | 19 | class ScenarioDAOImpl(collection: MongoCollection[BsonDocument]) 20 | extends DAOBase[Scenario](collection) 21 | with ScenarioDAO[Task] { 22 | def createIndexes: Task[Unit] = 23 | createIndex( 24 | ascending(nameOf[Scenario](_.source), nameOf[Scenario](_.scope)) 25 | ) *> createIndex( 26 | descending(nameOf[Scenario](_.created)) 27 | ) *> createIndex( 28 | ascending(nameOf[Scenario](_.service)) 29 | ) *> createIndex( 30 | ascending(nameOf[Scenario](_.labels)) 31 | ) 32 | } 33 | 34 | object ScenarioDAOImpl { 35 | val live = ZLayer { 36 | for { 37 | mc <- ZIO.service[MongoCollection[BsonDocument]] 38 | sd = new ScenarioDAOImpl(mc) 39 | _ <- sd.createIndexes 40 | } yield sd.asInstanceOf[ScenarioDAO[Task]] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/grpc/Method.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.grpc 2 | 3 | import java.io.ByteArrayInputStream 4 | import java.io.InputStream 5 | 6 | import io.grpc.MethodDescriptor 7 | import io.grpc.MethodDescriptor.Marshaller 8 | 9 | object Method { 10 | 11 | /* 12 | Universal marshaller that does not alter byte stream 13 | */ 14 | final case class ByteMarshaller() extends Marshaller[Array[Byte]] { 15 | override def stream(value: Array[Byte]): InputStream = new ByteArrayInputStream(value) 16 | 17 | override def parse(stream: InputStream): Array[Byte] = stream.readAllBytes() 18 | } 19 | 20 | val byteMethod: MethodDescriptor[Array[Byte], Array[Byte]] = 21 | MethodDescriptor 22 | .newBuilder() 23 | .setType(MethodDescriptor.MethodType.UNARY) 24 | .setFullMethodName(MethodDescriptor.generateFullMethodName("Any", "Any")) 25 | .setRequestMarshaller(ByteMarshaller()) 26 | .setResponseMarshaller(ByteMarshaller()) 27 | .build() 28 | 29 | def byteMethod(serviceName: String, methodName: String): MethodDescriptor[Array[Byte], Array[Byte]] = 30 | MethodDescriptor 31 | .newBuilder() 32 | .setType(MethodDescriptor.MethodType.BIDI_STREAMING) 33 | .setFullMethodName(MethodDescriptor.generateFullMethodName(serviceName, methodName)) 34 | .setRequestMarshaller(ByteMarshaller()) 35 | .setResponseMarshaller(ByteMarshaller()) 36 | .build() 37 | 38 | } 39 | -------------------------------------------------------------------------------- /backend/mockingbird/src/test/scala/ru/tinkoff/tcb/protobuf/ProtoToDescriptorSpec.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.protobuf 2 | 3 | import scala.jdk.CollectionConverters.SetHasAsScala 4 | 5 | import com.github.os72.protobuf.dynamic.DynamicSchema 6 | import zio.test.* 7 | 8 | object ProtoToDescriptorSpec extends ZIOSpecDefault { 9 | 10 | val messageTypes: Set[String] = Set("CarGenRequest", "CarSearchRequest", "MemoRequest") 11 | 12 | val nestedMessageTypes: Set[String] = Set( 13 | "utp.stock_service.v1.GetStocksRequest", 14 | "utp.stock_service.v1.GetStocksResponse", 15 | "utp.stock_service.v1.GetStocksResponse.Stock", 16 | "utp.stock_service.v1.GetStocksResponse.Stocks" 17 | ) 18 | 19 | override def spec: Spec[TestEnvironment & Scope, Any] = 20 | suite("Proto to descriptor")( 21 | test("DynamicSchema is successfully parsed from proto file") { 22 | for { 23 | content <- Utils.getProtoDescriptionFromResource("requests.proto") 24 | schema = DynamicSchema.parseFrom(content) 25 | } yield assertTrue(messageTypes.subsetOf(schema.getMessageTypes.asScala)) 26 | }, 27 | test("DynamicSchema is successfully parsed from proto file with nested schema") { 28 | for { 29 | content <- Utils.getProtoDescriptionFromResource("nested.proto") 30 | schema = DynamicSchema.parseFrom(content) 31 | } yield assertTrue(nestedMessageTypes.subsetOf(schema.getMessageTypes.asScala)) 32 | } 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/infrastructure/notifications/store/store.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | import { createReducer, createEvent } from '@tramvai/state'; 3 | 4 | export type Notification = 5 | | { 6 | type: 'error'; 7 | icon?: ReactNode; 8 | timer: number; 9 | title: string; 10 | description?: string; 11 | } 12 | | { 13 | type: 'success'; 14 | icon?: ReactNode; 15 | timer: number; 16 | title: string; 17 | description?: string; 18 | } 19 | | { 20 | type: 'info'; 21 | icon?: ReactNode; 22 | timer: number; 23 | title: string; 24 | description?: string; 25 | }; 26 | 27 | export type StoreState = { 28 | notifications: Notification[]; 29 | }; 30 | 31 | const storeName = 'notificationsState' as const; 32 | const initialState: StoreState = { 33 | notifications: [], 34 | }; 35 | 36 | export const addToast = createEvent('ADD_TOAST_SUCCESS'); 37 | export const removeToast = createEvent('REMOVE_TOAST_SUCCESS'); 38 | 39 | export const selector = (state: { notificationsState: StoreState }) => 40 | state[storeName]; 41 | 42 | export default createReducer(storeName, initialState) 43 | .on(addToast, (state, item) => ({ 44 | ...state, 45 | notifications: [...state.notifications, item], 46 | })) 47 | .on(removeToast, (state, item) => ({ 48 | ...state, 49 | notifications: state.notifications.filter((n) => n.title !== item.title), 50 | })); 51 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/protocol/bson.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.protocol 2 | 3 | import scala.util.Try 4 | 5 | import cats.data.NonEmptyVector 6 | import oolong.bson.* 7 | import oolong.bson.given 8 | import org.mongodb.scala.bson.* 9 | 10 | import ru.tinkoff.tcb.utils.circe.optics.JLens 11 | import ru.tinkoff.tcb.utils.circe.optics.JsonOptic 12 | import ru.tinkoff.tcb.utils.string.* 13 | 14 | object bson { 15 | implicit val jsonOpticBsonEncoder: BsonEncoder[JsonOptic] = 16 | (value: JsonOptic) => BsonString(value.path.replace('.', '⋮')) 17 | 18 | implicit val jsonOpticBsonDecoder: BsonDecoder[JsonOptic] = 19 | (value: BsonValue) => 20 | Try(value.asString().getValue) 21 | .map(_.nonEmptyString.map(_.replace('⋮', '.')).map(JsonOptic.fromPathString).getOrElse(JLens)) 22 | 23 | implicit val jsonOpticBsonKeyEncoder: BsonKeyEncoder[JsonOptic] = (j: JsonOptic) => j.path.replace('.', '⋮') 24 | 25 | implicit val jsonOpticBsonKeyDecoder: BsonKeyDecoder[JsonOptic] = (value: String) => 26 | Try(value.nonEmptyString.map(_.replace('⋮', '.')).map(JsonOptic.fromPathString).getOrElse(JLens)) 27 | 28 | implicit final def nonEmptyVectorBsonEncoder[T: BsonEncoder]: BsonEncoder[NonEmptyVector[T]] = 29 | BsonEncoder[Vector[T]].beforeWrite(_.toVector) 30 | 31 | implicit final def nonEmptyVectorBsonDecoder[T: BsonDecoder]: BsonDecoder[NonEmptyVector[T]] = 32 | BsonDecoder[Vector[T]].afterReadTry(v => Try(NonEmptyVector.fromVectorUnsafe(v))) 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/services/actions/createAction.ts: -------------------------------------------------------------------------------- 1 | import { createAction as createActionCore } from '@tramvai/core'; 2 | import { getJson } from 'src/mockingbird/infrastructure/request'; 3 | import { 4 | setLoading, 5 | createSuccess, 6 | createFail, 7 | reset, 8 | } from '../reducers/createStore'; 9 | 10 | type ServiceRequestParams = { 11 | name: string; 12 | suffix: string; 13 | }; 14 | 15 | export const createAction = createActionCore({ 16 | name: 'CREATE_SERVICE_ACTION', 17 | fn: ({ dispatch, getState }, body: ServiceRequestParams) => { 18 | dispatch(setLoading()); 19 | const { 20 | environment: { MOCKINGBIRD_API }, 21 | } = getState(); 22 | return getJson(`${MOCKINGBIRD_API}/v2/service`, { 23 | httpMethod: 'post', 24 | body: normalizeRequest(body), 25 | }) 26 | .then((response) => { 27 | const event = 28 | response.status === 'success' && response.id 29 | ? createSuccess(response.id) 30 | : createFail(null); 31 | return dispatch(event); 32 | }) 33 | .catch((e) => dispatch(createFail(e))); 34 | }, 35 | }); 36 | 37 | function normalizeRequest(params: ServiceRequestParams): ServiceRequestParams { 38 | return { 39 | name: params.name.trim(), 40 | suffix: params.suffix.trim(), 41 | }; 42 | } 43 | 44 | export const resetCreateStateAction = createActionCore({ 45 | name: 'RESET_CREATE_SERVICE_ACTION', 46 | fn: ({ dispatch }) => dispatch(reset()), 47 | }); 48 | -------------------------------------------------------------------------------- /frontend/src/infrastructure/helpers/copy-to-clipboard/index.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'src/mockingbird/i18n'; 2 | 3 | export default function copyToClipboard( 4 | text: string, 5 | callback: (error: Error) => void 6 | ) { 7 | if (text == null) throw new Error(i18n.t('components.copy.textEmptyError')); 8 | const el = window.document.createElement('textarea'); 9 | el.readOnly = true; // подавляем экранную клавиатуру на touch-устройствах 10 | Object.assign( 11 | // важно, чтобы поле было незаменто, но находилось во viewport'е, 12 | // иначе может измениться позиция скролла (например, в IE) 13 | el.style, 14 | { 15 | width: 1, 16 | height: 1, 17 | position: 'fixed', 18 | top: 0, 19 | left: 0, 20 | border: 0, 21 | padding: 0, 22 | margin: 0, 23 | backgroundColor: 'transparent', 24 | color: 'transparent', 25 | overflow: 'hidden', 26 | } 27 | ); 28 | el.value = text; 29 | window.document.body.appendChild(el); 30 | let eventFired = false; 31 | el.addEventListener('copy', () => { 32 | eventFired = true; 33 | }); 34 | try { 35 | el.select(); // для большинства 36 | el.setSelectionRange(0, text.length); // для iOS 37 | global.document.execCommand('copy'); 38 | if (!eventFired) throw new Error(i18n.t('components.copy.copyError')); 39 | if (callback) callback(); 40 | } catch (e) { 41 | if (callback) callback(e); 42 | } finally { 43 | global.document.body.removeChild(el); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/bundles/mainDefault.ts: -------------------------------------------------------------------------------- 1 | import { createBundle } from '@tramvai/core'; 2 | import Services from '../modules/services'; 3 | import { PageSources } from '../modules/sources'; 4 | import { PageSource, PageSourceNew } from '../modules/source'; 5 | import { PageDestinations } from '../modules/destinations'; 6 | import { PageDestination, PageDestinationNew } from '../modules/destination'; 7 | import Mocks from '../layers/pages/Mocks'; 8 | import Mock from '../layers/pages/Mock'; 9 | import MockNew from '../layers/pages/MockNew'; 10 | import NotFound from '../layers/pages/NotFound'; 11 | 12 | export default createBundle({ 13 | name: 'mainDefault', 14 | components: { 15 | // регистрируем компонент страницы, который будет использоваться для всех страниц, к которым привязан этот бандл, по умолчанию 16 | pageDefault: Services, 17 | 18 | // регистрируем компонент страницы, который будет использован при соответствующих настройках роута 19 | pageServices: Services, 20 | pageSources: PageSources, 21 | pageSource: PageSource, 22 | pageSourceNew: PageSourceNew, 23 | pageDestinations: PageDestinations, 24 | pageDestination: PageDestination, 25 | pageDestinationNew: PageDestinationNew, 26 | pageMocks: Mocks, 27 | pageMock: Mock, 28 | pageMockNew: MockNew, 29 | 30 | // страница не найдена 31 | pageNotfound: NotFound, 32 | }, 33 | // регистрируем экшены, которые будут выполняться для всех страниц, к которым привязан этот бандл 34 | actions: [], 35 | }); 36 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/layers/pages/mock/GrpcNew.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect } from 'react'; 2 | import { useActions, useStoreSelector } from '@tramvai/state'; 3 | import { selectorAsIs } from 'src/mockingbird/infrastructure/helpers/state'; 4 | import createStubStore from 'src/mockingbird/models/mockCreate/reducers/store'; 5 | import { 6 | createAction, 7 | resetCreateStateAction, 8 | } from 'src/mockingbird/models/mockCreate/actions'; 9 | import FormGrpc from './FormGrpc'; 10 | import { mapFormDataToGrpc } from './utils'; 11 | import type { TGRPCFormData } from './types'; 12 | 13 | type Props = { 14 | labels: string[]; 15 | serviceId: string; 16 | }; 17 | 18 | export default function GrpcNew({ labels, serviceId }: Props) { 19 | const create = useActions(createAction); 20 | const { status } = useStoreSelector(createStubStore, selectorAsIs); 21 | const resetCreateState = useActions(resetCreateStateAction); 22 | useEffect(() => resetCreateState as any, [resetCreateState]); 23 | 24 | const onCreate = useCallback( 25 | (formData: TGRPCFormData) => { 26 | mapFormDataToGrpc(formData, serviceId).then((data) => 27 | create({ 28 | type: 'grpc', 29 | data, 30 | serviceId, 31 | }) 32 | ); 33 | }, 34 | [serviceId, create] 35 | ); 36 | 37 | return ( 38 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/layers/pages/mock/HttpNew.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect } from 'react'; 2 | import { useActions, useStoreSelector } from '@tramvai/state'; 3 | import { selectorAsIs } from 'src/mockingbird/infrastructure/helpers/state'; 4 | import createStubStore from 'src/mockingbird/models/mockCreate/reducers/store'; 5 | import { 6 | createAction, 7 | resetCreateStateAction, 8 | } from 'src/mockingbird/models/mockCreate/actions'; 9 | import FormHttp from './FormHttp'; 10 | import { mapFormDataToStub } from './utils'; 11 | import type { TFormCallback, THTTPFormData } from './types'; 12 | 13 | type Props = { 14 | labels: string[]; 15 | serviceId: string; 16 | }; 17 | 18 | export default function HttpNew({ labels, serviceId }: Props) { 19 | const create = useActions(createAction); 20 | const { status } = useStoreSelector(createStubStore, selectorAsIs); 21 | const resetCreateState = useActions(resetCreateStateAction); 22 | useEffect(() => resetCreateState as any, [resetCreateState]); 23 | 24 | const onCreate = useCallback( 25 | (data: THTTPFormData, callbacks: TFormCallback[]) => { 26 | create({ 27 | type: 'http', 28 | data: mapFormDataToStub(data, serviceId, callbacks), 29 | serviceId, 30 | }); 31 | }, 32 | [serviceId, create] 33 | ); 34 | 35 | return ( 36 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/layers/pages/mock/types.ts: -------------------------------------------------------------------------------- 1 | export type THTTPFormData = { 2 | name: string; 3 | labels: string[]; 4 | scope: string; 5 | times: number; 6 | method: string; 7 | path: string; 8 | isPathPattern: boolean; 9 | request: string; 10 | response: string; 11 | state: string; 12 | persist: string; 13 | seed: string; 14 | }; 15 | 16 | export type TScenarioFormData = { 17 | name: string; 18 | labels: string[]; 19 | scope: string; 20 | times: number; 21 | source: string; 22 | destination: string; 23 | input: string; 24 | output: string; 25 | state: string; 26 | persist: string; 27 | seed: string; 28 | }; 29 | 30 | export type TGRPCFormData = { 31 | name: string; 32 | labels: string[]; 33 | scope: string; 34 | times: number; 35 | methodName: string; 36 | requestCodecs: any; 37 | requestSchema?: any; 38 | requestClass: string; 39 | requestPredicates: string; 40 | responseCodecs: any; 41 | responseSchema?: any; 42 | responseClass: string; 43 | response: string; 44 | state: string; 45 | seed: string; 46 | }; 47 | 48 | export type TFormCallback = TFormCallbackHTTP | TFormCallbackMessage; 49 | 50 | export type TFormCallbackHTTP = { 51 | type: 'http'; 52 | request: string; 53 | responseMode: '' | 'json' | 'xml'; 54 | persist: string; 55 | delay?: string; 56 | id: string; 57 | }; 58 | 59 | export type TFormCallbackMessage = { 60 | type: 'message'; 61 | destination: string; 62 | output: string; 63 | delay?: string; 64 | id: string; 65 | }; 66 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/dal/GrpcMethodDescriptionDAO.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.dal 2 | 3 | import scala.annotation.implicitNotFound 4 | 5 | import com.github.dwickern.macros.NameOf.* 6 | import org.mongodb.scala.MongoCollection 7 | import org.mongodb.scala.bson.BsonDocument 8 | import org.mongodb.scala.model.Indexes.* 9 | 10 | import ru.tinkoff.tcb.mockingbird.model.GrpcMethodDescription 11 | import ru.tinkoff.tcb.mongo.DAOBase 12 | import ru.tinkoff.tcb.mongo.MongoDAO 13 | 14 | @implicitNotFound("Could not find an instance of GrpcMethodDescriptionDAO for ${F}") 15 | trait GrpcMethodDescriptionDAO[F[_]] extends MongoDAO[F, GrpcMethodDescription] 16 | 17 | object GrpcMethodDescriptionDAO 18 | 19 | class GrpcMethodDescriptionDAOImpl(collection: MongoCollection[BsonDocument]) 20 | extends DAOBase[GrpcMethodDescription](collection) 21 | with GrpcMethodDescriptionDAO[Task] { 22 | def createIndexes: Task[Unit] = createIndex( 23 | ascending(nameOf[GrpcMethodDescription](_.methodName)) 24 | ) *> createIndex( 25 | ascending(nameOf[GrpcMethodDescription](_.service)) 26 | ) *> createIndex( 27 | descending(nameOf[GrpcMethodDescription](_.created)) 28 | ) 29 | } 30 | 31 | object GrpcMethodDescriptionDAOImpl { 32 | val live = ZLayer { 33 | for { 34 | mc <- ZIO.service[MongoCollection[BsonDocument]] 35 | sd = new GrpcMethodDescriptionDAOImpl(mc) 36 | _ <- sd.createIndexes 37 | } yield sd.asInstanceOf[GrpcMethodDescriptionDAO[Task]] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/logging/Mdc.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.logging 2 | 3 | import io.circe.Json 4 | import tofu.logging.Loggable 5 | import tofu.logging.LoggedValue 6 | import tofu.logging.derivation.derived 7 | import tofu.logging.derivation.unembed 8 | 9 | import ru.tinkoff.tcb.utils.map.* 10 | 11 | final case class Mdc( 12 | payload: Option[Map[String, LoggedValue]] = None, 13 | @unembed 14 | traceInfo: Map[String, String] = Map.empty 15 | ) derives Loggable { 16 | @inline def +?[T: Loggable](kv: (String, Option[T])): Mdc = 17 | copy( 18 | payload = Some(payload.getOrElse(Map.empty) +? (kv._1 -> kv._2.map(Loggable[T].loggedValue))).filter(_.nonEmpty) 19 | ) 20 | @inline def ++(values: Map[String, LoggedValue]): Mdc = 21 | copy(payload = Some(payload.getOrElse(Map.empty) ++ values).filter(_.nonEmpty)) 22 | @inline def +[T: Loggable](value: (String, T)): Mdc = this.+(value._1 -> Loggable[T].loggedValue(value._2)) 23 | @inline def +(value: (String, LoggedValue)): Mdc = copy(payload = Some((payload.getOrElse(Map.empty) + value))) 24 | 25 | @inline def setTraceInfo(name: String, value: String): Mdc = copy(traceInfo = traceInfo + (name -> value)) 26 | } 27 | 28 | object Mdc { 29 | val empty: Mdc = Mdc() 30 | 31 | def withPayload(kvp: (String, LoggedValue)*): Mdc = Mdc(payload = Some(Map(kvp*))) 32 | 33 | def fromJson(json: Json)(implicit lj: Loggable[Json]): Mdc = 34 | Mdc(payload = json.asObject.map(_.toMap.view.mapValues(Loggable[Json].loggedValue).toMap)) 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/infrastructure/helpers/forms/index.ts: -------------------------------------------------------------------------------- 1 | import type { FieldErrors, FieldError } from 'react-hook-form'; 2 | import i18n from 'src/mockingbird/i18n'; 3 | 4 | export function extractError(name: string, errors: FieldErrors): string | null { 5 | const error = errors[name]; 6 | return getErrorMessage(error) || null; 7 | } 8 | 9 | function getErrorMessage(error: FieldError) { 10 | if (!error) return ''; 11 | const { type, message } = error; 12 | switch (type) { 13 | case 'required': 14 | return i18n.t('validation.required'); 15 | case 'validate': 16 | return message; 17 | } 18 | } 19 | 20 | export function validateJSON(value: string) { 21 | const message = i18n.t('validation.invalidJson'); 22 | if (!value) return; 23 | try { 24 | if (!isObject(JSON.parse(value))) return message; 25 | } catch (e) { 26 | return message; 27 | } 28 | } 29 | 30 | export function validateJSONArray(value: string) { 31 | const message = i18n.t('validation.invalidArray'); 32 | if (!value) return; 33 | try { 34 | if (!Array.isArray(JSON.parse(value))) return message; 35 | } catch (e) { 36 | return message; 37 | } 38 | } 39 | 40 | function isObject(item: any) { 41 | return item !== null && typeof item === 'object' && !Array.isArray(item); 42 | } 43 | 44 | export function mapSelectItem(value: string) { 45 | return { 46 | label: value, 47 | value, 48 | }; 49 | } 50 | 51 | export function mapSelectValue(value: string | any) { 52 | return typeof value === 'string' ? value : value.value; 53 | } 54 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/layers/pages/mock/ScenarioNew.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect } from 'react'; 2 | import { useActions, useStoreSelector } from '@tramvai/state'; 3 | import { selectorAsIs } from 'src/mockingbird/infrastructure/helpers/state'; 4 | import createStubStore from 'src/mockingbird/models/mockCreate/reducers/store'; 5 | import { 6 | createAction, 7 | resetCreateStateAction, 8 | } from 'src/mockingbird/models/mockCreate/actions'; 9 | import FormScenario from './FormScenario'; 10 | import { mapFormDataToScenario } from './utils'; 11 | import type { TFormCallback, TScenarioFormData } from './types'; 12 | 13 | type Props = { 14 | labels: string[]; 15 | serviceId: string; 16 | }; 17 | 18 | export default function ScenarioNew({ labels, serviceId }: Props) { 19 | const create = useActions(createAction); 20 | const { status } = useStoreSelector(createStubStore, selectorAsIs); 21 | const resetCreateState = useActions(resetCreateStateAction); 22 | useEffect(() => resetCreateState as any, [resetCreateState]); 23 | 24 | const onCreate = useCallback( 25 | (data: TScenarioFormData, callbacks: TFormCallback[]) => { 26 | create({ 27 | type: 'scenario', 28 | data: mapFormDataToScenario(data, serviceId, callbacks), 29 | serviceId, 30 | }); 31 | }, 32 | [serviceId, create] 33 | ); 34 | 35 | return ( 36 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/CreateSourceConfigurationRequest.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.api.request 2 | 3 | import cats.data.NonEmptyVector 4 | import io.circe.Decoder 5 | import io.circe.Encoder 6 | import sttp.tapir.Schema 7 | import sttp.tapir.Schema.annotations.description 8 | 9 | import ru.tinkoff.tcb.generic.PropSubset 10 | import ru.tinkoff.tcb.mockingbird.model.EventSourceRequest 11 | import ru.tinkoff.tcb.mockingbird.model.ResourceRequest 12 | import ru.tinkoff.tcb.mockingbird.model.ResponseSpec 13 | import ru.tinkoff.tcb.mockingbird.model.SourceConfiguration 14 | import ru.tinkoff.tcb.protocol.schema.* 15 | import ru.tinkoff.tcb.utils.id.SID 16 | 17 | final case class CreateSourceConfigurationRequest( 18 | @description("Unique configuration name") 19 | name: SID[SourceConfiguration], 20 | @description("Configuration description") 21 | description: String, 22 | service: String, 23 | @description("Request specification") 24 | request: EventSourceRequest, 25 | @description("Initializer specification") 26 | init: Option[NonEmptyVector[ResourceRequest]], 27 | @description("Finalizer specification") 28 | shutdown: Option[NonEmptyVector[ResourceRequest]], 29 | @description("Reinitialization triggers specification") 30 | reInitTriggers: Option[NonEmptyVector[ResponseSpec]] 31 | ) derives Decoder, 32 | Encoder, 33 | Schema 34 | 35 | object CreateSourceConfigurationRequest { 36 | implicitly[PropSubset[CreateSourceConfigurationRequest, SourceConfiguration]] 37 | } 38 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/dal/HttpStubDAO.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.dal 2 | 3 | import scala.annotation.implicitNotFound 4 | 5 | import com.github.dwickern.macros.NameOf.* 6 | import org.mongodb.scala.MongoCollection 7 | import org.mongodb.scala.bson.BsonDocument 8 | import org.mongodb.scala.model.Indexes.* 9 | 10 | import ru.tinkoff.tcb.mockingbird.model.HttpStub 11 | import ru.tinkoff.tcb.mongo.DAOBase 12 | import ru.tinkoff.tcb.mongo.MongoDAO 13 | 14 | @implicitNotFound("Could not find an instance of HttpStubDAO for ${F}") 15 | trait HttpStubDAO[F[_]] extends MongoDAO[F, HttpStub] 16 | 17 | object HttpStubDAO 18 | 19 | class HttpStubDAOImpl(collection: MongoCollection[BsonDocument]) 20 | extends DAOBase[HttpStub](collection) 21 | with HttpStubDAO[Task] { 22 | def createIndexes: Task[Unit] = 23 | createIndex( 24 | ascending( 25 | nameOf[HttpStub](_.method), 26 | nameOf[HttpStub](_.path), 27 | nameOf[HttpStub](_.scope), 28 | nameOf[HttpStub](_.times) 29 | ), 30 | ) *> createIndex( 31 | descending(nameOf[HttpStub](_.created)) 32 | ) *> createIndex( 33 | ascending(nameOf[HttpStub](_.serviceSuffix)) 34 | ) *> createIndex( 35 | ascending(nameOf[HttpStub](_.labels)) 36 | ) 37 | } 38 | 39 | object HttpStubDAOImpl { 40 | val live = ZLayer { 41 | for { 42 | mc <- ZIO.service[MongoCollection[BsonDocument]] 43 | sd = new HttpStubDAOImpl(mc) 44 | _ <- sd.createIndexes 45 | } yield sd.asInstanceOf[HttpStubDAO[Task]] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/predicatedsl/PredicateConstructionError.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.predicatedsl 2 | 3 | import cats.data.NonEmptyList 4 | import io.circe.Json 5 | import io.circe.Printer 6 | 7 | import ru.tinkoff.tcb.utils.circe.optics.JsonOptic 8 | 9 | sealed trait PredicateConstructionError 10 | 11 | object PredicateConstructionError { 12 | implicit val show: Show[PredicateConstructionError] = Show.show[PredicateConstructionError] { s => 13 | def mkError(errs: NonEmptyList[(Keyword, Json)]): String = { 14 | val printer = Printer.noSpaces.copy(dropNullValues = true) 15 | errs 16 | .foldLeft(List.empty[String]) { case (acc, (kw, json)) => 17 | acc :+ s"""invalid op: "${kw.value}" on: ${printer.print(json)}""" 18 | } 19 | .mkString("[", ", ", "]") 20 | } 21 | 22 | s match { 23 | case XPathError(xpath, error) => s"""xpath: "$xpath" error:$error""" 24 | case SpecificationError(xpath, errors) => 25 | s"""[xpath: "$xpath" errors:${mkError(errors)}]""" 26 | 27 | case JSpecificationError(optic, errors) => 28 | s"""[optic: "$optic" errors:${mkError(errors)}]""" 29 | } 30 | } 31 | } 32 | 33 | final case class XPathError(xpath: String, error: String) extends PredicateConstructionError 34 | final case class SpecificationError(xpath: String, errors: NonEmptyList[(Keyword, Json)]) 35 | extends PredicateConstructionError 36 | 37 | final case class JSpecificationError(optic: JsonOptic, errors: NonEmptyList[(Keyword, Json)]) 38 | extends PredicateConstructionError 39 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/CreateGrpcMethodDescriptionRequest.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.api.request 2 | 3 | import eu.timepit.refined.types.string.NonEmptyString 4 | import io.circe.Decoder 5 | import io.circe.Encoder 6 | import io.circe.refined.* 7 | import sttp.tapir.Schema 8 | import sttp.tapir.Schema.annotations.description 9 | import sttp.tapir.codec.refined.* 10 | 11 | import ru.tinkoff.tcb.mockingbird.model.ByteArray 12 | import ru.tinkoff.tcb.mockingbird.model.GrpcConnectionType 13 | import ru.tinkoff.tcb.mockingbird.model.GrpcMethodDescription 14 | import ru.tinkoff.tcb.utils.id.SID 15 | 16 | final case class CreateGrpcMethodDescriptionRequest( 17 | @description("Unique method description name") 18 | id: SID[GrpcMethodDescription], 19 | @description("Description of the method description") 20 | description: String, 21 | @description("Service name") 22 | service: NonEmptyString, 23 | @description("gRPC method") 24 | methodName: String, 25 | @description("gRPC connection type") 26 | connectionType: GrpcConnectionType, 27 | @description("Proxy url. Only relevant for proxy stubs") 28 | proxyUrl: Option[String], 29 | @description("gRPC request class") 30 | requestClass: String, 31 | @description("gRPC base64 encoded request proto") 32 | requestCodecs: ByteArray.Type, 33 | @description("gRPC response class") 34 | responseClass: String, 35 | @description("gRPC base64 encoded response proto") 36 | responseCodecs: ByteArray.Type 37 | ) derives Decoder, 38 | Encoder, 39 | Schema 40 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/components/form/InputJson/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import type { Control } from 'react-hook-form'; 3 | import { useController } from 'react-hook-form'; 4 | import { JsonInput } from '@mantine/core'; 5 | import type { JsonInputProps } from '@mantine/core'; 6 | import { 7 | validateJSON, 8 | extractError, 9 | } from 'src/mockingbird/infrastructure/helpers/forms'; 10 | 11 | type Props = Omit & { 12 | name: string; 13 | control: Control; 14 | validate?: (value: string) => string | undefined; 15 | }; 16 | 17 | export function InputJson(props: Props) { 18 | const { 19 | name, 20 | label, 21 | control, 22 | required = false, 23 | disabled = false, 24 | validate = validateJSON, 25 | error = '', 26 | ...restProps 27 | } = props; 28 | const { field, fieldState, formState } = useController({ 29 | name, 30 | control, 31 | rules: { 32 | required, 33 | validate, 34 | }, 35 | defaultValue: '', 36 | }); 37 | const { onChange } = field; 38 | const handleChange = useCallback((value) => onChange(value), [onChange]); 39 | const formError = extractError(name, formState.errors); 40 | const uiError = error || formError || fieldState.error?.message; 41 | return ( 42 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/models/mock/types.ts: -------------------------------------------------------------------------------- 1 | export type Mock = THTTPMock | TScenarioMock | TGRPCMock; 2 | 3 | export type THTTPMock = { 4 | id: string; 5 | name: string; 6 | method: string; 7 | path?: string; 8 | pathPattern?: string; 9 | scope: string; 10 | times: number; 11 | labels: string[]; 12 | request: any; 13 | response: any; 14 | state: any; 15 | persist: any; 16 | seed: any; 17 | callback?: TCallBack; 18 | }; 19 | 20 | export type TScenarioMock = { 21 | id: string; 22 | name: string; 23 | source: string; 24 | destination: string; 25 | scope: string; 26 | times?: number; 27 | labels: string[]; 28 | input: any; 29 | output: any; 30 | state: any; 31 | persist: any; 32 | seed: any; 33 | callback?: TCallBack; 34 | }; 35 | 36 | export type TGRPCMock = { 37 | id: string; 38 | name: string; 39 | scope: string; 40 | times?: number; 41 | labels: string[]; 42 | methodName: string; 43 | requestCodecs: any; 44 | requestSchema?: any; 45 | requestClass: string; 46 | requestPredicates: any; 47 | responseCodecs: any; 48 | responseSchema?: any; 49 | responseClass: string; 50 | response: any; 51 | state: any; 52 | seed: any; 53 | }; 54 | 55 | export type TCallBack = TCallBackHTTP | TCallbackMessage; 56 | 57 | export type TCallBackHTTP = { 58 | type: 'http'; 59 | request: any; 60 | responseMode?: 'json' | 'xml'; 61 | persist?: any; 62 | delay?: string; 63 | callback?: TCallBack; 64 | }; 65 | 66 | export type TCallbackMessage = { 67 | type: 'message'; 68 | destination: string; 69 | output: any; 70 | delay?: string; 71 | callback?: TCallBack; 72 | }; 73 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/source/ui/PageSourceNew.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { useUrl } from '@tramvai/module-router'; 4 | import { useActions, useStoreSelector } from '@tramvai/state'; 5 | import PageHeader from 'src/components/PageHeader/PageHeader'; 6 | import Page from 'src/mockingbird/components/Page'; 7 | import { getPathSources } from 'src/mockingbird/paths'; 8 | import { selectorAsIs } from 'src/mockingbird/infrastructure/helpers/state'; 9 | import Form from './Form'; 10 | import { createAction } from '../actions'; 11 | import { createStore } from '../reducers'; 12 | import { mapFormDataToSource } from '../utils'; 13 | import type { SourceFormData } from '../types'; 14 | 15 | export default function MockNew() { 16 | const { t } = useTranslation(); 17 | const url = useUrl(); 18 | const serviceId = Array.isArray(url.query.service) 19 | ? url.query.service[0] 20 | : url.query.service; 21 | const create = useActions(createAction); 22 | const { status } = useStoreSelector(createStore, selectorAsIs); 23 | const onCreate = useCallback( 24 | (data: SourceFormData) => { 25 | create({ 26 | data: mapFormDataToSource(data, serviceId), 27 | serviceId, 28 | }); 29 | }, 30 | [serviceId, create] 31 | ); 32 | return ( 33 | 34 | 39 |
40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/CallbackChecker.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.mockingbird.model 2 | 3 | import com.github.dwickern.macros.NameOf.nameOf 4 | import mouse.all.booleanSyntaxMouse 5 | 6 | import ru.tinkoff.tcb.utils.id.SID 7 | import ru.tinkoff.tcb.utils.unpack.* 8 | 9 | trait CallbackChecker { 10 | 11 | protected def checkCallback( 12 | callback: Option[Callback], 13 | destinations: Set[SID[DestinationConfiguration]] 14 | ): Vector[String] = 15 | callback match { 16 | case None => Vector.empty 17 | case Some(value) => 18 | value match { 19 | case MessageCallback(dest, _, mcallback, _) => 20 | (destinations(dest) !? Vector( 21 | s"The field ${nameOf[MessageCallback](_.destination)} must be filled" 22 | )) ++ checkCallback(mcallback, destinations) 23 | case HttpCallback(_, rm, p, hcallback, _) => 24 | (rm, p) match { 25 | case Some(_) <*> None => 26 | s"The field ${nameOf[HttpCallback](_.responseMode)} must be filled in ONLY if ${nameOf[HttpCallback](_.persist)} is present" +: checkCallback( 27 | hcallback, 28 | destinations 29 | ) 30 | case None <*> Some(_) => 31 | s"The field ${nameOf[HttpCallback](_.responseMode)} must be filled in if ${nameOf[HttpCallback](_.persist)} is present" +: checkCallback( 32 | hcallback, 33 | destinations 34 | ) 35 | case _ => checkCallback(hcallback, destinations) 36 | } 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/tofu/logging/impl/ZUniversalContextLogging.scala: -------------------------------------------------------------------------------- 1 | package tofu.logging.impl 2 | 3 | import org.slf4j.LoggerFactory 4 | import org.slf4j.Marker 5 | import tofu.logging.Loggable 6 | import tofu.logging.LoggedValue 7 | import tofu.logging.Logging 8 | 9 | class ZUniversalContextLogging[R, C: Loggable](name: String, ctxLog: URIO[R, C]) extends Logging[[X] =>> URIO[R, X]] { 10 | override def write(level: Logging.Level, message: String, values: LoggedValue*): URIO[R, Unit] = 11 | ctxLog.flatMap { ctx => 12 | ZIO.succeed { 13 | val logger = LoggerFactory.getLogger(name) 14 | if (UniversalLogging.enabled(level, logger)) 15 | UniversalLogging.writeMarker(level, logger, ContextMarker(ctx), message, values) 16 | } 17 | } 18 | 19 | override def writeMarker(level: Logging.Level, message: String, marker: Marker, values: LoggedValue*): URIO[R, Unit] = 20 | ctxLog.flatMap { ctx => 21 | ZIO.succeed { 22 | val logger = LoggerFactory.getLogger(name) 23 | if (UniversalLogging.enabled(level, logger)) 24 | UniversalLogging.writeMarker(level, logger, ContextMarker(ctx, List(marker)), message, values) 25 | } 26 | } 27 | 28 | override def writeCause( 29 | level: Logging.Level, 30 | message: String, 31 | cause: Throwable, 32 | values: LoggedValue* 33 | ): URIO[R, Unit] = 34 | ctxLog.flatMap { ctx => 35 | ZIO.succeed { 36 | val logger = LoggerFactory.getLogger(name) 37 | if (UniversalLogging.enabled(level, logger)) 38 | UniversalLogging.writeMarkerCause(level, logger, ContextMarker(ctx), cause, message, values) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/mockingbird/modules/destination/ui/PageDestinationNew.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { useUrl } from '@tramvai/module-router'; 3 | import { useActions, useStoreSelector } from '@tramvai/state'; 4 | import PageHeader from 'src/components/PageHeader/PageHeader'; 5 | import Page from 'src/mockingbird/components/Page'; 6 | import { getPathDestinations } from 'src/mockingbird/paths'; 7 | import { selectorAsIs } from 'src/mockingbird/infrastructure/helpers/state'; 8 | import { useTranslation } from 'react-i18next'; 9 | import Form from './Form'; 10 | import { createAction } from '../actions'; 11 | import { createStore } from '../reducers'; 12 | import { mapFormDataToDestination } from '../utils'; 13 | import type { DestinationFormData } from '../types'; 14 | 15 | export default function MockNew() { 16 | const { t } = useTranslation(); 17 | const url = useUrl(); 18 | const serviceId = Array.isArray(url.query.service) 19 | ? url.query.service[0] 20 | : url.query.service; 21 | const create = useActions(createAction); 22 | const { status } = useStoreSelector(createStore, selectorAsIs); 23 | const onCreate = useCallback( 24 | (data: DestinationFormData) => { 25 | create({ 26 | data: mapFormDataToDestination(data, serviceId), 27 | serviceId, 28 | }); 29 | }, 30 | [serviceId, create] 31 | ); 32 | return ( 33 | 34 | 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/id/IDCompanion.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils.id 2 | 3 | import com.softwaremill.tagging.* 4 | import glass.Equivalent 5 | import io.circe.* 6 | import oolong.bson.* 7 | import pureconfig.ConfigReader 8 | import sttp.tapir.Schema 9 | import tofu.logging.Loggable 10 | 11 | import ru.tinkoff.tcb.generic.PropSubset 12 | import ru.tinkoff.tcb.generic.RootOptionFields 13 | 14 | trait IDCompanion[I] { 15 | type Aux[T] >: I @@ T <: I @@ T 16 | 17 | def apply[T](id: I): I @@ T = id.taggedWith[T] 18 | 19 | def unapply(id: I @@ ?): Some[I] = Some(id) 20 | 21 | def equiv[T]: Equivalent[I, I @@ T] = Equivalent[I](apply[T](_))(_.asInstanceOf[I]) 22 | 23 | implicit def encForID[T](implicit ei: Encoder[I]): Encoder[I @@ T] = ei.contramap(identity) 24 | implicit def decForID[T](implicit di: Decoder[I]): Decoder[I @@ T] = di.map(apply) 25 | 26 | implicit def bencForID[T](implicit iwrt: BsonEncoder[I]): BsonEncoder[I @@ T] = 27 | iwrt.beforeWrite(identity) 28 | implicit def brdrForID[T](implicit irdr: BsonDecoder[I]): BsonDecoder[I @@ T] = 29 | irdr.afterRead(apply) 30 | 31 | implicit def schemaForID[T](implicit st: Schema[I]): Schema[I @@ T] = 32 | st.as[I @@ T] 33 | 34 | implicit def rofForID[T]: RootOptionFields[I @@ T] = 35 | RootOptionFields.mk(Set.empty) 36 | 37 | implicit def idSubset[T]: PropSubset[I @@ T, I] = new PropSubset[I @@ T, I] {} 38 | implicit def idSubsetRev[T]: PropSubset[I, I @@ T] = new PropSubset[I, I @@ T] {} 39 | 40 | implicit def idLoggable[T](implicit il: Loggable[I]): Loggable[I @@ T] = il.narrow 41 | 42 | implicit def idConfigReader[T](implicit cr: ConfigReader[I]): ConfigReader[I @@ T] = cr.map(apply) 43 | } 44 | -------------------------------------------------------------------------------- /backend/utils/src/main/scala/ru/tinkoff/tcb/utils/string/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils 2 | 3 | import java.util.Locale.ENGLISH 4 | 5 | package object string { 6 | implicit class ExtStringOps(private val text: String) extends AnyVal { 7 | 8 | /** 9 | * Converts camelCase into camel_case 10 | */ 11 | def camel2Underscore: String = 12 | text.drop(1).foldLeft(text.headOption.map(ch => s"${ch.toLower}") getOrElse "") { 13 | case (acc, c) if c.isUpper => acc + "_" + c.toLower 14 | case (acc, c) => acc + c 15 | } 16 | 17 | /** 18 | * Converts snake_case into snakeCase 19 | */ 20 | def underscore2Camel: String = 21 | camelize(text) 22 | 23 | /** 24 | * Converts snake_case into SnakeCase 25 | */ 26 | def underscore2UpperCamel: String = 27 | pascalize(text) 28 | 29 | private def camelize(word: String): String = 30 | if (word.nonEmpty) { 31 | val w = pascalize(word) 32 | w.substring(0, 1).toLowerCase(ENGLISH) + w.substring(1) 33 | } else { 34 | word 35 | } 36 | 37 | private def pascalize(word: String): String = 38 | word 39 | .split("_") 40 | .map(s => s.substring(0, 1).toUpperCase(ENGLISH) + s.substring(1).toLowerCase(ENGLISH)) 41 | .mkString 42 | 43 | def nonEmptyString: Option[String] = Option(text).filterNot(_.isEmpty) 44 | 45 | def insertAt(position: Int, insertion: String): String = { 46 | val (fst, snd) = text.splitAt(position) 47 | fst + insertion + snd 48 | } 49 | 50 | def replaceAt(position: Int, newChar: Char): String = 51 | s"${text.take(position)}$newChar${text.substring(position + 1)}" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/xml/package.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils 2 | 3 | import scala.util.Try 4 | import scala.xml.Elem 5 | import scala.xml.Node 6 | 7 | import io.circe.Decoder 8 | import io.circe.Encoder 9 | import neotype.* 10 | import oolong.bson.* 11 | import oolong.bson.given 12 | import sttp.tapir.Schema 13 | 14 | import ru.tinkoff.tcb.generic.RootOptionFields 15 | 16 | package object xml { 17 | val emptyNode: Elem = 18 | 19 | object XMLString extends Newtype[Node] { 20 | def fromString(xml: String): Try[XMLString.Type] = 21 | Try(SafeXML.loadString(xml).asInstanceOf[Node]).map(this.unsafeMake) 22 | 23 | def fromNode(node: Node): XMLString.Type = this.unsafeMake(node) 24 | 25 | def unapply(str: String): Option[XMLString.Type] = 26 | fromString(str).toOption 27 | 28 | implicit val xmlStringDecoder: Decoder[XMLString.Type] = 29 | Decoder.decodeString.emapTry(fromString) 30 | 31 | implicit val xmlStringEncoder: Encoder[XMLString.Type] = 32 | Encoder.encodeString.contramap[XMLString.Type](_.asString) 33 | 34 | implicit val xmlStringBsonDecoder: BsonDecoder[XMLString.Type] = 35 | BsonDecoder[String].afterReadTry(fromString) 36 | 37 | implicit val xmlStringBsonEncoder: BsonEncoder[XMLString.Type] = 38 | BsonEncoder[String].beforeWrite(_.asString) 39 | 40 | implicit val xmlStringSchema: Schema[XMLString.Type] = 41 | Schema.schemaForString.as[XMLString.Type] 42 | 43 | implicit val xmlStringRof: RootOptionFields[XMLString.Type] = 44 | RootOptionFields.mk(Set.empty) 45 | } 46 | 47 | implicit class XMLStringSyntax(private val self: XMLString.Type) extends AnyVal { 48 | def asString: String = self.unwrap.toString 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /backend/mockingbird/src/test/resources/requests.proto: -------------------------------------------------------------------------------- 1 | syntax = 'proto3'; 2 | 3 | option java_package = 'ru.tinkoff.tcb.reference.grpc'; 4 | 5 | message BrandAndModelRequest { 6 | string vin = 1; 7 | } 8 | 9 | message CarGenRequest { 10 | int32 brandId = 1; 11 | int32 modelId = 2; 12 | int32 year = 3; 13 | } 14 | 15 | message CarSearchRequest { 16 | optional string text = 1; 17 | optional int32 year = 2; 18 | optional int32 brandId = 3; 19 | optional int32 limit = 4; 20 | optional string wuid = 5; 21 | } 22 | 23 | message CarConfigurationsRequest { 24 | int32 brandId = 1; 25 | int32 modelId = 2; 26 | int32 year = 3; 27 | optional int32 generationId = 4; 28 | optional bool allGenerations = 5; 29 | } 30 | 31 | enum Condition { 32 | NEW_CAR = 0; 33 | USED_CAR = 1; 34 | } 35 | 36 | message CarPriceRequest { 37 | int32 brandId = 1; 38 | int32 modelId = 2; 39 | optional int32 generationId = 3; 40 | optional int32 bodyId = 4; 41 | optional int32 modificationId = 5; 42 | optional int32 transmissionId = 6; 43 | optional int32 fuelId = 7; 44 | optional int32 wheelDriveId = 8; 45 | optional int32 engineId = 9; 46 | int32 year = 10; 47 | optional Condition condition = 11; 48 | } 49 | 50 | message PreciseCarPriceRequest { 51 | int64 configurationId = 1; 52 | optional Condition condition = 2; 53 | } 54 | 55 | message PriceRequest { 56 | oneof body { 57 | CarPriceRequest request = 2; 58 | PreciseCarPriceRequest preciseRequest = 3; 59 | } 60 | oneof body2 { 61 | string request2 = 4; 62 | int32 preciseRequest2 = 5; 63 | } 64 | string field = 1; 65 | } 66 | 67 | message MemoRequest { 68 | int32 brandId = 1; 69 | int32 modelId = 2; 70 | string wuid = 3; 71 | map req = 4; 72 | } -------------------------------------------------------------------------------- /backend/utils/src/main/scala/ru/tinkoff/tcb/utils/resource/Resource.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.utils.resource 2 | 3 | import ru.tinkoff.tcb.utils.`lazy`.Lazy 4 | 5 | /* 6 | Initial implementation was taken from 7 | https://bszwej.medium.com/composable-resource-management-in-scala-ce902bda48b2 8 | */ 9 | 10 | trait Resource[R] { 11 | def use[U](f: R => U): U 12 | 13 | def useAsIs: R = use(identity) 14 | } 15 | 16 | object Resource { 17 | def make[R](acquire: => R)(close: R => Unit): Resource[R] = 18 | new Resource[R] { 19 | override def use[U](f: R => U): U = { 20 | val resource = acquire 21 | try 22 | f(resource) 23 | finally 24 | close(resource) 25 | } 26 | } 27 | 28 | def lean[R](acquire: => R)(close: R => Unit): Resource[Lazy[R]] = 29 | new Resource[Lazy[R]] { 30 | override def use[U](f: Lazy[R] => U): U = { 31 | val resource = Lazy(acquire) 32 | try 33 | f(resource) 34 | finally 35 | if (resource.isComputed) 36 | close(resource.value) 37 | } 38 | } 39 | 40 | implicit val resourceMonad: Monad[Resource] = 41 | new Monad[Resource] with StackSafeMonad[Resource] { 42 | override def pure[R](r: R): Resource[R] = Resource.make(r)(_ => ()) 43 | 44 | override def map[A, B](r: Resource[A])(mapping: A => B): Resource[B] = 45 | new Resource[B] { 46 | override def use[U](f: B => U): U = r.use(a => f(mapping(a))) 47 | } 48 | 49 | override def flatMap[A, B](r: Resource[A])(mapping: A => Resource[B]): Resource[B] = 50 | new Resource[B] { 51 | override def use[U](f: B => U): U = 52 | r.use(res1 => mapping(res1).use(res2 => f(res2))) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /backend/mockingbird/src/test/scala/ru/tinkoff/tcb/protobuf/Utils.scala: -------------------------------------------------------------------------------- 1 | package ru.tinkoff.tcb.protobuf 2 | 3 | import java.io.File 4 | import java.io.FileInputStream 5 | import java.nio.file.Files 6 | import java.nio.file.Path 7 | import java.nio.file.Paths 8 | import scala.language.postfixOps 9 | import scala.reflect.io.Directory 10 | import scala.sys.process.* 11 | 12 | object Utils { 13 | 14 | private def openResource(resources: String) = { 15 | val cl = Thread.currentThread().getContextClassLoader() 16 | cl.getResourceAsStream(resources) 17 | } 18 | 19 | private val tmpPath: RIO[Scope, Path] = 20 | ZIO.acquireRelease(ZIO.attemptBlockingIO(Files.createTempDirectory("temp"))) { path => 21 | ZIO.attemptBlockingIO { 22 | val dir = new Directory(path.toFile) 23 | dir.deleteRecursively() 24 | }.orDie 25 | } 26 | 27 | def getProtoDescriptionFromResource(protoFile: String): RIO[Scope, Array[Byte]] = 28 | for { 29 | path <- tmpPath 30 | readStream <- ZIO.fromAutoCloseable(ZIO.attemptBlockingIO(openResource(protoFile))) 31 | writeStream <- ZIO.fromAutoCloseable { 32 | ZIO.attemptBlockingIO { 33 | Files.newOutputStream(Paths.get(s"${path.toString}/requests.proto")) 34 | } 35 | } 36 | _ <- ZIO.attemptBlockingIO(writeStream.write(readStream.readAllBytes())) 37 | _ <- ZIO.attemptBlockingIO { 38 | s"protoc --descriptor_set_out=${path.toString}/descriptor.desc --proto_path=${path.toString} requests.proto" ! 39 | } 40 | stream <- ZIO.fromAutoCloseable { 41 | ZIO.attemptBlockingIO(new FileInputStream(new File(s"${path.toString}/descriptor.desc"))) 42 | } 43 | content <- ZIO.attemptBlockingIO(stream.readAllBytes()) 44 | } yield content 45 | } 46 | --------------------------------------------------------------------------------