├── 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 |
--------------------------------------------------------------------------------
/frontend/src/mockingbird/layers/pages/mocks/Item.css:
--------------------------------------------------------------------------------
1 | .root {
2 | display: flex;
3 | justify-content: space-between;
4 | padding: 16px;
5 | word-break: break-word;
6 | cursor: pointer;
7 | }
8 |
9 | .block {
10 | &:last-child {
11 | min-width: 100px;
12 | margin-left: 16px;
13 | text-align: right;
14 | }
15 |
16 | & > * + * {
17 | margin-top: 4px;
18 | }
19 | }
20 |
21 | .tags {
22 | display: inline-flex;
23 | flex-wrap: wrap;
24 | max-width: 30rem;
25 |
26 | & > * {
27 | margin-left: 4px;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/model/Label.scala:
--------------------------------------------------------------------------------
1 | package ru.tinkoff.tcb.mockingbird.model
2 |
3 | import oolong.bson.*
4 | import oolong.bson.given
5 | import oolong.bson.meta.QueryMeta
6 | import oolong.bson.meta.queryMeta
7 |
8 | import ru.tinkoff.tcb.utils.id.SID
9 |
10 | final case class Label(
11 | id: SID[Label],
12 | serviceSuffix: String,
13 | label: String
14 | ) derives BsonDecoder,
15 | BsonEncoder
16 |
17 | object Label {
18 | inline given QueryMeta[Label] = queryMeta(_.id -> "_id")
19 | }
20 |
--------------------------------------------------------------------------------
/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/xttp/package.scala:
--------------------------------------------------------------------------------
1 | package ru.tinkoff.tcb.utils
2 |
3 | import sttp.client4.DuplicateHeaderBehavior
4 | import sttp.client4.PartialRequest
5 |
6 | package object xttp {
7 | implicit class PartialRequestTXtras[T](private val rqt: PartialRequest[T]) extends AnyVal {
8 | def headersReplacing(hs: Map[String, String]): PartialRequest[T] =
9 | hs.foldLeft(rqt) { case (request, (key, value)) =>
10 | request.header(key, value, DuplicateHeaderBehavior.Replace)
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/backend/mockingbird/src/main/resources/reference.conf:
--------------------------------------------------------------------------------
1 | ru.tinkoff.tcb {
2 | db {
3 | mongo {
4 | collections {
5 | stub = mockingbirdStubs
6 | state = mockingbirdStates
7 | scenario = mockingbirdScenarios
8 | service = mockingbirdServices
9 | label = mockingbirdLabels
10 | grpcStub = mockingbirdGrpcStubs
11 | grpcMethodDescription = mockingbirdGrpcMethodDescriptions
12 | source = mockingbirdSources
13 | destination = mockingbirdDestinations
14 | }
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/backend/edsl/src/main/scala/ru/tinkoff/tcb/mockingbird/edsl/model/Step.scala:
--------------------------------------------------------------------------------
1 | package ru.tinkoff.tcb.mockingbird.edsl.model
2 |
3 | import org.scalactic.source
4 |
5 | sealed trait Step[T]
6 |
7 | final case class Describe(text: String, pos: source.Position) extends Step[Unit]
8 |
9 | final case class SendHttp[R](
10 | request: HttpRequest,
11 | pos: source.Position,
12 | ) extends Step[R]
13 |
14 | final case class CheckHttp[R](
15 | response: R,
16 | expects: HttpResponseExpected,
17 | pos: source.Position,
18 | ) extends Step[HttpResponse]
19 |
--------------------------------------------------------------------------------
/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/request/SearchRequest.scala:
--------------------------------------------------------------------------------
1 | package ru.tinkoff.tcb.mockingbird.api.request
2 |
3 | import io.circe.Decoder
4 | import io.circe.Encoder
5 | import io.circe.Json
6 | import sttp.tapir.Schema
7 |
8 | import ru.tinkoff.tcb.predicatedsl.Keyword
9 | import ru.tinkoff.tcb.protocol.json.*
10 | import ru.tinkoff.tcb.protocol.schema.*
11 | import ru.tinkoff.tcb.utils.circe.optics.JsonOptic
12 |
13 | final case class SearchRequest(query: Map[JsonOptic, Map[Keyword.Json, Json]]) derives Encoder, Decoder, Schema
14 |
--------------------------------------------------------------------------------
/backend/dataAccess/src/main/scala/ru/tinkoff/tcb/dataaccess/UpdateResult.scala:
--------------------------------------------------------------------------------
1 | package ru.tinkoff.tcb.dataaccess
2 |
3 | final case class UpdateResult(matched: Long, modified: Long) {
4 | val successful: Boolean = matched > 0 || modified > 0
5 | val noMatch: Boolean = matched == 0
6 | val noOp: Boolean = modified == 0
7 | val unsuccessful: Boolean = noMatch && noOp
8 | }
9 |
10 | object UpdateResult {
11 | val empty: UpdateResult = UpdateResult(0, 0)
12 |
13 | def apply(affected: Long): UpdateResult = UpdateResult(affected, affected)
14 | }
15 |
--------------------------------------------------------------------------------
/backend/dataAccess/src/test/scala/ru/tinkoff/tcb/bson/enumeratum/values/EnumBsonHandlerSpec.scala:
--------------------------------------------------------------------------------
1 | package ru.tinkoff.tcb.bson.enumeratum.values
2 |
3 | import oolong.bson.given
4 | import org.scalatest.funspec.AnyFunSpec
5 | import org.scalatest.matchers.should.Matchers
6 |
7 | class EnumBsonHandlerSpec extends AnyFunSpec with Matchers with EnumBsonHandlerHelpers {
8 | describe(".reader") {
9 |
10 | testReader("IntEnum", BsonLibraryItem)
11 | testReader("LongEnum", BsonContentType)
12 | testReader("StringEnum", BsonOperatingSystem)
13 |
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/backend/dataAccess/src/test/scala/ru/tinkoff/tcb/generic/RootOptionFieldsSpec.scala:
--------------------------------------------------------------------------------
1 | package ru.tinkoff.tcb.generic
2 |
3 | import org.scalatest.funsuite.AnyFunSuite
4 | import org.scalatest.matchers.should.Matchers
5 |
6 | class RootOptionFieldsSpec extends AnyFunSuite with Matchers {
7 | case class Entity(
8 | id: Int,
9 | name: String,
10 | data: Option[Vector[String]],
11 | description: Option[String]
12 | )
13 |
14 | test("Fields are correct") {
15 | RootOptionFields[Entity].fields shouldBe Set("data", "description")
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/logging/LogContext.scala:
--------------------------------------------------------------------------------
1 | package ru.tinkoff.tcb.logging
2 |
3 | import tofu.logging.LoggedValue
4 |
5 | trait LogContext {
6 | def mdc(): Mdc
7 |
8 | def addToPayload(entries: (String, LoggedValue)*): LogContext = LogContext(mdc() ++ entries.toMap)
9 |
10 | def setTraceInfo(name: String, value: String): LogContext = LogContext(mdc().setTraceInfo(name, value))
11 | }
12 |
13 | object LogContext {
14 |
15 | val empty: LogContext = () => Mdc.empty
16 |
17 | def apply(mdc: Mdc): LogContext = () => mdc
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/frontend/src/mockingbird/modules/destinations/ui/Destinations.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 { useDestinations } from '../hooks';
5 |
6 | type Props = Omit & {
7 | serviceId: string;
8 | };
9 |
10 | export default function Destinations({ serviceId, ...props }: Props) {
11 | const destinations = useDestinations(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 |
--------------------------------------------------------------------------------